From c4dce82e6e016f79e2f2c67cdbaa350e75989f1a Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 20 Dec 2024 07:10:35 +0100 Subject: [PATCH 01/71] chore: remove duplicated tests for metrics for redesigned signatures (#29359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes the duplicated tests for redesigned signature metrics, the same metrics are checked in the tests in the folder : test/e2e/tests/confirmations/signatures [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29359?quickstart=1) ## **Related issues** Fixes: [29228](https://github.com/MetaMask/metamask-extension/issues/29228) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../tests/metrics/signature-approved.spec.js | 174 ------------------ 1 file changed, 174 deletions(-) diff --git a/test/e2e/tests/metrics/signature-approved.spec.js b/test/e2e/tests/metrics/signature-approved.spec.js index d0d1bb9c32c3..56e376f878ff 100644 --- a/test/e2e/tests/metrics/signature-approved.spec.js +++ b/test/e2e/tests/metrics/signature-approved.spec.js @@ -10,8 +10,6 @@ const { clickSignOnSignatureConfirmation, tempToggleSettingRedesignedConfirmations, validateContractDetails, - clickSignOnRedesignedSignatureConfirmation, - WINDOW_TITLES, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -60,10 +58,6 @@ const expectedEventPropertiesBase = { security_alert_response: 'loading', }; -const additionalRedesignEventProperties = { - ui_customizations: ['redesigned_confirmation'], -}; - describe('Signature Approved Event @no-mmi', function () { describe('Old confirmation screens', function () { it('Successfully tracked for signTypedData_v4', async function () { @@ -230,172 +224,4 @@ describe('Signature Approved Event @no-mmi', function () { ); }); }); - - describe('Redesigned confirmation screens', function () { - it('Successfully tracked for signTypedData_v4', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedDataV4'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v4', - eip712_primary_type: 'Mail', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v4', - eip712_primary_type: 'Mail', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for signTypedData_v3', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedDataV3'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v3', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData_v3', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for signTypedData', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#signTypedData'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'eth_signTypedData', - security_alert_response: 'Benign', - }); - }, - ); - }); - - it('Successfully tracked for personalSign', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#personalSign'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await clickSignOnRedesignedSignatureConfirmation({ driver }); - const events = await getEventPayloads(driver, mockedEndpoints); - - assert.deepStrictEqual(events[0].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'personal_sign', - }); - - assert.deepStrictEqual(events[1].properties, { - ...expectedEventPropertiesBase, - ...additionalRedesignEventProperties, - signature_type: 'personal_sign', - security_alert_response: 'Benign', - }); - }, - ); - }); - }); }); From fd3c51cf42b124277effd0a0f68e693f76872c45 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 20 Dec 2024 08:29:46 +0100 Subject: [PATCH 02/71] fix: Sanitize `signTypedDatav3v4` params before calling security API (#29343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR aims to filter request params before calling security API call if method is `signTypedDatav3v4` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29343?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3830 ## **Manual testing steps** 1. Copy the following payload ``` // Request the current account addresses from the Ethereum provider const addresses = await window.ethereum.request({ "method": "eth_accounts" }); // Construct the JSON string for eth_signTypedData_v4, including the dynamic owner address const jsonData = { domain: { name: "USD Coin", version: "2", chainId: "1", verifyingContract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }, types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ], Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }, primaryType: "Permit", message: { owner: addresses[0], spender: "0xa2d86c5ff6fbf5f455b1ba2737938776c24d7a58", value: "115792089237316195423570985008687907853269984665640564039457584007913129639935", nonce: "0", deadline: "115792089237316195423570985008687907853269984665640564039457584007913129639935" } }; // Use the first account address for signing the typed data window.ethereum.sendAsync({ method: "eth_signTypedData_v4", params: [ addresses[0], JSON.stringify(jsonData), {}, {}, {} ] }); ``` 2. Navigate to MM E2E Test Dapp > Connect Wallet > Open up the console > Paste the payload above > Hit enter 3. Notice that the transaction is considered as malicious (which was not flagged before) ## **Screenshots/Recordings** https://github.com/user-attachments/assets/ffcdd83f-bb79-4490-b729-f96559ce5769 ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/ppom/ppom-util.test.ts | 50 +++++++++++++++++++++++++- app/scripts/lib/ppom/ppom-util.ts | 20 ++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index d3ff3015ca41..c143f3dfe11b 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -10,7 +10,7 @@ import { SignatureController, SignatureRequest, } from '@metamask/signature-controller'; -import { Hex } from '@metamask/utils'; +import { Hex, JsonRpcRequest } from '@metamask/utils'; import { BlockaidReason, BlockaidResultType, @@ -22,6 +22,8 @@ import { AppStateController } from '../../controllers/app-state-controller'; import { generateSecurityAlertId, isChainSupported, + METHOD_SIGN_TYPED_DATA_V3, + METHOD_SIGN_TYPED_DATA_V4, updateSecurityAlertResponse, validateRequestWithPPOM, } from './ppom-util'; @@ -57,6 +59,10 @@ const TRANSACTION_PARAMS_MOCK_1: TransactionParams = { value: '0x123', }; +const SIGN_TYPED_DATA_PARAMS_MOCK_1 = '0x123'; +const SIGN_TYPED_DATA_PARAMS_MOCK_2 = + '{"primaryType":"Permit","domain":{},"types":{}}'; + const TRANSACTION_PARAMS_MOCK_2: TransactionParams = { ...TRANSACTION_PARAMS_MOCK_1, to: '0x456', @@ -261,6 +267,48 @@ describe('PPOM Utils', () => { ); }); + // @ts-expect-error This is missing from the Mocha type definitions + it.each([METHOD_SIGN_TYPED_DATA_V3, METHOD_SIGN_TYPED_DATA_V4])( + 'sanitizes request params if method is %s', + async (method: string) => { + const ppom = createPPOMMock(); + const ppomController = createPPOMControllerMock(); + + ppomController.usePPOM.mockImplementation( + (callback) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback(ppom as any) as any, + ); + + const firstTwoParams = [ + SIGN_TYPED_DATA_PARAMS_MOCK_1, + SIGN_TYPED_DATA_PARAMS_MOCK_2, + ]; + + const unwantedParams = [{}, undefined, 1, null]; + + const params = [...firstTwoParams, ...unwantedParams]; + + const request = { + ...REQUEST_MOCK, + method, + params, + } as unknown as JsonRpcRequest; + + await validateRequestWithPPOM({ + ...validateRequestWithPPOMOptionsBase, + ppomController, + request, + }); + + expect(ppom.validateJsonRpc).toHaveBeenCalledTimes(1); + expect(ppom.validateJsonRpc).toHaveBeenCalledWith({ + ...request, + params: firstTwoParams, + }); + }, + ); + it('updates response indicating chain is not supported', async () => { const ppomController = {} as PPOMController; const CHAIN_ID_UNSUPPORTED_MOCK = '0x2'; diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 7dbf8c92ec5f..0407c0604a69 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -29,6 +29,8 @@ import { const { sentry } = global; const METHOD_SEND_TRANSACTION = 'eth_sendTransaction'; +export const METHOD_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3'; +export const METHOD_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4'; const SECURITY_ALERT_RESPONSE_ERROR = { result_type: BlockaidResultType.Errored, @@ -171,7 +173,7 @@ function normalizePPOMRequest( request, ) ) { - return request; + return sanitizeRequest(request); } const transactionParams = request.params[0]; @@ -183,6 +185,22 @@ function normalizePPOMRequest( }; } +function sanitizeRequest(request: JsonRpcRequest): JsonRpcRequest { + // This is a temporary fix to prevent a PPOM bypass + if ( + request.method === METHOD_SIGN_TYPED_DATA_V4 || + request.method === METHOD_SIGN_TYPED_DATA_V3 + ) { + if (Array.isArray(request.params)) { + return { + ...request, + params: request.params.slice(0, 2), + }; + } + } + return request; +} + function getErrorMessage(error: unknown) { if (error instanceof Error) { return `${error.name}: ${error.message}`; From d4c5a7368df7ce83ea8d63caf6fe2f592f3aaa28 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:46:25 +0700 Subject: [PATCH 03/71] fix: Network URL toPunycodeUrl preserve no path slash (#29325) 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/29325?quickstart=1) ## **Related issues** Related to: https://github.com/MetaMask/metamask-extension/pull/29322 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../confirmation/templates/add-ethereum-chain.test.js | 1 + ui/pages/confirmations/utils/confirm.test.ts | 9 ++++++--- ui/pages/confirmations/utils/confirm.ts | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js index 19f51b6fa798..50fb4a04e4c7 100644 --- a/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js +++ b/ui/pages/confirmations/confirmation/templates/add-ethereum-chain.test.js @@ -159,6 +159,7 @@ describe('add-ethereum-chain confirmation', () => { "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended site before you continue. Punycode version: https://xn--ifura-dig.io/gnosis", ), ).toBeInTheDocument(); + expect(getByText('https://iոfura.io/gnosis')).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/confirmations/utils/confirm.test.ts b/ui/pages/confirmations/utils/confirm.test.ts index 9a8b3d1a0f8a..a81b5959a916 100644 --- a/ui/pages/confirmations/utils/confirm.test.ts +++ b/ui/pages/confirmations/utils/confirm.test.ts @@ -93,8 +93,11 @@ describe('confirm util', () => { expect(toPunycodeURL('https://iոfura.io/gnosis')).toStrictEqual( 'https://xn--ifura-dig.io/gnosis', ); - expect(toPunycodeURL('https://www.google.com')).toStrictEqual( - 'https://www.google.com/', + expect(toPunycodeURL('https://iոfura.io')).toStrictEqual( + 'https://xn--ifura-dig.io', + ); + expect(toPunycodeURL('https://iոfura.io/')).toStrictEqual( + 'https://xn--ifura-dig.io/', ); expect( toPunycodeURL('https://iոfura.io/gnosis:5050?test=iոfura&foo=bar'), @@ -102,7 +105,7 @@ describe('confirm util', () => { 'https://xn--ifura-dig.io/gnosis:5050?test=i%D5%B8fura&foo=bar', ); expect(toPunycodeURL('https://www.google.com')).toStrictEqual( - 'https://www.google.com/', + 'https://www.google.com', ); }); }); diff --git a/ui/pages/confirmations/utils/confirm.ts b/ui/pages/confirmations/utils/confirm.ts index f464f51e8159..379d98728be2 100644 --- a/ui/pages/confirmations/utils/confirm.ts +++ b/ui/pages/confirmations/utils/confirm.ts @@ -80,7 +80,12 @@ export const isValidASCIIURL = (urlString?: string) => { export const toPunycodeURL = (urlString: string) => { try { - return new URL(urlString).href; + const url = new URL(urlString); + const { protocol, hostname, port, search, hash } = url; + const pathname = + url.pathname === '/' && !urlString.endsWith('/') ? '' : url.pathname; + + return `${protocol}//${hostname}${port}${pathname}${search}${hash}`; } catch (err: unknown) { console.error(`Failed to convert URL to Punycode: ${err}`); return undefined; From 367769b9e299428e836e6e9d5bb49a1ebbf04cf0 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:19:26 +0100 Subject: [PATCH 04/71] test: [POM] Dapp subscribe network switch spec migration (#29346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the `test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js` spec to use page object model and typescript. It also updates the assertions to test what it's intended. 1. We go to the test dapp and connect 2. We subscribe to the newHeads event ``` await window.ethereum.request({ "method": "eth_subscribe", "params": [ "newHeads" ], }); ``` 3. We add an event listener for subscribe messages, and we'll store this into a window variable, so we can access it later ``` window.ethereum.on('message', (message) => { if (message.type === 'eth_subscription' && message.data.subscription === '0x4bc2639eb3ac769db7a90f60a47b33c4') { console.log('New block header:', message.data.result); } }) ``` 4. We switch networks from MM wide screen 5. We go back to the dapp 6. We mine a block deterministically --> In ganache we have setup auto-mining by default, however this happens every some seconds, by performing a mine ourselves, we know for sure that this happened at least once at the point we want 7. We wait a couple of seconds to see if more event logs appear 8. We assert that we got more events, after switching networks [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29346?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/29348 ## **Manual testing steps** 1. Check ci continues to pass 3. Run spec manually `yarn test:e2e:single test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts --browser=chrome --leave-running=true ## **Screenshots/Recordings** Messages when we console log them in the spec ![Screenshot from 2024-12-19 12-04-23](https://github.com/user-attachments/assets/7d55edc0-107f-419f-9f2a-d7bdbe3fc80a) This is the flow that happens in the spec, done manually. https://github.com/user-attachments/assets/cd68d1ac-c0ac-4bd2-b089-fec605ffed6d ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/seeder/ganache.ts | 7 ++ .../dapp1-subscribe-network-switch.spec.js | 95 ------------------ .../dapp1-subscribe-network-switch.spec.ts | 98 +++++++++++++++++++ 3 files changed, 105 insertions(+), 95 deletions(-) delete mode 100644 test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js create mode 100644 test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts diff --git a/test/e2e/seeder/ganache.ts b/test/e2e/seeder/ganache.ts index 7fa3d1ab0238..d262924ab61e 100644 --- a/test/e2e/seeder/ganache.ts +++ b/test/e2e/seeder/ganache.ts @@ -76,6 +76,13 @@ export class Ganache { }); } + async mineBlock() { + return await this.getProvider()?.request({ + method: 'evm_mine', + params: [], + }); + } + async quit() { if (!this.#server) { throw new Error('Server not running yet'); 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 deleted file mode 100644 index 53c763d8891f..000000000000 --- a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.js +++ /dev/null @@ -1,95 +0,0 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { - withFixtures, - openDapp, - unlockWallet, - DAPP_URL, - regularDelayMs, - WINDOW_TITLES, - switchToNotificationWindow, - defaultGanacheOptions, -} = require('../../helpers'); - -describe('Request Queueing', function () { - it('should keep subscription on dapp network when switching different mm network', async function () { - const port = 8546; - const chainId = 1338; - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() - .build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [ - { - port, - chainId, - ganacheOptions2: defaultGanacheOptions, - }, - ], - }, - title: this.test.fullTitle(), - }, - - async ({ driver }) => { - await unlockWallet(driver); - - await openDapp(driver, undefined, DAPP_URL); - - // 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', - }); - - // Navigate to test dapp - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - 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'); - - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); - - // Switch to second network - await driver.clickElement({ - text: 'Localhost 8546', - css: 'p', - }); - - assert.strictEqual(subscribe, subscriptionMessage.data.subscription); - assert.strictEqual(subscriptionMessage.type, 'eth_subscription'); - }, - ); - }); -}); diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts new file mode 100644 index 000000000000..ec607fc34936 --- /dev/null +++ b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts @@ -0,0 +1,98 @@ +import { strict as assert } from 'assert'; +import FixtureBuilder from '../../fixture-builder'; +import { + defaultGanacheOptions, + WINDOW_TITLES, + withFixtures, +} from '../../helpers'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { loginWithoutBalanceValidation } from '../../page-objects/flows/login.flow'; +import { switchToNetworkFlow } from '../../page-objects/flows/network.flow'; + +describe('Request Queueing', function () { + it('should keep subscription on dapp network when switching different mm network', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerDoubleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test?.fullTitle(), + }, + + async ({ driver, ganacheServer }) => { + await loginWithoutBalanceValidation(driver); + + // Connect to dapp + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await testDapp.check_pageIsLoaded(); + await testDapp.connectAccount({}); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Subscribe to newHeads event + const subscribeRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_subscribe', + params: ['newHeads'], + }); + + await driver.executeScript( + `return window.ethereum.request(${subscribeRequest})`, + ); + + // Save event logs into the messages variable in the window context, to access it later + await driver.executeScript(` + window.messages = []; + window.ethereum.on('message', (message) => { + if (message.type === 'eth_subscription') { + console.log('New block header:', message.data.result); + window.messages.push(message.data.result); + } + }); + `); + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Switch networks + await switchToNetworkFlow(driver, 'Localhost 8546'); + + // Navigate back to the test dapp + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // Access to the window messages variable + const messagesBeforeMining = await driver.executeScript( + 'return window.messages;', + ); + + // Mine a block deterministically + await ganacheServer.mineBlock(); + + // Wait a couple of seconds for the logs to populate into the messages window variable + await driver.delay(5000); + + // Access the window messages variable and check there are more events than before mining + const messagesAfterMining = await driver.executeScript( + 'return window.messages;', + ); + + assert.ok(messagesBeforeMining.length < messagesAfterMining.length); + }, + ); + }); +}); From fd6e75559bc374698e84a1a4c42808e542c2838a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 20 Dec 2024 10:34:24 +0100 Subject: [PATCH 05/71] fix: Use `toUnicode` function to normalize ens domains in the UI (#29231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In `ENSController`(https://github.com/MetaMask/core/blob/main/packages/ens-controller/src/EnsController.ts#L375) right before saving the ens domain in the state we use `toASCII`. This function is typically used to convert a domain name from its Unicode representation to ASCII, specifically using the `Punycode` package encoding. This is necessary because the Domain Name System (DNS) operates with ASCII characters, and internationalized domain names (IDNs) need to be converted to a format that DNS can understand. On the other side, in the client, we are not converting/normalizing this domain value. That is causing that unwanted ASCII coded domain in the UI when using smileys in the ENS domain. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29231?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28610 ## **Manual testing steps** See https://github.com/MetaMask/metamask-extension/issues/28610 for repro steps ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-16 at 13 37 04](https://github.com/user-attachments/assets/62163ce0-007a-404b-8f2e-7a49eaa7b927) ### **After** ![Screenshot 2024-12-16 at 13 36 32](https://github.com/user-attachments/assets/cd287cb0-aa81-49da-aafb-1e753d8544e7) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/selectors/selectors.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 61c15000baba..403db6cadb91 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1,3 +1,4 @@ +import { toUnicode } from 'punycode/punycode.js'; import { SubjectType } from '@metamask/permission-controller'; import { ApprovalType } from '@metamask/controller-utils'; import { @@ -729,7 +730,10 @@ export function getAddressBook(state) { export function getEnsResolutionByAddress(state, address) { if (state.metamask.ensResolutionsByAddress[address]) { - return state.metamask.ensResolutionsByAddress[address]; + const ensResolution = state.metamask.ensResolutionsByAddress[address]; + // ensResolution is a punycode encoded string hence toUnicode is used to decode it from same package + const normalizedEnsResolution = toUnicode(ensResolution); + return normalizedEnsResolution; } const entry = From a33f52631f87275f86459b74d0e7ae12511b954c Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 20 Dec 2024 15:26:18 +0530 Subject: [PATCH 06/71] fix: UI is not displaying gas limit set by dapp (#29352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** UI is not displaying correct gas limit set by dapp. It always displays `21000`. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/18417 ## **Manual testing steps** 1. Submit confirmation request with gas limit not `21000` 2. Check gas limit in gas editing popup ## **Screenshots/Recordings** Screenshot 2024-12-19 at 7 20 45 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/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/pages/confirmations/hooks/useGasFeeInputs.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/pages/confirmations/hooks/useGasFeeInputs.js b/ui/pages/confirmations/hooks/useGasFeeInputs.js index 8675b726038a..129778bfcf20 100644 --- a/ui/pages/confirmations/hooks/useGasFeeInputs.js +++ b/ui/pages/confirmations/hooks/useGasFeeInputs.js @@ -164,7 +164,11 @@ export function useGasFeeInputs( }); const [gasLimit, setGasLimit] = useState(() => - Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')), + Number( + hexToDecimal( + transaction?.txParams?.gasLimit ?? transaction?.txParams?.gas ?? '0x0', + ), + ), ); const properGasLimit = Number(hexToDecimal(transaction?.originalGasEstimate)); @@ -195,7 +199,15 @@ export function useGasFeeInputs( setEstimateUsed(transaction?.userFeeLevel); } - setGasLimit(Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0'))); + setGasLimit( + Number( + hexToDecimal( + transaction?.txParams?.gasLimit ?? + transaction?.txParams?.gas ?? + '0x0', + ), + ), + ); } }, [ setEstimateUsed, From 356ad476f75183e17f9493b75625575daedfd018 Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:38:04 +0100 Subject: [PATCH 07/71] test: [POM] Migrate watch account tests (#29314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Created a new page class `AccountDetailsModal`. Previously, it was a part of `AccountList`. I think it's better to separate it and make it an independent class. - I also took the chance to improve the function `addAccount` and remove the origin `addNewAccount`. So now for creating ethereum, bitcoin, solana accounts, we use the same `addAccount` function with the account type as a parameter. - Migrate watch account e2e tests to Page Object Model - Created `watchEoaAddress` flow that can be reusable. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/constants.ts | 7 + test/e2e/flask/btc/common-btc.ts | 4 +- test/e2e/flask/btc/create-btc-account.spec.ts | 37 +- test/e2e/flask/create-watch-account.spec.ts | 407 +++++++----------- test/e2e/flask/solana/common-solana.ts | 7 +- .../solana/create-solana-account.spec.ts | 7 +- test/e2e/page-objects/common.ts | 6 - .../page-objects/flows/watch-account.flow.ts | 22 + .../page-objects/pages/account-list-page.ts | 194 ++++----- .../pages/dialog/account-details-modal.ts | 106 +++++ test/e2e/page-objects/pages/header-navbar.ts | 26 ++ .../pages/home/bitcoin-homepage.ts | 10 +- test/e2e/page-objects/pages/home/homepage.ts | 50 ++- .../pages/settings/experimental-settings.ts | 20 + .../tests/account/account-custom-name.spec.ts | 14 +- test/e2e/tests/account/add-account.spec.ts | 13 +- test/e2e/tests/account/import-flow.spec.ts | 5 +- .../account-syncing/new-user-sync.spec.ts | 6 +- .../onboarding-with-opt-out.spec.ts | 7 +- .../sync-after-adding-account.spec.ts | 10 +- .../sync-after-modifying-account-name.spec.ts | 11 +- .../sync-with-account-balances.spec.ts | 14 +- test/e2e/tests/tokens/nft/import-nft.spec.ts | 5 +- 23 files changed, 559 insertions(+), 429 deletions(-) create mode 100644 test/e2e/page-objects/flows/watch-account.flow.ts create mode 100644 test/e2e/page-objects/pages/dialog/account-details-modal.ts diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 9d4d5bbf3c8d..6af1056d7232 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -76,3 +76,10 @@ export const DEFAULT_SOLANA_BALANCE = 1; // SOL /* Title of the mocked E2E test empty HTML page */ export const EMPTY_E2E_TEST_PAGE_TITLE = 'E2E Test Page'; + +/* Account types */ +export enum ACCOUNT_TYPE { + Ethereum, + Bitcoin, + Solana, +} diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index db2db85eb554..ea9cb6b1f2e6 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -2,6 +2,7 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures } from '../../helpers'; import { + ACCOUNT_TYPE, DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE, DEFAULT_BTC_FEES_RATE, @@ -14,7 +15,6 @@ import { Driver } from '../../webdriver/driver'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u; @@ -218,7 +218,7 @@ export async function withBtcAccountSnap( await new HeaderNavbar(driver).openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await test(driver, mockServer); }, ); diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts index f563454ad1e6..68cdcd82caf5 100644 --- a/test/e2e/flask/btc/create-btc-account.spec.ts +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import { WALLET_PASSWORD } from '../../helpers'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import LoginPage from '../../page-objects/pages/login-page'; import PrivacySettings from '../../page-objects/pages/settings/privacy-settings'; import ResetPasswordPage from '../../page-objects/pages/reset-password-page'; import SettingsPage from '../../page-objects/pages/settings/settings-page'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { withBtcAccountSnap } from './common-btc'; describe('Create BTC Account', function (this: Suite) { @@ -82,9 +83,11 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + const accountAddress = await accountDetailsModal.getAccountAddress(); await headerNavbar.openAccountMenu(); await accountListPage.removeAccount('Bitcoin Account'); @@ -97,14 +100,15 @@ describe('Create BTC Account', function (this: Suite) { ); await accountListPage.closeAccountModal(); await headerNavbar.openAccountMenu(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - const recreatedAccountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + await accountDetailsModal.check_pageIsLoaded(); + const recreatedAccountAddress = + await accountDetailsModal.getAccountAddress(); assert(accountAddress === recreatedAccountAddress); }, @@ -123,9 +127,10 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - const accountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + const accountAddress = await accountDetailsModal.getAccountAddress(); // go to privacy settings page and get the SRP await headerNavbar.openSettingsPage(); @@ -151,14 +156,16 @@ describe('Create BTC Account', function (this: Suite) { await headerNavbar.check_pageIsLoaded(); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, ''); + await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin }); await headerNavbar.check_accountLabel('Bitcoin Account'); await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - const recreatedAccountAddress = await accountListPage.getAccountAddress( - 'Bitcoin Account', - ); + await accountListPage.openAccountDetailsModal('Bitcoin Account'); + await accountDetailsModal.check_pageIsLoaded(); + const recreatedAccountAddress = + await accountDetailsModal.getAccountAddress(); + assert(accountAddress === recreatedAccountAddress); }, ); diff --git a/test/e2e/flask/create-watch-account.spec.ts b/test/e2e/flask/create-watch-account.spec.ts index f25f38f0b2ce..5cfca1dda5ef 100644 --- a/test/e2e/flask/create-watch-account.spec.ts +++ b/test/e2e/flask/create-watch-account.spec.ts @@ -1,70 +1,22 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import messages from '../../../app/_locales/en/messages.json'; import FixtureBuilder from '../fixture-builder'; -import { defaultGanacheOptions, unlockWallet, withFixtures } from '../helpers'; +import { withFixtures } from '../helpers'; import { Driver } from '../webdriver/driver'; +import AccountDetailsModal from '../page-objects/pages/dialog/account-details-modal'; +import AccountListPage from '../page-objects/pages/account-list-page'; +import ExperimentalSettings from '../page-objects/pages/settings/experimental-settings'; +import HeaderNavbar from '../page-objects/pages/header-navbar'; +import HomePage from '../page-objects/pages/home/homepage'; +import SettingsPage from '../page-objects/pages/settings/settings-page'; +import { loginWithBalanceValidation } from '../page-objects/flows/login.flow'; +import { watchEoaAddress } from '../page-objects/flows/watch-account.flow'; const ACCOUNT_1 = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; const EOA_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; const SHORTENED_EOA_ADDRESS = '0xd8dA6...96045'; const DEFAULT_WATCHED_ACCOUNT_NAME = 'Watched Account 1'; -/** - * Start the flow to create a watch account by clicking the account menu and selecting the option to add a watch account. - * - * @param driver - The WebDriver instance used to control the browser. - * @param unlockWalletFirst - Whether to unlock the wallet before starting the flow. - */ -async function startCreateWatchAccountFlow( - driver: Driver, - unlockWalletFirst: boolean = true, -): Promise { - if (unlockWalletFirst) { - await unlockWallet(driver); - } - - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-add-watch-only-account"]', - ); -} - -/** - * Watches an EOA address. - * - * @param driver - The WebDriver instance used to control the browser. - * @param unlockWalletFirst - Whether to unlock the wallet before watching the address. - * @param address - The EOA address to watch. - */ -async function watchEoaAddress( - driver: Driver, - unlockWalletFirst: boolean = true, - address: string = EOA_ADDRESS, -): Promise { - await startCreateWatchAccountFlow(driver, unlockWalletFirst); - await driver.fill('input#address-input[type="text"]', address); - await driver.clickElement({ text: 'Watch account', tag: 'button' }); - await driver.clickElement('[data-testid="submit-add-account-with-name"]'); -} - -/** - * Removes the selected account. - * - * @param driver - The WebDriver instance used to control the browser. - */ -async function removeSelectedAccount(driver: Driver): Promise { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', - ); - await driver.clickElement('[data-testid="account-list-menu-remove"]'); - await driver.clickElement({ text: 'Remove', tag: 'button' }); -} - describe('Account-watcher snap', function (this: Suite) { describe('Adding watched accounts', function () { it('adds watch account with valid EOA address', async function () { @@ -76,22 +28,17 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); // new account should be displayed in the account list - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.findElement({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + await headerNavbar.check_accountAddress(SHORTENED_EOA_ADDRESS); }, ); }); @@ -105,40 +52,29 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + const homePage = new HomePage(driver); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); // 'Send' button should be disabled - await driver.findElement( - '[data-testid="eth-overview-send"][disabled]', - ); - await driver.findElement( - '[data-testid="eth-overview-send"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifSendButtonIsClickable(), false); // 'Swap' button should be disabled - await driver.findElement( - '[data-testid="token-overview-button-swap"][disabled]', - ); - await driver.findElement( - '[data-testid="token-overview-button-swap"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifSwapButtonIsClickable(), false); // 'Bridge' button should be disabled - await driver.findElement( - '[data-testid="eth-overview-bridge"][disabled]', - ); - await driver.findElement( - '[data-testid="eth-overview-bridge"].icon-button--disabled', - ); + assert.equal(await homePage.check_ifBridgeButtonIsClickable(), false); // check tooltips for disabled buttons - await driver.findElement( - '.icon-button--disabled [data-tooltipped][data-original-title="Not supported with this account."]', + await homePage.check_disabledButtonTooltip( + 'Not supported with this account.', ); }, ); @@ -182,20 +118,19 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await startCreateWatchAccountFlow(driver); - - await driver.fill('input#address-input[type="text"]', input); - await driver.clickElement({ text: 'Watch account', tag: 'button' }); - - // error message should be displayed by the snap - await driver.findElement({ - css: '.snap-ui-renderer__text', - text: message, - }); + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // error message should be displayed by snap when try to watch an EOA with invalid input + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addEoaAccount(input, message); }, ); }); @@ -216,30 +151,23 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address for ACCOUNT_2 - await watchEoaAddress(driver, true, ACCOUNT_2); - - // try to import private key of watched ACCOUNT_2 address - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - await driver.clickElement({ text: 'Import account', tag: 'button' }); - await driver.findClickableElement('#private-key-box'); - await driver.fill('#private-key-box', PRIVATE_KEY_TWO); - await driver.clickElement( - '[data-testid="import-account-confirm-button"]', + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, ACCOUNT_2); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + + // try to import private key of watched ACCOUNT_2 address and check error message + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addNewImportedAccount( + PRIVATE_KEY_TWO, + 'KeyringController - The account you are trying to import is a duplicate', ); - - // error message should be displayed - await driver.findElement({ - css: '.mm-box--color-error-default', - text: 'KeyringController - The account you are trying to import is a duplicate', - }); }, ); }); @@ -253,62 +181,27 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - // watch an EOA address - await watchEoaAddress(driver); - - // click to view account details - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement( - '[data-testid="account-list-menu-details"]', - ); - // 'Show private key' button should not be displayed - await driver.assertElementNotPresent({ - css: 'button', - text: 'Show private key', - }); - }, - ); - }); - - it('removes a watched account', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder() - .withPreferencesControllerAndFeatureFlag({ - watchEthereumAccountEnabled: true, - }) - .withNetworkControllerOnMainnet() - .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); - - // remove the selected watched account - await removeSelectedAccount(driver); - - // account should be removed from the account list - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.assertElementNotPresent({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + + // open account details modal in header navbar + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel(DEFAULT_WATCHED_ACCOUNT_NAME); + await headerNavbar.openAccountDetailsModal(); + + // check 'Show private key' button should not be displayed + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.check_showPrivateKeyButtonIsNotDisplayed(); }, ); }); - it('can remove and recreate a watched account', async function () { + it('removes a watched account and recreate a watched account', async function () { await withFixtures( { fixtures: new FixtureBuilder() @@ -317,113 +210,83 @@ describe('Account-watcher snap', function (this: Suite) { }) .withNetworkControllerOnMainnet() .build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { // watch an EOA address - await watchEoaAddress(driver); + await loginWithBalanceValidation(driver); + await watchEoaAddress(driver, EOA_ADDRESS); + const homePage = new HomePage(driver); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); // remove the selected watched account - await removeSelectedAccount(driver); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.removeAccount(DEFAULT_WATCHED_ACCOUNT_NAME); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); // account should be removed from the account list - await driver.assertElementNotPresent({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.assertElementNotPresent({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); - - // watch the same EOA address again - await watchEoaAddress(driver, false); - - // same account should be displayed in the account list - await driver.findElement({ - css: '[data-testid="account-menu-icon"]', - text: DEFAULT_WATCHED_ACCOUNT_NAME, - }); - await driver.findElement({ - css: '.mm-text--ellipsis', - text: SHORTENED_EOA_ADDRESS, - }); + await homePage.headerNavbar.openAccountMenu(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); + await accountListPage.closeAccountModal(); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // watch the same EOA address again and check the account is recreated + await watchEoaAddress(driver, EOA_ADDRESS); + await homePage.headerNavbar.check_accountLabel( + DEFAULT_WATCHED_ACCOUNT_NAME, + ); + await homePage.headerNavbar.check_accountAddress( + SHORTENED_EOA_ADDRESS, + ); }, ); }); }); describe('Experimental toggle', function () { - const navigateToExperimentalSettings = async (driver: Driver) => { - await driver.clickElement('[data-testid="account-options-menu-button"]'); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ text: 'Experimental', tag: 'div' }); - await driver.waitForSelector({ - text: messages.watchEthereumAccountsToggle.message, - tag: 'span', - }); - }; - - const getToggleState = async (driver: Driver): Promise => { - const toggleInput = await driver.findElement( - '[data-testid="watch-account-toggle"]', - ); - return toggleInput.isSelected(); - }; - - const toggleWatchAccountOptionAndCloseSettings = async (driver: Driver) => { - await driver.clickElement('[data-testid="watch-account-toggle-div"]'); - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); - }; - - const verifyWatchAccountOptionAndCloseMenu = async ( - driver: Driver, - shouldBePresent: boolean, - ) => { - await driver.clickElement('[data-testid="account-menu-icon"]'); - await driver.clickElement( - '[data-testid="multichain-account-menu-popover-action-button"]', - ); - if (shouldBePresent) { - await driver.waitForSelector({ - text: messages.addEthereumWatchOnlyAccount.message, - tag: 'button', - }); - } else { - await driver.assertElementNotPresent({ - text: messages.addEthereumWatchOnlyAccount.message, - tag: 'button', - }); - } - await driver.clickElement('header button[aria-label="Close"]'); - }; - it("will show the 'Watch an Ethereum account (Beta)' option when setting is enabled", async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); - await navigateToExperimentalSettings(driver); - - // verify toggle is off by default + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // navigate to experimental settings + await homePage.headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + const experimentalSettings = new ExperimentalSettings(driver); + await experimentalSettings.check_pageIsLoaded(); + + // verify watch account toggle is off by default and enable the toggle assert.equal( - await getToggleState(driver), + await experimentalSettings.getWatchAccountToggleState(), false, 'Toggle should be off by default', ); - - // enable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await experimentalSettings.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is available - await verifyWatchAccountOptionAndCloseMenu(driver, true); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(true); }, ); }); @@ -432,27 +295,49 @@ describe('Account-watcher snap', function (this: Suite) { await withFixtures( { fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); - await navigateToExperimentalSettings(driver); - - // enable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await loginWithBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + + // navigate to experimental settings and enable the toggle + await homePage.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.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is available - await verifyWatchAccountOptionAndCloseMenu(driver, true); - - // navigate back to experimental settings - await navigateToExperimentalSettings(driver); - - // disable the toggle - await toggleWatchAccountOptionAndCloseSettings(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(true); + await accountListPage.closeAccountModal(); + + // navigate back to experimental settings and disable the toggle + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openSettingsPage(); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToExperimentalSettings(); + await experimentalSettings.check_pageIsLoaded(); + await experimentalSettings.toggleWatchAccount(); + await settingsPage.closeSettingsPage(); // verify the 'Watch and Ethereum account (Beta)' option is not available - await verifyWatchAccountOptionAndCloseMenu(driver, false); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); + await homePage.headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_addWatchAccountAvailable(false); }, ); }); diff --git a/test/e2e/flask/solana/common-solana.ts b/test/e2e/flask/solana/common-solana.ts index 2ac107bb442c..ac36d5ebed86 100644 --- a/test/e2e/flask/solana/common-solana.ts +++ b/test/e2e/flask/solana/common-solana.ts @@ -4,7 +4,7 @@ import { Driver } from '../../webdriver/driver'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; import FixtureBuilder from '../../fixture-builder'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; const SOLANA_URL_REGEX = /^https:\/\/.*\.solana.*/u; @@ -64,7 +64,10 @@ export async function withSolanaAccountSnap( const headerComponen = new HeaderNavbar(driver); await headerComponen.openAccountMenu(); const accountListPage = new AccountListPage(driver); - await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 1'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Solana, + accountName: 'Solana 1', + }); await test(driver, mockServer); }, ); diff --git a/test/e2e/flask/solana/create-solana-account.spec.ts b/test/e2e/flask/solana/create-solana-account.spec.ts index cca4f5222993..0cac9fab7375 100644 --- a/test/e2e/flask/solana/create-solana-account.spec.ts +++ b/test/e2e/flask/solana/create-solana-account.spec.ts @@ -1,7 +1,7 @@ import { Suite } from 'mocha'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import AccountListPage from '../../page-objects/pages/account-list-page'; -import { ACCOUNT_TYPE } from '../../page-objects/common'; +import { ACCOUNT_TYPE } from '../../constants'; import { withSolanaAccountSnap } from './common-solana'; // Scenarios skipped due to https://consensyssoftware.atlassian.net/browse/SOL-87 @@ -17,7 +17,10 @@ describe('Create Solana Account', function (this: Suite) { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_accountDisplayedInAccountList('Account 1'); - await accountListPage.addAccount(ACCOUNT_TYPE.Solana, 'Solana 2'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Solana, + accountName: 'Solana 2', + }); await headerNavbar.check_accountLabel('Solana 2'); await headerNavbar.openAccountMenu(); await accountListPage.check_numberOfAvailableAccounts(3); diff --git a/test/e2e/page-objects/common.ts b/test/e2e/page-objects/common.ts index 40eb625d94ac..5bf1a91e1859 100644 --- a/test/e2e/page-objects/common.ts +++ b/test/e2e/page-objects/common.ts @@ -2,9 +2,3 @@ export type RawLocator = | string | { css?: string; text?: string } | { tag: string; text: string }; - -export enum ACCOUNT_TYPE { - Ethereum, - Bitcoin, - Solana, -} diff --git a/test/e2e/page-objects/flows/watch-account.flow.ts b/test/e2e/page-objects/flows/watch-account.flow.ts new file mode 100644 index 000000000000..8481c71599a6 --- /dev/null +++ b/test/e2e/page-objects/flows/watch-account.flow.ts @@ -0,0 +1,22 @@ +import { Driver } from '../../webdriver/driver'; +import HomePage from '../pages/home/homepage'; +import AccountListPage from '../pages/account-list-page'; + +/** + * Initiates the flow of watching an EOA address. + * + * @param driver - The WebDriver instance. + * @param address - The EOA address that is to be watched. + */ +export async function watchEoaAddress( + driver: Driver, + address: string, +): Promise { + // watch a new EOA + const homePage = new HomePage(driver); + await homePage.headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addEoaAccount(address); + await homePage.check_pageIsLoaded(); +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 628d758e7e71..92eede81652b 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,13 +1,11 @@ import { Driver } from '../../webdriver/driver'; import { largeDelayMs, regularDelayMs } from '../../helpers'; import messages from '../../../../app/_locales/en/messages.json'; -import { ACCOUNT_TYPE } from '../common'; +import { ACCOUNT_TYPE } from '../../constants'; class AccountListPage { private readonly driver: Driver; - private readonly accountAddressText = '.qr-code__address-segments'; - private readonly accountListAddressItem = '[data-testid="account-list-address"]'; @@ -28,10 +26,6 @@ class AccountListPage { private readonly accountOptionsMenuButton = '[data-testid="account-list-item-menu-button"]'; - private readonly accountQrCodeImage = '.qr-code__wrapper'; - - private readonly accountQrCodeAddress = '.qr-code__address-segments'; - private readonly addAccountConfirmButton = '[data-testid="submit-add-account-with-name"]'; @@ -48,6 +42,9 @@ class AccountListPage { private readonly addEthereumAccountButton = '[data-testid="multichain-account-menu-popover-add-account"]'; + private readonly addEoaAccountButton = + '[data-testid="multichain-account-menu-popover-add-watch-only-account"]'; + private readonly addHardwareWalletButton = { text: 'Add hardware wallet', tag: 'button', @@ -70,11 +67,6 @@ class AccountListPage { private readonly currentSelectedAccount = '.multichain-account-list-item--selected'; - private readonly editableLabelButton = - '[data-testid="editable-label-button"]'; - - private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private readonly hiddenAccountOptionsMenuButton = '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; @@ -124,8 +116,18 @@ class AccountListPage { tag: 'button', }; - private readonly saveAccountLabelButton = - '[data-testid="save-account-label-input"]'; + private readonly watchAccountAddressInput = + 'input#address-input[type="text"]'; + + private readonly watchAccountConfirmButton = { + text: 'Watch account', + tag: 'button', + }; + + private readonly watchAccountModalTitle = { + text: 'Watch any Ethereum account', + tag: 'h4', + }; constructor(driver: Driver) { this.driver = driver; @@ -145,34 +147,36 @@ class AccountListPage { } /** - * Adds a new account with an optional custom label. + * Watch an EOA (external owned account). * - * @param customLabel - The custom label for the new account. If not provided, a default name will be used. + * @param address - The address to watch. + * @param expectedErrorMessage - Optional error message to display if the address is invalid. */ - async addNewAccount(customLabel?: string): Promise { - if (customLabel) { - console.log(`Adding new account with custom label: ${customLabel}`); - } else { - console.log(`Adding new account with default name`); - } + async addEoaAccount( + address: string, + expectedErrorMessage: string = '', + ): Promise { + console.log(`Watch EOA account with address ${address}`); await this.driver.clickElement(this.createAccountButton); - await this.driver.clickElement(this.addEthereumAccountButton); - if (customLabel) { - await this.driver.fill(this.accountNameInput, customLabel); - } - // needed to mitigate a race condition with the state update - // there is no condition we can wait for in the UI - await this.driver.delay(largeDelayMs); + await this.driver.clickElement(this.addEoaAccountButton); + await this.driver.waitForSelector(this.watchAccountModalTitle); + await this.driver.fill(this.watchAccountAddressInput, address); await this.driver.clickElementAndWaitToDisappear( - this.addAccountConfirmButton, + this.watchAccountConfirmButton, ); - } - - async isBtcAccountCreationButtonEnabled() { - const createButton = await this.driver.findElement( - this.addBtcAccountButton, - ); - return await createButton.isEnabled(); + if (expectedErrorMessage) { + console.log( + `Check if error message is displayed: ${expectedErrorMessage}`, + ); + await this.driver.waitForSelector({ + css: '.snap-ui-renderer__text', + text: expectedErrorMessage, + }); + } else { + await this.driver.clickElementAndWaitToDisappear( + this.addAccountConfirmButton, + ); + } } /** @@ -205,20 +209,27 @@ class AccountListPage { /** * Adds a new account of the specified type with an optional custom name. * - * @param accountType - The type of account to add (Ethereum, Bitcoin, or Solana) - * @param accountName - Optional custom name for the new account + * @param options - Options for adding a new account + * @param options.accountType - The type of account to add (Ethereum, Bitcoin, or Solana) + * @param [options.accountName] - Optional custom name for the new account * @throws {Error} If the specified account type is not supported * @example * // Add a new Ethereum account with default name - * await accountListPage.addAccount(ACCOUNT_TYPE.Ethereum); + * await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Ethereum }); * * // Add a new Bitcoin account with custom name - * await accountListPage.addAccount(ACCOUNT_TYPE.Bitcoin, 'My BTC Wallet'); + * await accountListPage.addAccount({ accountType: ACCOUNT_TYPE.Bitcoin, accountName: 'My BTC Wallet' }); */ - async addAccount(accountType: ACCOUNT_TYPE, accountName?: string) { + async addAccount({ + accountType, + accountName, + }: { + accountType: ACCOUNT_TYPE; + accountName?: string; + }) { + console.log(`Adding new account of type: ${ACCOUNT_TYPE[accountType]}`); await this.driver.clickElement(this.createAccountButton); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let addAccountButton: any; + let addAccountButton; switch (accountType) { case ACCOUNT_TYPE.Ethereum: addAccountButton = this.addEthereumAccountButton; @@ -235,56 +246,20 @@ class AccountListPage { await this.driver.clickElement(addAccountButton); if (accountName) { + console.log( + `Customize the new account with account name: ${accountName}`, + ); await this.driver.fill(this.accountNameInput, accountName); } - + // needed to mitigate a race condition with the state update + // there is no condition we can wait for in the UI + await this.driver.delay(largeDelayMs); await this.driver.clickElementAndWaitToDisappear( this.addAccountConfirmButton, 5000, ); } - /** - * Changes the label of the current account. - * - * @param newLabel - The new label to set for the account. - */ - async changeAccountLabel(newLabel: string): Promise { - console.log(`Changing account label to: ${newLabel}`); - await this.driver.clickElement(this.accountMenuButton); - await this.changeLabelFromAccountDetailsModal(newLabel); - } - - /** - * Changes the account label from within an already opened account details modal. - * Note: This method assumes the account details modal is already open. - * - * Recommended usage: - * ```typescript - * await accountListPage.openAccountDetailsModal('Current Account Name'); - * await accountListPage.changeLabelFromAccountDetailsModal('New Account Name'); - * ``` - * - * @param newLabel - The new label to set for the account - * @throws Will throw an error if the modal is not open when method is called - * @example - * // To rename a specific account, first open its details modal: - * await accountListPage.openAccountDetailsModal('Current Account Name'); - * await accountListPage.changeLabelFromAccountDetailsModal('New Account Name'); - * - * // Note: Using changeAccountLabel() alone will only work for the first account - */ - async changeLabelFromAccountDetailsModal(newLabel: string): Promise { - await this.driver.waitForSelector(this.editableLabelButton); - console.log( - `Account details modal opened, changing account label to: ${newLabel}`, - ); - await this.driver.clickElement(this.editableLabelButton); - await this.driver.fill(this.editableLabelInput, newLabel); - await this.driver.clickElement(this.saveAccountLabelButton); - await this.driver.clickElement(this.closeAccountModalButton); - } - async closeAccountModal(): Promise { console.log(`Close account modal in account list`); await this.driver.clickElementAndWaitToDisappear( @@ -292,23 +267,6 @@ class AccountListPage { ); } - /** - * Get the address of the specified account. - * - * @param accountLabel - The label of the account to get the address. - */ - async getAccountAddress(accountLabel: string): Promise { - console.log(`Get account address in account list`); - await this.openAccountOptionsInAccountList(accountLabel); - await this.driver.clickElement(this.accountMenuButton); - await this.driver.waitForSelector(this.accountAddressText); - const accountAddress = await ( - await this.driver.findElement(this.accountAddressText) - ).getText(); - await this.driver.clickElement(this.closeAccountModalButton); - return accountAddress; - } - async hideAccount(): Promise { console.log(`Hide account in account list`); await this.driver.clickElement(this.hideUnhideAccountButton); @@ -340,6 +298,13 @@ class AccountListPage { ); } + async isBtcAccountCreationButtonEnabled(): Promise { + const createButton = await this.driver.findElement( + this.addBtcAccountButton, + ); + return await createButton.isEnabled(); + } + /** * Open the account details modal for the specified account in account list. * @@ -556,21 +521,24 @@ class AccountListPage { } /** - * Check that the correct address is displayed in the account details modal. + * Checks that the add watch account button is displayed in the create account modal. * - * @param expectedAddress - The expected address to check. + * @param expectedAvailability - Whether the add watch account button is expected to be displayed. */ - async check_addressInAccountDetailsModal( - expectedAddress: string, + async check_addWatchAccountAvailable( + expectedAvailability: boolean, ): Promise { console.log( - `Check that address ${expectedAddress} is displayed in account details modal`, + `Check add watch account button is ${ + expectedAvailability ? 'displayed ' : 'not displayed' + }`, ); - await this.driver.waitForSelector(this.accountQrCodeImage); - await this.driver.waitForSelector({ - css: this.accountQrCodeAddress, - text: expectedAddress, - }); + await this.openAddAccountModal(); + if (expectedAvailability) { + await this.driver.waitForSelector(this.addEoaAccountButton); + } else { + await this.driver.assertElementNotPresent(this.addEoaAccountButton); + } } /** diff --git a/test/e2e/page-objects/pages/dialog/account-details-modal.ts b/test/e2e/page-objects/pages/dialog/account-details-modal.ts new file mode 100644 index 000000000000..0dbb2e0f87d7 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/account-details-modal.ts @@ -0,0 +1,106 @@ +import { Driver } from '../../../webdriver/driver'; + +class AccountDetailsModal { + private driver: Driver; + + private readonly accountAddressText = '.qr-code__address-segments'; + + private readonly accountQrCodeAddress = '.qr-code__address-segments'; + + private readonly accountQrCodeImage = '.qr-code__wrapper'; + + private readonly closeAccountModalButton = + 'header button[aria-label="Close"]'; + + private readonly copyAddressButton = + '[data-testid="address-copy-button-text"]'; + + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; + + private readonly editableLabelInput = '[data-testid="editable-input"] input'; + + private readonly saveAccountLabelButton = + '[data-testid="save-account-label-input"]'; + + private readonly showPrivateKeyButton = { + css: 'button', + text: 'Show private key', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.editableLabelButton, + this.copyAddressButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for account details modal to be loaded', + e, + ); + throw e; + } + console.log('Account details modal is loaded'); + } + + async closeAccountDetailsModal(): Promise { + await this.driver.clickElementAndWaitToDisappear( + this.closeAccountModalButton, + ); + } + + /** + * Change the label of the account in the account details modal. + * + * @param newLabel - The new label to set for the account. + */ + async changeAccountLabel(newLabel: string): Promise { + console.log( + `Account details modal opened, changing account label to: ${newLabel}`, + ); + await this.driver.clickElement(this.editableLabelButton); + await this.driver.fill(this.editableLabelInput, newLabel); + await this.driver.clickElement(this.saveAccountLabelButton); + await this.closeAccountDetailsModal(); + } + + async getAccountAddress(): Promise { + console.log(`Get account address in account details modal`); + await this.driver.waitForSelector(this.accountAddressText); + const accountAddress = await ( + await this.driver.findElement(this.accountAddressText) + ).getText(); + await this.closeAccountDetailsModal(); + return accountAddress; + } + + /** + * Check that the correct address is displayed in the account details modal. + * + * @param expectedAddress - The expected address to check. + */ + async check_addressInAccountDetailsModal( + expectedAddress: string, + ): Promise { + console.log( + `Check that address ${expectedAddress} is displayed in account details modal`, + ); + await this.driver.waitForSelector(this.accountQrCodeImage); + await this.driver.waitForSelector({ + css: this.accountQrCodeAddress, + text: expectedAddress, + }); + } + + async check_showPrivateKeyButtonIsNotDisplayed(): Promise { + console.log('Check that show private key button is not displayed'); + await this.driver.assertElementNotPresent(this.showPrivateKeyButton); + } +} + +export default AccountDetailsModal; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index 57e02e864624..e64fdc5a4cd7 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -9,6 +9,8 @@ class HeaderNavbar { private readonly allPermissionsButton = '[data-testid="global-menu-connected-sites"]'; + private readonly copyAddressButton = '[data-testid="app-header-copy-button"]'; + private readonly threeDotMenuButton = '[data-testid="account-options-menu-button"]'; @@ -19,6 +21,9 @@ class HeaderNavbar { private readonly mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; + private readonly openAccountDetailsButton = + '[data-testid="account-list-menu-details"]'; + private readonly settingsButton = '[data-testid="global-menu-settings"]'; private readonly switchNetworkDropDown = '[data-testid="network-display"]'; @@ -51,6 +56,12 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openAccountDetailsModal(): Promise { + console.log('Open account details modal'); + await this.openThreeDotMenu(); + await this.driver.clickElement(this.openAccountDetailsButton); + } + async openThreeDotMenu(): Promise { console.log('Open account options menu'); await this.driver.clickElement(this.threeDotMenuButton); @@ -98,6 +109,21 @@ class HeaderNavbar { ); } + /** + * Verifies that the displayed account address in header matches the expected address. + * + * @param expectedAddress - The expected address of the account. + */ + async check_accountAddress(expectedAddress: string): Promise { + console.log( + `Verify the displayed account address in header is: ${expectedAddress}`, + ); + await this.driver.waitForSelector({ + css: this.copyAddressButton, + text: expectedAddress, + }); + } + /** * Verifies that the displayed account label in header matches the expected label. * diff --git a/test/e2e/page-objects/pages/home/bitcoin-homepage.ts b/test/e2e/page-objects/pages/home/bitcoin-homepage.ts index 19b235636405..691d85763b14 100644 --- a/test/e2e/page-objects/pages/home/bitcoin-homepage.ts +++ b/test/e2e/page-objects/pages/home/bitcoin-homepage.ts @@ -4,10 +4,7 @@ class BitcoinHomepage extends HomePage { protected readonly balance = '[data-testid="coin-overview__primary-currency"]'; - private readonly bridgeButton = { - text: 'Bridge', - tag: 'button', - }; + protected readonly bridgeButton = '[data-testid="coin-overview-bridge"]'; private readonly buySellButton = '[data-testid="coin-overview-buy"]'; @@ -15,10 +12,7 @@ class BitcoinHomepage extends HomePage { protected readonly sendButton = '[data-testid="coin-overview-send"]'; - private readonly swapButton = { - text: 'Swap', - tag: 'button', - }; + protected readonly swapButton = '[data-testid="coin-overview-swap"]'; async check_pageIsLoaded(): Promise { try { diff --git a/test/e2e/page-objects/pages/home/homepage.ts b/test/e2e/page-objects/pages/home/homepage.ts index b4b79846fb06..708ae5ac0277 100644 --- a/test/e2e/page-objects/pages/home/homepage.ts +++ b/test/e2e/page-objects/pages/home/homepage.ts @@ -20,6 +20,9 @@ class HomePage { css: '.mm-banner-alert', }; + protected readonly bridgeButton: string = + '[data-testid="eth-overview-bridge"]'; + private readonly closeUseNetworkNotificationModalButton = { text: 'Got it', tag: 'h6', @@ -45,12 +48,15 @@ class HomePage { testId: 'sensitive-toggle', }; + protected readonly sendButton: string = '[data-testid="eth-overview-send"]'; + + protected readonly swapButton: string = + '[data-testid="token-overview-button-swap"]'; + private readonly refreshErc20Tokens = { testId: 'refreshList', }; - protected readonly sendButton: string = '[data-testid="eth-overview-send"]'; - private readonly tokensTab = { testId: 'account-overview__asset-tab', }; @@ -142,6 +148,13 @@ class HomePage { await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); } + async check_disabledButtonTooltip(tooltipText: string): Promise { + console.log(`Check if disabled button tooltip is displayed on homepage`); + await this.driver.waitForSelector( + `.icon-button--disabled [data-tooltipped][data-original-title="${tooltipText}"]`, + ); + } + /** * Checks if the toaster message for editing a network is displayed on the homepage. * @@ -197,6 +210,39 @@ class HomePage { }, 10000); } + async check_ifBridgeButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.bridgeButton, 1000); + } catch (e) { + console.log('Bridge button not clickable', e); + return false; + } + console.log('Bridge button is clickable'); + return true; + } + + async check_ifSendButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.sendButton, 1000); + } catch (e) { + console.log('Send button not clickable', e); + return false; + } + console.log('Send button is clickable'); + return true; + } + + async check_ifSwapButtonIsClickable(): Promise { + try { + await this.driver.findClickableElement(this.swapButton, 1000); + } catch (e) { + console.log('Swap button not clickable', e); + return false; + } + console.log('Swap button is clickable'); + return true; + } + async check_localBlockchainBalanceIsDisplayed( localBlockchainServer?: Ganache, address = null, diff --git a/test/e2e/page-objects/pages/settings/experimental-settings.ts b/test/e2e/page-objects/pages/settings/experimental-settings.ts index f551db6d47c5..69df0525093d 100644 --- a/test/e2e/page-objects/pages/settings/experimental-settings.ts +++ b/test/e2e/page-objects/pages/settings/experimental-settings.ts @@ -22,6 +22,12 @@ class ExperimentalSettings { private readonly requestQueueToggle = '[data-testid="experimental-setting-toggle-request-queue"] label'; + private readonly watchAccountToggleState = + '[data-testid="watch-account-toggle"]'; + + private readonly watchAccountToggle = + '[data-testid="watch-account-toggle-div"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -39,6 +45,15 @@ class ExperimentalSettings { console.log('Experimental Settings page is loaded'); } + // Get the state of the Watch Account Toggle, returns true if the toggle is selected + async getWatchAccountToggleState(): Promise { + console.log('Get Watch Account Toggle State'); + const toggleInput = await this.driver.findElement( + this.watchAccountToggleState, + ); + return toggleInput.isSelected(); + } + async toggleBitcoinAccount(): Promise { console.log('Toggle Add new Bitcoin account on experimental setting page'); await this.driver.waitForSelector({ @@ -62,6 +77,11 @@ class ExperimentalSettings { console.log('Toggle Request Queue on experimental setting page'); await this.driver.clickElement(this.requestQueueToggle); } + + async toggleWatchAccount(): Promise { + console.log('Toggle Watch Account on experimental setting page'); + await this.driver.clickElement(this.watchAccountToggle); + } } export default ExperimentalSettings; diff --git a/test/e2e/tests/account/account-custom-name.spec.ts b/test/e2e/tests/account/account-custom-name.spec.ts index 4c0ecbe196f9..92302e032224 100644 --- a/test/e2e/tests/account/account-custom-name.spec.ts +++ b/test/e2e/tests/account/account-custom-name.spec.ts @@ -1,8 +1,10 @@ import { Suite } from 'mocha'; import { Driver } from '../../webdriver/driver'; import { withFixtures } from '../../helpers'; +import { ACCOUNT_TYPE } from '../../constants'; import FixtureBuilder from '../../fixture-builder'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; @@ -25,14 +27,20 @@ describe('Account Custom Name Persistence', function (this: Suite) { // Change account label for existing account and verify edited account label const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.openAccountOptionsMenu(); - await accountListPage.changeAccountLabel(newAccountLabel); + await accountListPage.openAccountDetailsModal('Account 1'); + + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel(newAccountLabel); await headerNavbar.check_accountLabel(newAccountLabel); // Add new account with custom label and verify new added account label await headerNavbar.openAccountMenu(); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(anotherAccountLabel); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: anotherAccountLabel, + }); await headerNavbar.check_accountLabel(anotherAccountLabel); // Switch back to the first account and verify first custom account persists diff --git a/test/e2e/tests/account/add-account.spec.ts b/test/e2e/tests/account/add-account.spec.ts index db62927b91d7..2df140899212 100644 --- a/test/e2e/tests/account/add-account.spec.ts +++ b/test/e2e/tests/account/add-account.spec.ts @@ -1,5 +1,6 @@ import { E2E_SRP } from '../../default-fixture'; import FixtureBuilder from '../../fixture-builder'; +import { ACCOUNT_TYPE } from '../../constants'; import { WALLET_PASSWORD, defaultGanacheOptions, @@ -43,7 +44,9 @@ describe('Add account', function () { const newAccountName = 'Account 2'; const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel(newAccountName); await homePage.check_expectedBalanceIsDisplayed(); @@ -112,7 +115,9 @@ describe('Add account', function () { const newAccountName = 'Account 2'; const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel(newAccountName); await homePage.check_expectedBalanceIsDisplayed(); @@ -177,7 +182,9 @@ describe('Add account', function () { // Create new account with default name Account 2 const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel('Account 2'); await homePage.check_expectedBalanceIsDisplayed(); diff --git a/test/e2e/tests/account/import-flow.spec.ts b/test/e2e/tests/account/import-flow.spec.ts index dbb7b2f85d49..c8ffcc334e49 100644 --- a/test/e2e/tests/account/import-flow.spec.ts +++ b/test/e2e/tests/account/import-flow.spec.ts @@ -3,6 +3,7 @@ import { DEFAULT_FIXTURE_ACCOUNT } from '../../constants'; import { withFixtures } from '../../helpers'; import FixtureBuilder from '../../fixture-builder'; import AccountListPage from '../../page-objects/pages/account-list-page'; +import AccountDetailsModal from '../../page-objects/pages/dialog/account-details-modal'; import HeaderNavbar from '../../page-objects/pages/header-navbar'; import HomePage from '../../page-objects/pages/home/homepage'; import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; @@ -32,7 +33,9 @@ describe('Import flow @no-mmi', function () { const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); await accountListPage.openAccountDetailsModal('Account 1'); - await accountListPage.check_addressInAccountDetailsModal( + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.check_addressInAccountDetailsModal( DEFAULT_FIXTURE_ACCOUNT.toLowerCase(), ); }, diff --git a/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts b/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts index e9a82b128352..64a3ddbf5fd7 100644 --- a/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts +++ b/test/e2e/tests/identity/account-syncing/new-user-sync.spec.ts @@ -2,6 +2,7 @@ import { Mockttp } from 'mockttp'; import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sdk'; import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; +import { ACCOUNT_TYPE } from '../../../constants'; import { mockIdentityServices } from '../mocks'; import { IDENTITY_TEAM_PASSWORD } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; @@ -64,7 +65,10 @@ describe('Account syncing - New User @no-mmi', function () { // Add a second account await accountListPage.openAccountOptionsMenu(); - await accountListPage.addNewAccount('My Second Account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'My Second Account', + }); // Set SRP to use for retreival const headerNavbar = new HeaderNavbar(driver); diff --git a/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts b/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts index 19908211181b..bd063c182baf 100644 --- a/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts +++ b/test/e2e/tests/identity/account-syncing/onboarding-with-opt-out.spec.ts @@ -7,6 +7,7 @@ import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, } from '../constants'; +import { ACCOUNT_TYPE } from '../../../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; @@ -135,7 +136,11 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'Account 1', ); - await accountListPage.addNewAccount('New Account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'New Account', + }); + // Set SRP to use for retreival const headerNavbar = new HeaderNavbar(driver); await headerNavbar.check_pageIsLoaded(); diff --git a/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts b/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts index 9c2fdb69c7e9..e02833e7c172 100644 --- a/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-after-adding-account.spec.ts @@ -3,6 +3,7 @@ import { USER_STORAGE_FEATURE_NAMES } from '@metamask/profile-sync-controller/sd import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockIdentityServices } from '../mocks'; +import { ACCOUNT_TYPE } from '../../../constants'; import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, @@ -65,7 +66,10 @@ describe('Account syncing - Add Account @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.addNewAccount('My third account'); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: 'My third account', + }); }, ); @@ -164,7 +168,9 @@ describe('Account syncing - Add Account @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); }, ); diff --git a/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts b/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts index 07f6e4848aba..ce10b3129d58 100644 --- a/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-after-modifying-account-name.spec.ts @@ -9,6 +9,7 @@ import { } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountDetailsModal from '../../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HomePage from '../../../page-objects/pages/home/homepage'; import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; @@ -65,8 +66,14 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { await accountListPage.check_accountDisplayedInAccountList( 'My Second Synced Account', ); - await accountListPage.openAccountOptionsMenu(); - await accountListPage.changeAccountLabel('My Renamed First Account'); + await accountListPage.openAccountDetailsModal( + 'My First Synced Account', + ); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel( + 'My Renamed First Account', + ); }, ); diff --git a/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts b/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts index 6fcc9e9cc0d0..ec52bb3124bd 100644 --- a/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts +++ b/test/e2e/tests/identity/account-syncing/sync-with-account-balances.spec.ts @@ -6,8 +6,10 @@ import { IDENTITY_TEAM_PASSWORD, IDENTITY_TEAM_SEED_PHRASE, } from '../constants'; +import { ACCOUNT_TYPE } from '../../../constants'; import { UserStorageMockttpController } from '../../../helpers/identity/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import AccountDetailsModal from '../../../page-objects/pages/dialog/account-details-modal'; import AccountListPage from '../../../page-objects/pages/account-list-page'; import HomePage from '../../../page-objects/pages/home/homepage'; import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; @@ -108,7 +110,9 @@ describe('Account syncing - User already has balances on multple accounts @no-mm } // Create new account and prepare for additional accounts - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); accountsToMock = [...INITIAL_ACCOUNTS, ...ADDITIONAL_ACCOUNTS]; }, ); @@ -158,11 +162,13 @@ describe('Account syncing - User already has balances on multple accounts @no-mm // Rename Account 6 to verify update to user storage await accountListPage.switchToAccount('Account 6'); + await header.check_accountLabel('Account 6'); await header.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); await accountListPage.openAccountDetailsModal('Account 6'); - await accountListPage.changeLabelFromAccountDetailsModal( - 'My Renamed Account 6', - ); + const accountDetailsModal = new AccountDetailsModal(driver); + await accountDetailsModal.check_pageIsLoaded(); + await accountDetailsModal.changeAccountLabel('My Renamed Account 6'); }, ); diff --git a/test/e2e/tests/tokens/nft/import-nft.spec.ts b/test/e2e/tests/tokens/nft/import-nft.spec.ts index 5983d1002035..e151e83a355f 100644 --- a/test/e2e/tests/tokens/nft/import-nft.spec.ts +++ b/test/e2e/tests/tokens/nft/import-nft.spec.ts @@ -1,4 +1,5 @@ import { defaultGanacheOptions, withFixtures } from '../../../helpers'; +import { ACCOUNT_TYPE } from '../../../constants'; import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; import FixtureBuilder from '../../../fixture-builder'; import AccountListPage from '../../../page-objects/pages/account-list-page'; @@ -67,7 +68,9 @@ describe('Import NFT', function () { await headerNavbar.openAccountMenu(); const accountListPage = new AccountListPage(driver); await accountListPage.check_pageIsLoaded(); - await accountListPage.addNewAccount(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + }); await headerNavbar.check_accountLabel('Account 2'); await homepage.check_expectedBalanceIsDisplayed(); From 7be1b0d3b193764eef0489ba3c9df49eba01f2fa Mon Sep 17 00:00:00 2001 From: Priya Date: Fri, 20 Dec 2024 11:55:45 +0100 Subject: [PATCH 08/71] test: remove duplicate signature tests (#29377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removing duplicated tests that are already covered in test/e2e/tests/confirmations/signatures [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29377?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/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../e2e/tests/signature/personal-sign.spec.js | 37 ------------ .../tests/signature/signature-request.spec.js | 58 ------------------- 2 files changed, 95 deletions(-) diff --git a/test/e2e/tests/signature/personal-sign.spec.js b/test/e2e/tests/signature/personal-sign.spec.js index 092a9518ba01..908a422bfea0 100644 --- a/test/e2e/tests/signature/personal-sign.spec.js +++ b/test/e2e/tests/signature/personal-sign.spec.js @@ -116,43 +116,6 @@ describe('Personal sign', function () { }); describe('Redesigned confirmation screens', function () { - it('can initiate and confirm a personal sign', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - const addresses = await ganacheServer.getAccounts(); - const publicAddress = addresses[0]; - await unlockWallet(driver); - - await openDapp(driver); - await driver.clickElement('#personalSign'); - - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await driver.findElement({ - css: 'p', - text: 'Example `personal_sign` message', - }); - - await driver.clickElement('[data-testid="confirm-footer-button"]'); - - await verifyAndAssertPersonalMessage(driver, publicAddress); - }, - ); - }); - it('can queue multiple personal signs and confirm', async function () { await withFixtures( { diff --git a/test/e2e/tests/signature/signature-request.spec.js b/test/e2e/tests/signature/signature-request.spec.js index 716ee1f98d95..7d37da3de0db 100644 --- a/test/e2e/tests/signature/signature-request.spec.js +++ b/test/e2e/tests/signature/signature-request.spec.js @@ -337,64 +337,6 @@ describe('Sign Typed Data Signature Request', function () { }); describe('Redesigned confirmation screens', function () { - testData.forEach((data) => { - it(`can initiate and confirm a Signature Request of ${data.type}`, async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver, ganacheServer }) => { - const addresses = await ganacheServer.getAccounts(); - const publicAddress = addresses[0]; - await unlockWallet(driver); - - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement(data.buttonId); - - await driver.waitUntilXWindowHandles(3); - let windowHandles = await driver.getAllWindowHandles(); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); - - await verifyAndAssertRedesignedSignTypedData( - driver, - data.expectedMessage, - ); - - // Approve signing typed data - await finalizeSignatureRequest( - driver, - '.confirm-scroll-to-bottom__button', - 'Confirm', - ); - await driver.waitUntilXWindowHandles(2); - windowHandles = await driver.getAllWindowHandles(); - - // switch to the Dapp and verify the signed address - await driver.switchToWindowWithTitle( - 'E2E Test Dapp', - windowHandles, - ); - await driver.clickElement(data.verifyId); - const recoveredAddress = await driver.findElement( - data.verifyResultId, - ); - - assert.equal(await recoveredAddress.getText(), publicAddress); - }, - ); - }); - }); - testData.forEach((data) => { it(`can queue multiple Signature Requests of ${data.type} and confirm`, async function () { await withFixtures( From 8a6f4f9198e19356ce545fb4d2561d86daabb6f5 Mon Sep 17 00:00:00 2001 From: Nidhi Kumari Date: Fri, 20 Dec 2024 10:59:13 +0000 Subject: [PATCH 09/71] fix: fixed truncation issue for long help text (#29269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to fix the truncation issue in send flow. The decimal value in error message used to show all the decimal numbers after decimal. This PR updates the max value to be shown after decimals to be 4 which is also the standard decimal value we try to show in other places of code ## **Related issues** Fixes: #26766 ## **Manual testing steps** 1. Go to Send Flow 2. Try sending a token with large input than the available balance 3. Check the value in helptext doesn't overlap and only shows 4 numbers ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-17 at 11 17 19 AM](https://github.com/user-attachments/assets/8f9e39ba-60a6-4cd9-88f3-c7e56aa14e1c) ### **After** ![Screenshot 2024-12-17 at 11 18 05 AM](https://github.com/user-attachments/assets/08a6913e-13a1-40ad-bd48-ae8528412287) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../asset-balance/asset-balance-text.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx index 3aec458341bd..29cbf7e40619 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx @@ -48,6 +48,10 @@ export function AssetBalanceText({ const balanceString = hexToDecimal(asset.balance) || tokensWithBalances[0]?.string; + const showFixedBalanceString = balanceString?.includes('.') + ? balanceString.slice(0, balanceString.indexOf('.') + 5) // Include 4 digits after the decimal + : balanceString; + const balanceValue = useSelector(getSelectedAccountCachedBalance); const nativeTokenFiatBalance = useCurrencyDisplay(balanceValue, { @@ -135,7 +139,7 @@ export function AssetBalanceText({ return ( ); } From 3fd27a9e9a170da753963ddc751adc9cf585596d Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:12:48 +0000 Subject: [PATCH 10/71] fix: `gasFeeEstimates` property undefined (#29312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes an issue created by Sentry where the property `gasFeeEstimates` is `undefined`. Added an early return when the property is `undefined` and covered the scenario with unit tests. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29312?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27501 https://github.com/MetaMask/metamask-extension/issues/27241 ## **Manual testing steps** 1. Go to this test dapp 2. Block requests against gas-api 3. Start a send or contract interaction 4. Verify the console ## **Screenshots/Recordings** [block-gas-api.webm](https://github.com/user-attachments/assets/1c3cf638-905e-4df9-b875-84f0056979b1) ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../hooks/useTransactionFunction.test.js | 12 ++++++++++++ .../confirmations/hooks/useTransactionFunctions.js | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ui/pages/confirmations/hooks/useTransactionFunction.test.js b/ui/pages/confirmations/hooks/useTransactionFunction.test.js index 22984ba71a37..ec823d2acf70 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunction.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunction.test.js @@ -164,4 +164,16 @@ describe('useMaxPriorityFeePerGasInput', () => { userFeeLevel: 'dappSuggested', }); }); + + it('returns early when gasFeeEstimates is undefined', () => { + const mockUpdateTransaction = jest + .spyOn(Actions, 'updateTransactionGasFees') + .mockImplementation(() => ({ type: '' })); + + const { result } = renderUseTransactionFunctions({ + gasFeeEstimates: undefined, + }); + result.current.updateTransactionUsingEstimate(GasRecommendations.low); + expect(mockUpdateTransaction).not.toHaveBeenCalled(); + }); }); diff --git a/ui/pages/confirmations/hooks/useTransactionFunctions.js b/ui/pages/confirmations/hooks/useTransactionFunctions.js index 17b5165da94e..96b41cd5e08e 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctions.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctions.js @@ -201,9 +201,10 @@ export const useTransactionFunctions = ({ const updateTransactionUsingEstimate = useCallback( (gasFeeEstimateToUse) => { - if (!gasFeeEstimates[gasFeeEstimateToUse]) { + if (!gasFeeEstimates?.[gasFeeEstimateToUse]) { return; } + const { suggestedMaxFeePerGas, suggestedMaxPriorityFeePerGas } = gasFeeEstimates[gasFeeEstimateToUse]; updateTransaction({ From 02230c725bcc28b327df9b81c2198e74c9e875ea Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Fri, 20 Dec 2024 12:22:10 +0100 Subject: [PATCH 11/71] fix: remove Text in the Activity Empty State (#29318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR updates the Activity empty state by removing the placeholder text. The change simplifies the UI, ensuring a cleaner and more visually appealing experience when no activity is present. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29318?quickstart=1) ## **Related issues** Fixes: #26669 ## **Manual testing steps** 1. Go to activity tab for any network where you don't have transactions 2. check the message ## **Screenshots/Recordings** ### **Before** Screenshot 2024-12-18 at 13 35 07 ### **After** Screenshot 2024-12-18 at 13 35 12 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/am/messages.json | 3 - app/_locales/ar/messages.json | 3 - app/_locales/bg/messages.json | 3 - app/_locales/bn/messages.json | 3 - app/_locales/ca/messages.json | 3 - app/_locales/cs/messages.json | 3 - app/_locales/da/messages.json | 3 - app/_locales/de/messages.json | 3 - app/_locales/el/messages.json | 3 - app/_locales/en/messages.json | 10 -- app/_locales/en_GB/messages.json | 3 - app/_locales/es/messages.json | 3 - app/_locales/es_419/messages.json | 3 - app/_locales/et/messages.json | 3 - app/_locales/fa/messages.json | 3 - app/_locales/fi/messages.json | 3 - app/_locales/fil/messages.json | 3 - app/_locales/fr/messages.json | 3 - app/_locales/he/messages.json | 3 - app/_locales/hi/messages.json | 3 - app/_locales/hn/messages.json | 3 - app/_locales/hr/messages.json | 3 - app/_locales/ht/messages.json | 3 - app/_locales/hu/messages.json | 3 - app/_locales/id/messages.json | 3 - app/_locales/it/messages.json | 3 - app/_locales/ja/messages.json | 3 - app/_locales/kn/messages.json | 3 - app/_locales/ko/messages.json | 3 - app/_locales/lt/messages.json | 3 - app/_locales/lv/messages.json | 3 - app/_locales/ms/messages.json | 3 - app/_locales/nl/messages.json | 3 - app/_locales/no/messages.json | 3 - app/_locales/ph/messages.json | 3 - app/_locales/pl/messages.json | 3 - app/_locales/pt/messages.json | 3 - app/_locales/pt_BR/messages.json | 3 - app/_locales/ro/messages.json | 3 - app/_locales/ru/messages.json | 3 - app/_locales/sk/messages.json | 3 - app/_locales/sl/messages.json | 3 - app/_locales/sr/messages.json | 3 - app/_locales/sv/messages.json | 3 - app/_locales/sw/messages.json | 3 - app/_locales/ta/messages.json | 3 - app/_locales/th/messages.json | 3 - app/_locales/tl/messages.json | 3 - app/_locales/tr/messages.json | 3 - app/_locales/uk/messages.json | 3 - app/_locales/vi/messages.json | 3 - app/_locales/zh_CN/messages.json | 3 - app/_locales/zh_TW/messages.json | 3 - .../transaction/multiple-transactions.spec.js | 12 --- .../transaction-list.component.js | 97 ++++++++----------- .../transaction-list/transaction-list.test.js | 22 +++-- .../__snapshots__/asset-page.test.tsx.snap | 36 +------ ui/pages/asset/components/asset-page.tsx | 4 +- 58 files changed, 55 insertions(+), 282 deletions(-) diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index ccb81d489af7..fac85861828c 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -428,9 +428,6 @@ "noConversionRateAvailable": { "message": "ምንም የልወጣ ተመን አይገኝም" }, - "noTransactions": { - "message": "ግብይቶች የሉዎትም" - }, "noWebcamFound": { "message": "የኮምፒዩተርዎ ካሜራ አልተገኘም። እባክዎ እንደገና ይሞክሩ።" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 00685f39df87..33585243d016 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -444,9 +444,6 @@ "noConversionRateAvailable": { "message": "لا يوجد معدل تحويل متاح" }, - "noTransactions": { - "message": "لا توجد لديك معاملات" - }, "noWebcamFound": { "message": "لم يتم العثور على كاميرا ويب للكمبيوتر الخاص بك. حاول مرة اخرى." }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 2e51cf4b24b4..a6dbac690242 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Няма наличен процент на преобръщане" }, - "noTransactions": { - "message": "Нямате транзакции" - }, "noWebcamFound": { "message": "Уеб камерата на компютърa Ви не беше намерена. Моля, опитайте отново." }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 6f3bb290215d..1df74dc0e941 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -437,9 +437,6 @@ "noConversionRateAvailable": { "message": "কোনো বিনিময় হার উপলভ্য নয়" }, - "noTransactions": { - "message": "আপনার কোনো লেনদেন নেই" - }, "noWebcamFound": { "message": "আপনার কম্পিউটারের ওয়েবক্যাম খুঁজে পাওয়া যায়নি। অনুগ্রহ করে আবার চেষ্টা করুন।" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index c54e236d8a21..b988ae488143 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "No hi ha cap tarifa de conversió disponible" }, - "noTransactions": { - "message": "No tens transaccions" - }, "noWebcamFound": { "message": "No s'ha trovat la webcam del teu ordinador. Si us plau prova de nou." }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index b6161b00a979..adf67dbda77d 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -210,9 +210,6 @@ "next": { "message": "Další" }, - "noTransactions": { - "message": "Žádné transakce" - }, "passwordNotLongEnough": { "message": "Heslo není dost dlouhé" }, diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index e9c884f28dbb..080dd75f22d6 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen tilgængelig omregningskurs" }, - "noTransactions": { - "message": "Du har ingen transaktioner" - }, "noWebcamFound": { "message": "Din computers webkamera blev ikke fundet. Prøv venligst igen." }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 04c45bd81348..9ad22ea297e0 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Nein, danke!" }, - "noTransactions": { - "message": "Keine Transaktionen" - }, "noWebcamFound": { "message": "Die Webcam Ihres Computers wurde nicht gefunden. Bitte versuchen Sie es erneut." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 24da4df88460..aa5a819d532b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Όχι, ευχαριστώ" }, - "noTransactions": { - "message": "Δεν έχετε καμιά συναλλαγή" - }, "noWebcamFound": { "message": "Η κάμερα του υπολογιστή σας δεν βρέθηκε. Προσπαθήστε ξανά." }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2d0f8baa4158..e8d8fe2779f9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3498,16 +3498,6 @@ "noThanks": { "message": "No thanks" }, - "noTransactions": { - "message": "You have no transactions" - }, - "noTransactionsChainIdMismatch": { - "message": "Please switch network to view transactions" - }, - "noTransactionsNetworkName": { - "message": "Please switch to $1 network to view transactions", - "description": "$1 represents the network name" - }, "noWebcamFound": { "message": "Your computer's webcam was not found. Please try again." }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index d242dc7e88f8..f66111064a90 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -3141,9 +3141,6 @@ "noThanks": { "message": "No thanks" }, - "noTransactions": { - "message": "You have no transactions" - }, "noWebcamFound": { "message": "Your computer's webcam was not found. Please try again." }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 0f24f5bc5b1a..2c6fa0a29885 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "No, gracias" }, - "noTransactions": { - "message": "No tiene transacciones" - }, "noWebcamFound": { "message": "No se encontró la cámara web del equipo. Vuelva a intentarlo." }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 5d940a269091..4e82c875c760 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1273,9 +1273,6 @@ "noConversionRateAvailable": { "message": "No hay tasa de conversión disponible" }, - "noTransactions": { - "message": "No tiene transacciones" - }, "noWebcamFound": { "message": "No se encontró la cámara web del equipo. Vuelva a intentarlo." }, diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index c5f55c6d3327..a2a85b36d987 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -437,9 +437,6 @@ "noConversionRateAvailable": { "message": "Ühtegi vahetuskurssi pole saadaval" }, - "noTransactions": { - "message": "Teil ei ole tehinguid" - }, "noWebcamFound": { "message": "Teie arvuti veebikaamerat ei leitud. Proovige uuesti." }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 8399ecb91aec..3ec5211c2dfb 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "هیچ نرخ تغییر موجود نمیباشد" }, - "noTransactions": { - "message": "شما هیچ معامله ندارید" - }, "noWebcamFound": { "message": "وب کم کمپیوتر تان پیدا نشد. لطفًا دوباره کوشش کنید." }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 5002c4eed2b3..c49ea583598d 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Vaihtokurssi ei saatavilla" }, - "noTransactions": { - "message": "Sinulla ei ole tapahtumia" - }, "noWebcamFound": { "message": "Tietokoneesi verkkokameraa ei löytynyt. Ole hyvä ja yritä uudestaan." }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index abee8f9c0a5e..14f91bc5c16a 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -381,9 +381,6 @@ "noConversionRateAvailable": { "message": "Walang Presyo ng Palitan na Available" }, - "noTransactions": { - "message": "Wala kang mga transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukang muli." }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index ce615bddd591..1c8436635797 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Non merci" }, - "noTransactions": { - "message": "Aucune transaction" - }, "noWebcamFound": { "message": "La caméra de votre ordinateur n’a pas été trouvée. Veuillez réessayer." }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 3bd6b6b67408..c9360ff612de 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "אין שער המרה זמין" }, - "noTransactions": { - "message": "אין לך עסקאות" - }, "noWebcamFound": { "message": "מצלמת הרשת של מחשבך לא נמצאה. נא לנסות שוב." }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 05488711d94f..8d115d214652 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "जी नहीं, धन्यवाद" }, - "noTransactions": { - "message": "आपके पास कोई ट्रांसेक्शन नहीं है" - }, "noWebcamFound": { "message": "आपके कंप्यूटर का वेबकैम नहीं मिला। कृपया फिर से कोशिश करें।" }, diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index f6bb938c4886..cecc6f26385b 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -193,9 +193,6 @@ "next": { "message": "अगला" }, - "noTransactions": { - "message": "कोई लेन-देन नहीं" - }, "pastePrivateKey": { "message": "यहां अपनी निजी कुंजी स्ट्रिंग चिपकाएं:", "description": "For importing an account from a private key" diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index d1ca9bac8057..77b864172ae2 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Nijedan konverzijski tečaj nije dostupan" }, - "noTransactions": { - "message": "Nemate transkacija" - }, "noWebcamFound": { "message": "Mrežna kamera vašeg računala nije pronađena. Pokušajte ponovno." }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 8d0e7e703559..29df50803fa9 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -310,9 +310,6 @@ "noConversionRateAvailable": { "message": "Pa gen okenn Konvèsyon Disponib" }, - "noTransactions": { - "message": "Pa gen tranzaksyon" - }, "noWebcamFound": { "message": "Nou pakay jwenn webcam òdinatè ou. Tanpri eseye ankò." }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index ee8699a64545..9255c752b4a7 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Nincs elérhető átváltási díj" }, - "noTransactions": { - "message": "Nincsenek tranzakciói" - }, "noWebcamFound": { "message": "Nem található számítógéped webkamerája. Kérünk, próbáld újra." }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index c918a6cc0fb0..fb488c423c61 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Tidak, terima kasih" }, - "noTransactions": { - "message": "Anda tidak memiliki transaksi" - }, "noWebcamFound": { "message": "Webcam komputer Anda tidak ditemukan. Harap coba lagi." }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index da1da37952f6..a88d710a6f81 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1046,9 +1046,6 @@ "noConversionRateAvailable": { "message": "Tasso di conversione non disponibile" }, - "noTransactions": { - "message": "Nessuna Transazione" - }, "noWebcamFound": { "message": "La fotocamera del tuo computer non è stata trovata. Riprova." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ead0da87b7b8..35c812fa53f0 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "結構です" }, - "noTransactions": { - "message": "トランザクションがありません" - }, "noWebcamFound": { "message": "お使いのコンピューターのWebカメラが見つかりませんでした。もう一度お試しください。" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index 805de3c78fc0..148a820f5bfa 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "ಯಾವುದೇ ಪರಿವರ್ತನೆ ದರ ಲಭ್ಯವಿಲ್ಲ" }, - "noTransactions": { - "message": "ನೀವು ಯಾವುದೇ ವಹಿವಾಟುಗಳನ್ನು ಹೊಂದಿಲ್ಲ" - }, "noWebcamFound": { "message": "ನಿಮ್ಮ ಕಂಪ್ಯೂಟರ್‌ನ ವೆಬ್‌ಕ್ಯಾಮ್ ಕಂಡುಬಂದಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index f22491040b4f..158a8a333c15 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "괜찮습니다" }, - "noTransactions": { - "message": "트랜잭션이 없습니다." - }, "noWebcamFound": { "message": "컴퓨터의 웹캠을 찾을 수 없습니다. 다시 시도하세요." }, diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index cd034a303c4a..3e5586f39f50 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Nėra keitimo kurso" }, - "noTransactions": { - "message": "Neturite jokių operacijų" - }, "noWebcamFound": { "message": "Jūsų kompiuterio vaizdo kamera nerasta. Bandykite dar kartą." }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index be5d43f4afd9..a1e166937385 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Konversijas kurss nav pieejams" }, - "noTransactions": { - "message": "Jums nav neviena darījuma." - }, "noWebcamFound": { "message": "Jūsu datora tīmekļa kamera netika atrasta. Lūdzu, mēģiniet vēlreiz." }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 20161aaf34da..3605febbc7d7 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -430,9 +430,6 @@ "noConversionRateAvailable": { "message": "Tiada Kadar Penukaran yang Tersedia" }, - "noTransactions": { - "message": "Anda tiada transaksi" - }, "noWebcamFound": { "message": "Webcam komputer anda tidak dijumpai. Sila cuba semula." }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index 7ac22889df2d..acf66091845f 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -183,9 +183,6 @@ "next": { "message": "volgende" }, - "noTransactions": { - "message": "Geen transacties" - }, "pastePrivateKey": { "message": "Plak hier uw privésleutelstring:", "description": "For importing an account from a private key" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 67948544fdbf..8459d3ef3a5f 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen konverteringsrate tilgjengelig " }, - "noTransactions": { - "message": "Du har ingen transaksjoner" - }, "noWebcamFound": { "message": "Datamaskinens webkamera ble ikke funnet. Vennligst prøv igjen." }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index af995dbf5f5f..3dd9ae28d896 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -819,9 +819,6 @@ "noConversionRateAvailable": { "message": "Hindi Available ang Rate ng Conversion" }, - "noTransactions": { - "message": "Wala kang transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukan ulit." }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 5baeee9b8a00..f7f5111c9973 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -440,9 +440,6 @@ "noConversionRateAvailable": { "message": "Brak kursu waluty" }, - "noTransactions": { - "message": "Nie ma transakcji" - }, "noWebcamFound": { "message": "Twoja kamera nie została znaleziona. Spróbuj ponownie." }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 50bf0a7d9996..e4fe18f9bab9 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Não, obrigado" }, - "noTransactions": { - "message": "Você não tem transações" - }, "noWebcamFound": { "message": "A webcam do seu computador não foi encontrada. Tente novamente." }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 3c61987263b7..3c9c30147276 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1273,9 +1273,6 @@ "noConversionRateAvailable": { "message": "Não há uma taxa de conversão disponível" }, - "noTransactions": { - "message": "Você não tem transações" - }, "noWebcamFound": { "message": "A webcam do seu computador não foi encontrada. Tente novamente." }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index f149a976cf1a..db440fcfd264 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -434,9 +434,6 @@ "noConversionRateAvailable": { "message": "Nici o rată de conversie disponibilă" }, - "noTransactions": { - "message": "Nu aveți tranzacții" - }, "noWebcamFound": { "message": "Webcam-ul computerului dvs. nu a fost găsit. Vă rugăm să încercați din nou." }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b1445befdd17..984223a22503 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Нет, спасибо" }, - "noTransactions": { - "message": "У вас нет транзакций" - }, "noWebcamFound": { "message": "Веб-камера вашего компьютера не найдена. Попробуйте еще раз." }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 1616af135d1a..a65bc68593f6 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -418,9 +418,6 @@ "noConversionRateAvailable": { "message": "Nie je k dispozícii žiadna sadzba konverzie" }, - "noTransactions": { - "message": "Žádné transakce" - }, "noWebcamFound": { "message": "Webová kamera vášho počítača sa nenašla. Skúste znova." }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index dd73bc8e373f..ebc303e90c5c 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Menjalni tečaj ni na voljo" }, - "noTransactions": { - "message": "Nimate transakcij" - }, "noWebcamFound": { "message": "Spletna kamera ni najdena. Poskusite znova kasneje." }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index c3986a0c0b98..1bbcd0754b92 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -434,9 +434,6 @@ "noConversionRateAvailable": { "message": "Nije dostupan kurs za konverziju" }, - "noTransactions": { - "message": "Nemate transakcije" - }, "noWebcamFound": { "message": "Nije pronađena veb kamera na vašem kompjuteru. Molimo vas pokušajte ponovo." }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 278515e908eb..818f1eb498d6 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -431,9 +431,6 @@ "noConversionRateAvailable": { "message": "Ingen omräkningskurs tillgänglig" }, - "noTransactions": { - "message": "Du har inga överföringar" - }, "noWebcamFound": { "message": "Din dators webbkamera hittades inte. Vänligen försök igen." }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 7c4a52f733fa..c33abcb51a18 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -425,9 +425,6 @@ "noConversionRateAvailable": { "message": "Hakuna Kiwango cha Ubadilishaji" }, - "noTransactions": { - "message": "Huna miamala." - }, "noWebcamFound": { "message": "Kamera yako ya kumpyuta haikupatikana. Tafadhali jaribu tena." }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index c5c36501cbb9..c5905e4e0bee 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -253,9 +253,6 @@ "next": { "message": "அடுத்தது" }, - "noTransactions": { - "message": "பரிவர்த்தனைகள் இல்லை" - }, "off": { "message": "ஆஃப்" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 08c9cb0df6dc..96166c6fb6ba 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -232,9 +232,6 @@ "next": { "message": "ถัดไป" }, - "noTransactions": { - "message": "ยังไม่มีรายการธุรกรรม" - }, "on": { "message": "เปิด" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 327be7838680..04650491ac24 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Salamat na lang" }, - "noTransactions": { - "message": "Wala kang transaksyon" - }, "noWebcamFound": { "message": "Hindi nakita ang webcam ng iyong computer. Pakisubukan ulit." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 35ec9700e7d0..06cba108c1e0 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Hayır, istemiyorum" }, - "noTransactions": { - "message": "İşleminiz yok" - }, "noWebcamFound": { "message": "Bilgisayarınızın web kamerası bulunamadı. Lütfen tekrar deneyin." }, diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 36a181b03ab2..58787e9a2238 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -443,9 +443,6 @@ "noConversionRateAvailable": { "message": "Немає доступного обмінного курсу" }, - "noTransactions": { - "message": "У вас немає транзакцій" - }, "noWebcamFound": { "message": "Веб-камеру комп’ютера не знайдено. Повторіть спробу." }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 21b187998966..6f9b8264e82a 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "Không, cảm ơn" }, - "noTransactions": { - "message": "Bạn không có giao dịch nào" - }, "noWebcamFound": { "message": "Không tìm thấy webcam trên máy tính của bạn. Vui lòng thử lại." }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 635753750343..2f011db8e804 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -3304,9 +3304,6 @@ "noThanks": { "message": "不,谢谢" }, - "noTransactions": { - "message": "您没有任何交易" - }, "noWebcamFound": { "message": "未找到您电脑的网络摄像头。请重试。" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 87d8ebfb5520..7b4a425a03ad 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -812,9 +812,6 @@ "noConversionRateAvailable": { "message": "尚未有匯率比較值" }, - "noTransactions": { - "message": "尚未有交易" - }, "noWebcamFound": { "message": "無法搜尋到攝影鏡頭裝置。請再試一次" }, diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js index b85937f5b6bd..72dbbe638e3b 100644 --- a/test/e2e/tests/transaction/multiple-transactions.spec.js +++ b/test/e2e/tests/transaction/multiple-transactions.spec.js @@ -122,12 +122,6 @@ describe('Multiple transactions', function () { '[data-testid="account-overview__activity-tab"]', ); - const isTransactionListEmpty = - await driver.isElementPresentAndVisible( - '.transaction-list__empty-text', - ); - assert.equal(isTransactionListEmpty, true); - // The previous isTransactionListEmpty wait already serves as the guard here for the assertElementNotPresent await driver.assertElementNotPresent( '.transaction-list__completed-transactions .activity-list-item', @@ -244,12 +238,6 @@ describe('Multiple transactions', function () { '[data-testid="account-overview__activity-tab"]', ); - const isTransactionListEmpty = - await driver.isElementPresentAndVisible( - '.transaction-list__empty-text', - ); - assert.equal(isTransactionListEmpty, true); - // The previous isTransactionListEmpty wait already serves as the guard here for the assertElementNotPresent await driver.assertElementNotPresent( '.transaction-list__completed-transactions .activity-list-item', diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index 27b0e1b5c60c..8ea78dee1948 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -13,10 +13,7 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../../shared/modules/selectors/networks'; +import { getCurrentChainId } from '../../../../shared/modules/selectors/networks'; import { getSelectedAccount, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -143,7 +140,6 @@ export default function TransactionList({ hideTokenTransactions, tokenAddress, boxProps, - tokenChainId, }) { const [limit, setLimit] = useState(PAGE_INCREMENT); const t = useI18nContext(); @@ -156,16 +152,7 @@ export default function TransactionList({ ); const chainId = useSelector(getCurrentChainId); - const networkConfigurationsByChainId = useSelector( - getNetworkConfigurationsByChainId, - ); - const networkName = networkConfigurationsByChainId[tokenChainId]?.name; const selectedAccount = useSelector(getSelectedAccount); - const isChainIdMismatch = tokenChainId && tokenChainId !== chainId; - - const noTransactionsMessage = networkName - ? t('noTransactionsNetworkName', [networkName]) - : t('noTransactionsChainIdMismatch'); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const shouldHideZeroBalanceTokens = useSelector( @@ -358,51 +345,43 @@ export default function TransactionList({ )} - {completedTransactions.length > 0 ? ( - completedTransactions - .map(removeIncomingTxsButToAnotherAddress) - .map(removeTxGroupsWithNoTx) - .filter(dateGroupsWithTransactionGroups) - .slice(0, limit) - .map((dateGroup) => { - return dateGroup.transactionGroups.map( - (transactionGroup, index) => { - return ( - - {renderDateStamp(index, dateGroup)} - {transactionGroup.initialTransaction - ?.isSmartTransaction ? ( - - ) : ( - - )} - - ); - }, - ); - }) - ) : ( - - - {isChainIdMismatch - ? noTransactionsMessage - : t('noTransactions')} - - - )} + {completedTransactions.length > 0 + ? completedTransactions + .map(removeIncomingTxsButToAnotherAddress) + .map(removeTxGroupsWithNoTx) + .filter(dateGroupsWithTransactionGroups) + .slice(0, limit) + .map((dateGroup) => { + return dateGroup.transactionGroups.map( + (transactionGroup, index) => { + return ( + + {renderDateStamp(index, dateGroup)} + {transactionGroup.initialTransaction + ?.isSmartTransaction ? ( + + ) : ( + + )} + + ); + }, + ); + }) + : null} {completedTransactions.length > limit && ( ); }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/banner.ts b/ui/components/app/snaps/snap-ui-renderer/components/banner.ts new file mode 100644 index 000000000000..fceb03d4be5d --- /dev/null +++ b/ui/components/app/snaps/snap-ui-renderer/components/banner.ts @@ -0,0 +1,20 @@ +import { BannerElement, JSXElement } from '@metamask/snaps-sdk/jsx'; +import { getJsxChildren } from '@metamask/snaps-utils'; +import { mapToTemplate } from '../utils'; +import { UIComponentFactory } from './types'; + +export const banner: UIComponentFactory = ({ + element, + ...params +}) => { + return { + element: 'SnapUIBanner', + children: getJsxChildren(element).map((children) => + mapToTemplate({ element: children as JSXElement, ...params }), + ), + props: { + title: element.props.title, + severity: element.props.severity, + }, + }; +}; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/button.ts b/ui/components/app/snaps/snap-ui-renderer/components/button.ts index f624ffb23195..4b0cdb808e79 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/button.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/button.ts @@ -2,6 +2,7 @@ import { ButtonElement, JSXElement } from '@metamask/snaps-sdk/jsx'; import { getJsxChildren } from '@metamask/snaps-utils'; import { NonEmptyArray } from '@metamask/utils'; import { mapTextToTemplate } from '../utils'; +import { TextVariant } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; export const button: UIComponentFactory = ({ @@ -15,6 +16,9 @@ export const button: UIComponentFactory = ({ variant: element.props.variant, name: element.props.name, disabled: element.props.disabled, + loading: element.props.loading, + textVariant: + element.props.size === 'sm' ? TextVariant.bodySm : TextVariant.bodyMd, }, children: mapTextToTemplate( getJsxChildren(element) as NonEmptyArray, 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 17a9b6aa37c1..f6173b7199b0 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/index.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/index.ts @@ -27,6 +27,7 @@ import { selector } from './selector'; import { icon } from './icon'; import { section } from './section'; import { avatar } from './avatar'; +import { banner } from './banner'; export const COMPONENT_MAPPING = { Box: box, @@ -58,4 +59,5 @@ export const COMPONENT_MAPPING = { Container: container, Selector: selector, Section: section, + Banner: banner, }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/text.ts b/ui/components/app/snaps/snap-ui-renderer/components/text.ts index fe9194817ca3..96bb6c520e48 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/text.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/text.ts @@ -6,32 +6,45 @@ import { TextVariant, OverflowWrap, TextColor, + FontWeight, } from '../../../../../helpers/constants/design-system'; import { UIComponentFactory } from './types'; +function getTextColor(color: TextElement['props']['color']) { + switch (color) { + case 'default': + return TextColor.textDefault; + case 'alternative': + return TextColor.textAlternative; + case 'muted': + return TextColor.textMuted; + case 'error': + return TextColor.errorDefault; + case 'success': + return TextColor.successDefault; + case 'warning': + return TextColor.warningDefault; + default: + return TextColor.inherit; + } +} + +function getFontWeight(color: TextElement['props']['fontWeight']) { + switch (color) { + case 'bold': + return FontWeight.Bold; + case 'medium': + return FontWeight.Medium; + case 'regular': + default: + return FontWeight.Normal; + } +} + export const text: UIComponentFactory = ({ element, ...params }) => { - const getTextColor = () => { - switch (element.props.color) { - case 'default': - return TextColor.textDefault; - case 'alternative': - return TextColor.textAlternative; - case 'muted': - return TextColor.textMuted; - case 'error': - return TextColor.errorDefault; - case 'success': - return TextColor.successDefault; - case 'warning': - return TextColor.warningDefault; - default: - return TextColor.inherit; - } - }; - return { element: 'Text', children: mapTextToTemplate( @@ -41,8 +54,9 @@ export const text: UIComponentFactory = ({ props: { variant: element.props.size === 'sm' ? TextVariant.bodySm : TextVariant.bodyMd, + fontWeight: getFontWeight(element.props.fontWeight), overflowWrap: OverflowWrap.Anywhere, - color: getTextColor(), + color: getTextColor(element.props.color), className: 'snap-ui-renderer__text', textAlign: element.props.alignment, }, diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss index ce149f922879..6efdbab6c51e 100644 --- a/ui/components/app/tab-bar/index.scss +++ b/ui/components/app/tab-bar/index.scss @@ -38,11 +38,16 @@ display: flex; align-items: center; position: relative; + overflow: hidden; width: 100%; &__title { @include design-system.H4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + @include design-system.screen-sm-min { @include design-system.H6; } diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index da12a52be812..ac21c32a2b1b 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -62,6 +62,9 @@ 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 SNAP_SETTINGS_ROUTE = '/settings/snap'; +PATH_NAME_MAP[`${SNAP_SETTINGS_ROUTE}/:snapId`] = 'Snap Settings Page'; + export const REVEAL_SEED_ROUTE = '/seed'; PATH_NAME_MAP[REVEAL_SEED_ROUTE] = 'Reveal Secret Recovery Phrase Page'; diff --git a/ui/helpers/utils/snaps.js b/ui/helpers/utils/snaps.js deleted file mode 100644 index 8a57ce516cf2..000000000000 --- a/ui/helpers/utils/snaps.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Check if the given value is a valid snap ID. - * - * NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils. - * - * @param value - The value to check. - * @returns `true` if the value is a valid snap ID, and `false` otherwise. - */ -export function isSnapId(value) { - return ( - (typeof value === 'string' || value instanceof String) && - (value.startsWith('local:') || value.startsWith('npm:')) - ); -} diff --git a/ui/helpers/utils/snaps.ts b/ui/helpers/utils/snaps.ts new file mode 100644 index 000000000000..c788393e83ab --- /dev/null +++ b/ui/helpers/utils/snaps.ts @@ -0,0 +1,40 @@ +import { SnapId } from '@metamask/snaps-sdk'; +import { isProduction } from '../../../shared/modules/environment'; + +/** + * Check if the given value is a valid snap ID. + * + * NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils. + * + * @param value - The value to check. + * @returns `true` if the value is a valid snap ID, and `false` otherwise. + */ +export function isSnapId(value: unknown): value is SnapId { + return ( + (typeof value === 'string' || value instanceof String) && + (value.startsWith('local:') || value.startsWith('npm:')) + ); +} + +/** + * Decode a snap ID fron a pathname. + * + * @param pathname - The pathname to decode the snap ID from. + * @returns The decoded snap ID, or `undefined` if the snap ID could not be decoded. + */ +export const decodeSnapIdFromPathname = (pathname: string) => { + const snapIdURI = pathname?.match(/[^/]+$/u)?.[0]; + return snapIdURI && decodeURIComponent(snapIdURI); +}; + +const IGNORED_EXAMPLE_SNAPS = ['npm:@metamask/preinstalled-example-snap']; + +/** + * Check if the given snap ID is ignored in production. + * + * @param snapId - The snap ID to check. + * @returns `true` if the snap ID is ignored in production, and `false` otherwise. + */ +export const isSnapIgnoredInProd = (snapId: string) => { + return isProduction() ? IGNORED_EXAMPLE_SNAPS.includes(snapId) : false; +}; diff --git a/ui/hooks/snaps/useSnapSettings.ts b/ui/hooks/snaps/useSnapSettings.ts new file mode 100644 index 000000000000..ad2debc8cac2 --- /dev/null +++ b/ui/hooks/snaps/useSnapSettings.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { + forceUpdateMetamaskState, + handleSnapRequest, +} from '../../store/actions'; + +export function useSnapSettings({ snapId }: { snapId?: string }) { + const dispatch = useDispatch(); + const [loading, setLoading] = useState(true); + const [data, setData] = useState<{ id: string } | undefined>(undefined); + const [error, setError] = useState(undefined); + + useEffect(() => { + let cancelled = false; + async function fetchPage(id: string) { + try { + setError(undefined); + setLoading(true); + + const newData = (await handleSnapRequest({ + snapId: id, + origin: '', + handler: 'onSettingsPage', + request: { + jsonrpc: '2.0', + method: ' ', + }, + })) as { id: string }; + if (!cancelled) { + setData(newData); + forceUpdateMetamaskState(dispatch); + } + } catch (err) { + if (!cancelled) { + setError(err as Error); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + } + + if (snapId) { + fetchPage(snapId); + } + + return () => { + cancelled = true; + }; + }, [snapId]); + + return { data, error, loading }; +} diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx index 3b5d1dfc3e62..336ef3f9f92f 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx @@ -149,7 +149,7 @@ describe('PersonalSignInfo', () => { getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { queryByText, getByText } = renderWithConfirmContextProvider( @@ -171,7 +171,7 @@ describe('PersonalSignInfo', () => { const state = getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { getByText, queryByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx index 2b1e6969ddd5..83696b69ac64 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx @@ -65,7 +65,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -88,7 +88,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx index 56421561ccd2..640b663e5cc1 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx @@ -153,7 +153,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -177,7 +177,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap index 12e984bc207a..656b1fc49740 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap @@ -46,7 +46,7 @@ exports[`SnapsSection renders section for typed sign request 1`] = ` style="overflow-y: auto;" >

Hello world again!

@@ -104,7 +104,7 @@ exports[`SnapsSection renders section personal sign request 1`] = ` style="overflow-y: auto;" >

Hello world!

diff --git a/ui/pages/settings/index.scss b/ui/pages/settings/index.scss index f57e1c310998..f9b08529ee65 100644 --- a/ui/pages/settings/index.scss +++ b/ui/pages/settings/index.scss @@ -56,7 +56,7 @@ &__title { @include design-system.screen-sm-min { - width: 197px; + margin-right: 16px; } @include design-system.screen-sm-max { @@ -230,6 +230,7 @@ display: flex; flex-direction: column; flex: 1 1 auto; + max-width: 100vw; @include design-system.screen-sm-min { flex: 0 0 40%; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 724c661c9aeb..37257e2c8fcb 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -21,6 +21,7 @@ import { ADD_POPULAR_CUSTOM_NETWORK, DEFAULT_ROUTE, NOTIFICATIONS_SETTINGS_ROUTE, + SNAP_SETTINGS_ROUTE, } from '../../helpers/constants/routes'; import { getSettingsRoutes } from '../../helpers/utils/settings-search'; @@ -31,6 +32,7 @@ import { IconName, Box, Text, + IconSize, } from '../../components/component-library'; import { AlignItems, @@ -44,6 +46,8 @@ import MetafoxLogo from '../../components/ui/metafox-logo'; // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; +import { SnapIcon } from '../../components/app/snaps/snap-icon'; +import { SnapSettingsRenderer } from '../../components/app/snaps/snap-settings-page'; import SettingsTab from './settings-tab'; import AdvancedTab from './advanced-tab'; import InfoTab from './info-tab'; @@ -70,6 +74,8 @@ class SettingsPage extends PureComponent { mostRecentOverviewPage: PropTypes.string.isRequired, pathnameI18nKey: PropTypes.string, remoteFeatureFlags: PropTypes.object.isRequired, + settingsPageSnaps: PropTypes.array, + snapSettingsTitle: PropTypes.string, toggleNetworkMenu: PropTypes.func.isRequired, useExternalServices: PropTypes.bool, }; @@ -210,19 +216,24 @@ class SettingsPage extends PureComponent { renderTitle() { const { t } = this.context; - const { isPopup, pathnameI18nKey, addressName } = this.props; + const { isPopup, pathnameI18nKey, addressName, snapSettingsTitle } = + this.props; let titleText; if (isPopup && addressName) { titleText = t('details'); } else if (pathnameI18nKey && isPopup) { titleText = t(pathnameI18nKey); + } else if (snapSettingsTitle) { + titleText = snapSettingsTitle; } else { titleText = t('settings'); } return (
- {titleText} + + {titleText} +
); } @@ -293,15 +304,31 @@ class SettingsPage extends PureComponent { } renderTabs() { - const { history, currentPath, useExternalServices } = this.props; + const { history, currentPath, useExternalServices, settingsPageSnaps } = + this.props; const { t } = this.context; + const snapsSettings = settingsPageSnaps.map(({ id, name }) => { + return { + content: name, + icon: ( + + ), + key: `${SNAP_SETTINGS_ROUTE}/${encodeURIComponent(id)}`, + }; + }); + const tabs = [ { content: t('general'), icon: , key: GENERAL_ROUTE, }, + ...snapsSettings, { content: t('advanced'), icon: , @@ -390,6 +417,10 @@ class SettingsPage extends PureComponent { )} /> + { metamask: { currencyRates }, } = state; const remoteFeatureFlags = getRemoteFeatureFlags(state); + + const settingsPageSnapsIds = getSettingsPageSnapsIds(state); + const snapsMetadata = getSnapsMetadata(state); const conversionDate = currencyRates[ticker]?.conversionDate; const pathNameTail = pathname.match(/[^/]+$/u)[0]; @@ -75,6 +83,7 @@ const mapStateToProps = (state, ownProps) => { const isAddPopularCustomNetwork = Boolean( pathname.match(ADD_POPULAR_CUSTOM_NETWORK), ); + const isSnapSettingsRoute = Boolean(pathname.match(SNAP_SETTINGS_ROUTE)); const isPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; const pathnameI18nKey = ROUTES_TO_I18N_KEYS[pathname]; @@ -102,6 +111,16 @@ const mapStateToProps = (state, ownProps) => { ); const useExternalServices = getUseExternalServices(state); + const snapNameGetter = getSnapName(snapsMetadata); + + const settingsPageSnaps = settingsPageSnapsIds.map((snapId) => ({ + id: snapId, + name: snapNameGetter(snapId), + })); + + const snapSettingsTitle = + isSnapSettingsRoute && snapNameGetter(decodeSnapIdFromPathname(pathname)); + return { addNewNetwork, addressName, @@ -115,6 +134,8 @@ const mapStateToProps = (state, ownProps) => { mostRecentOverviewPage: getMostRecentOverviewPage(state), pathnameI18nKey, remoteFeatureFlags, + settingsPageSnaps, + snapSettingsTitle, useExternalServices, }; }; diff --git a/ui/pages/settings/settings.stories.js b/ui/pages/settings/settings.stories.js index 53437f4175db..b6b695a89cb1 100644 --- a/ui/pages/settings/settings.stories.js +++ b/ui/pages/settings/settings.stories.js @@ -62,6 +62,7 @@ const Settings = ({ history }) => { pathnameI18nKey={pathnameI18nKey} backRoute={SETTINGS_ROUTE} remoteFeatureFlags={{}} + settingsPageSnaps={[]} /> ); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 403db6cadb91..ce117af76262 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -13,6 +13,7 @@ import { NameType } from '@metamask/name-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import { isEvmAccountType } from '@metamask/keyring-api'; import { RpcEndpointType } from '@metamask/network-controller'; +import { SnapEndowments } from '@metamask/snaps-rpc-methods'; import { getCurrentChainId, getProviderConfig, @@ -111,6 +112,7 @@ import { BridgeFeatureFlagsKey } from '../../shared/types/bridge'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; +import { isSnapIgnoredInProd } from '../helpers/utils/snaps'; import { getAllUnapprovedTransactions, getCurrentNetworkTransactions, @@ -1918,6 +1920,19 @@ export const getInsightSnaps = createDeepEqualSelector( }, ); +export const getSettingsPageSnaps = createDeepEqualSelector( + getEnabledSnaps, + getPermissionSubjects, + (snaps, subjects) => { + return Object.values(snaps).filter( + ({ id, preinstalled }) => + subjects[id]?.permissions[SnapEndowments.SettingsPage] && + preinstalled && + !isSnapIgnoredInProd(id), + ); + }, +); + export const getSignatureInsightSnaps = createDeepEqualSelector( getEnabledSnaps, getPermissionSubjects, @@ -1948,6 +1963,11 @@ export const getNameLookupSnapsIds = createDeepEqualSelector( }, ); +export const getSettingsPageSnapsIds = createDeepEqualSelector( + getSettingsPageSnaps, + (snaps) => snaps.map((snap) => snap.id), +); + export const getNotifySnaps = createDeepEqualSelector( getEnabledSnaps, getPermissionSubjects, diff --git a/yarn.lock b/yarn.lock index 530e073f615d..e00947e58386 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6063,12 +6063,12 @@ __metadata: languageName: node linkType: hard -"@metamask/preinstalled-example-snap@npm:^0.2.0": - version: 0.2.0 - resolution: "@metamask/preinstalled-example-snap@npm:0.2.0" +"@metamask/preinstalled-example-snap@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/preinstalled-example-snap@npm:0.3.0" dependencies: - "@metamask/snaps-sdk": "npm:^6.9.0" - checksum: 10/f8ad6f42c9bd7ce3b7fc9b45eecda6191320ff762b48c482ba4944a6d7a228682b833c15e56058f26ac7bb10417dfe9de340af1c8eb9bbe5dc03c665426ccb13 + "@metamask/snaps-sdk": "npm:^6.14.0" + checksum: 10/add8f89c1b7327bc90486d868a9d4b7eff426ef98a5a96235fc6fdce4710c6d17842636ccd02db6638d061ce2b16939c6fe1f06e69cdde8bde2f6026c7b82df5 languageName: node linkType: hard @@ -6265,9 +6265,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.15.0": - version: 9.15.0 - resolution: "@metamask/snaps-controllers@npm:9.15.0" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.16.0": + version: 9.16.0 + resolution: "@metamask/snaps-controllers@npm:9.16.0" dependencies: "@metamask/approval-controller": "npm:^7.1.1" "@metamask/base-controller": "npm:^7.0.2" @@ -6280,9 +6280,9 @@ __metadata: "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-rpc-methods": "npm:^11.7.0" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-rpc-methods": "npm:^11.8.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/utils": "npm:^10.0.0" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" @@ -6296,30 +6296,30 @@ __metadata: semver: "npm:^7.5.4" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.10.0 + "@metamask/snaps-execution-environments": ^6.11.0 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/dd849398c4deefbca55b3b4a5e3fe885c45012cd8132bb83367d024c0c2dc99b13be036aa84049d5a1ba1431f9fd66897623f3961a34ebbbf70fe7bce4db322e + checksum: 10/f0a9efaad8fac2aa833edd5df6a4929a84de31f3e11457d407f39793c9ecd3b94eff543135729691b125c32f4290183375ae6416bc04e4aad31466517727af4f languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.10.0": - version: 6.10.0 - resolution: "@metamask/snaps-execution-environments@npm:6.10.0" +"@metamask/snaps-execution-environments@npm:^6.11.0": + version: 6.11.0 + resolution: "@metamask/snaps-execution-environments@npm:6.11.0" dependencies: "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-sdk": "npm:^6.11.0" - "@metamask/snaps-utils": "npm:^8.6.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/a881696ec942f268d7485869fcb8c6bc0c278319bbfaf7e5c6099e86278c7f59049595f00ecfc27511d0106b5ad2f7621f734c7b17f088b835e38e638d80db01 + checksum: 10/3fc46e1b1d7e11996ce8c3694738d1cdab9b5d6c129a45e691b98f0d753e044869c3d0471729cba9e120bb2ff7ebd8e9aa644e608792903e74eee61213509b08 languageName: node linkType: hard @@ -6335,38 +6335,38 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.7.0": - version: 11.7.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.7.0" +"@metamask/snaps-rpc-methods@npm:^11.8.0": + version: 11.8.0 + resolution: "@metamask/snaps-rpc-methods@npm:11.8.0" dependencies: "@metamask/key-tree": "npm:^10.0.1" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" - checksum: 10/92e4131d15b8dd68a29bd845e6c795aab8c3299048eaff2c3970db78a5eb476d8841f6a612b42e878812bb0757f2126287581f4e12259846851f02d6e6d836f5 + checksum: 10/a84c3648195efaeaeb021bd86c8a90e3f555236a8804cb2191778dddcf2acf7ea23ebc30f4670f6669dc881736c28f387b6ca2fc61050393411ee947a86cd47b languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.13.0": - version: 6.13.0 - resolution: "@metamask/snaps-sdk@npm:6.13.0" +"@metamask/snaps-sdk@npm:^6.14.0": + version: 6.14.0 + resolution: "@metamask/snaps-sdk@npm:6.14.0" dependencies: "@metamask/key-tree": "npm:^10.0.1" "@metamask/providers": "npm:^18.1.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" - checksum: 10/115c738cb140810856ded055ac92a538c011adbd6a5f32a4e1fde42dcbd162c7eac182aab904de6b65af99b9520995d768627bd7f460da11d0aa359700e05b04 + checksum: 10/dafe8618418c607c5d962bbcf675324651254631791a257898f4a939c58a8b2f56a743dcff534aa7889662d5fb1a4dd1048558f4b025404e0dcd507ff5a5e89a languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.6.0, @metamask/snaps-utils@npm:^8.6.1": - version: 8.6.1 - resolution: "@metamask/snaps-utils@npm:8.6.1" +"@metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.7.0": + version: 8.7.0 + resolution: "@metamask/snaps-utils@npm:8.7.0" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -6376,7 +6376,7 @@ __metadata: "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.2" - "@metamask/snaps-sdk": "npm:^6.13.0" + "@metamask/snaps-sdk": "npm:^6.14.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" @@ -6391,7 +6391,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/d58e276b2849662e764a4ce5f45a03df361a5c65e9c09814e41b723407dffbd3a215626f5d9a8c4705a0cad73b702e10d76201174b44f92fbc68fdf59fb24d5d + checksum: 10/0681878e29c010853b610ed99569044feaa37b4cc92bafdba28b1eec68694d7779833fb4262a05a4d18182c6931d258531ed628e422d7c96567b61d1b710a95d languageName: node linkType: hard @@ -26663,7 +26663,7 @@ __metadata: "@metamask/post-message-stream": "npm:^8.0.0" "@metamask/ppom-validator": "npm:0.36.0" "@metamask/preferences-controller": "npm:^15.0.1" - "@metamask/preinstalled-example-snap": "npm:^0.2.0" + "@metamask/preinstalled-example-snap": "npm:^0.3.0" "@metamask/profile-sync-controller": "npm:^3.1.1" "@metamask/providers": "npm:^18.2.0" "@metamask/queued-request-controller": "npm:^7.0.1" @@ -26675,11 +26675,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/signature-controller": "npm:^23.1.0" "@metamask/smart-transactions-controller": "npm:^16.0.0" - "@metamask/snaps-controllers": "npm:^9.15.0" - "@metamask/snaps-execution-environments": "npm:^6.10.0" - "@metamask/snaps-rpc-methods": "npm:^11.7.0" - "@metamask/snaps-sdk": "npm:^6.13.0" - "@metamask/snaps-utils": "npm:^8.6.1" + "@metamask/snaps-controllers": "npm:^9.16.0" + "@metamask/snaps-execution-environments": "npm:^6.11.0" + "@metamask/snaps-rpc-methods": "npm:^11.8.0" + "@metamask/snaps-sdk": "npm:^6.14.0" + "@metamask/snaps-utils": "npm:^8.7.0" "@metamask/solana-wallet-snap": "npm:^1.0.4" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" From 36f084c904b97c5f34432e44b1434acc5dff5430 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 20 Dec 2024 15:34:09 +0000 Subject: [PATCH 18/71] fix(28081): design tweak for network badge (#29324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR address feedback from design quality check to tweak board color and width for network badge. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29324?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28081 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** See PR for related changes ### **After** See PR for related changes ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: georgewrmarshall --- .../__snapshots__/token-cell.test.tsx.snap | 2 +- .../detected-token-details/detected-token-details.js | 1 + .../__snapshots__/account-list-item.test.js.snap | 4 ++-- .../account-list-item/account-list-item.js | 2 +- .../multichain/badge-status/badge-status.stories.tsx | 2 +- .../multichain/badge-status/badge-status.tsx | 2 +- .../connect-accounts-modal.test.tsx.snap | 2 +- .../__snapshots__/connected-site-menu.test.js.snap | 4 ++-- .../connected-site-menu/connected-site-menu.js | 4 ++-- .../multichain/connected-status/connected-status.tsx | 7 +++---- .../__snapshots__/connections.test.tsx.snap | 2 +- .../pages/send/__snapshots__/send.test.js.snap | 2 +- .../__snapshots__/your-accounts.test.tsx.snap | 12 ++++++------ .../__snapshots__/token-list-item.test.tsx.snap | 12 ++++++------ .../multichain/token-list-item/token-list-item.tsx | 5 ++--- .../__snapshots__/asset-page.test.tsx.snap | 6 +++--- .../__snapshots__/remove-snap-account.test.js.snap | 2 +- 17 files changed, 35 insertions(+), 36 deletions(-) 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 7fb51f212ebd..3ec0face8295 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 @@ -28,7 +28,7 @@ exports[`Token Cell should match snapshot 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo } marginRight={2} 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 fb3bed6fd34b..cd0bb60f90e1 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 @@ -116,7 +116,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam style="bottom: -1px; right: 2px;" >
@@ -424,7 +424,7 @@ exports[`AccountListItem renders AccountListItem component and shows account nam style="bottom: -1px; right: 2px;" >
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 3ab048597f40..2c080c940d8a 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -62,7 +62,7 @@ import { getMultichainShouldShowFiat, } from '../../../selectors/multichain'; import { useMultichainAccountTotalFiatBalance } from '../../../hooks/useMultichainAccountTotalFiatBalance'; -import { ConnectedStatus } from '../connected-status/connected-status'; +import { ConnectedStatus } from '../connected-status'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors'; import { useTheme } from '../../../hooks/useTheme'; diff --git a/ui/components/multichain/badge-status/badge-status.stories.tsx b/ui/components/multichain/badge-status/badge-status.stories.tsx index b6cc485ef0e6..7122e10468a9 100644 --- a/ui/components/multichain/badge-status/badge-status.stories.tsx +++ b/ui/components/multichain/badge-status/badge-status.stories.tsx @@ -42,7 +42,7 @@ export const DefaultStory = Template.bind({}); export const NotConnectedStory = Template.bind({}); NotConnectedStory.args = { - badgeBackgroundColor: Color.borderMuted, + badgeBackgroundColor: BackgroundColor.iconAlternative, badgeBorderColor: BackgroundColor.backgroundDefault, }; diff --git a/ui/components/multichain/badge-status/badge-status.tsx b/ui/components/multichain/badge-status/badge-status.tsx index d4e3668bccd8..30e156779308 100644 --- a/ui/components/multichain/badge-status/badge-status.tsx +++ b/ui/components/multichain/badge-status/badge-status.tsx @@ -74,7 +74,7 @@ export const BadgeStatus: React.FC = ({ backgroundColor={badgeBackgroundColor} borderRadius={BorderRadius.full} borderColor={badgeBorderColor} - borderWidth={isConnectedAndNotActive ? 2 : 4} + borderWidth={2} /> } > diff --git a/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap b/ui/components/multichain/connect-accounts-modal/__snapshots__/connect-accounts-modal.test.tsx.snap index b4a4836db2d6..8bcb788bb745 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 @@ -232,7 +232,7 @@ exports[`Connect More Accounts Modal should render correctly 1`] = ` style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap b/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap index fa6f6a0b202a..18b794d1e971 100644 --- a/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap +++ b/ui/components/multichain/connected-site-menu/__snapshots__/connected-site-menu.test.js.snap @@ -32,7 +32,7 @@ exports[`Connected Site Menu should render the site menu in connected state 1`] style="bottom: -1px; right: -4px; z-index: 1;" >
@@ -74,7 +74,7 @@ exports[`Connected Site Menu should render the site menu in not connected state style="bottom: -1px; right: -4px; z-index: 1;" >
diff --git a/ui/components/multichain/connected-site-menu/connected-site-menu.js b/ui/components/multichain/connected-site-menu/connected-site-menu.js index 4f532710288e..cbceafa7d99f 100644 --- a/ui/components/multichain/connected-site-menu/connected-site-menu.js +++ b/ui/components/multichain/connected-site-menu/connected-site-menu.js @@ -88,9 +88,9 @@ export const ConnectedSiteMenu = ({ borderColor={ isConnectedtoOtherAccountOrSnap ? BorderColor.successDefault - : BackgroundColor.backgroundDefault + : BorderColor.backgroundDefault } - borderWidth={isConnectedtoOtherAccountOrSnap ? 2 : 3} + borderWidth={2} /> } > diff --git a/ui/components/multichain/connected-status/connected-status.tsx b/ui/components/multichain/connected-status/connected-status.tsx index 83dc0c77ec45..8de3d7c4b38b 100644 --- a/ui/components/multichain/connected-status/connected-status.tsx +++ b/ui/components/multichain/connected-status/connected-status.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux'; import { BackgroundColor, BorderColor, - Color, } from '../../../helpers/constants/design-system'; import { isAccountConnectedToCurrentTab } from '../../../selectors'; import { @@ -43,11 +42,11 @@ export const ConnectedStatus: React.FC = ({ status = STATUS_CONNECTED_TO_ANOTHER_ACCOUNT; } - let badgeBorderColor = BackgroundColor.backgroundDefault; // TODO: Replace it once border-color has this value. - let badgeBackgroundColor = Color.borderMuted; // //TODO: Replace it once Background color has this value. + let badgeBorderColor = BorderColor.backgroundDefault; // TODO: Replace it once border-color has this value. + let badgeBackgroundColor = BackgroundColor.iconAlternative; let tooltipText = t('statusNotConnected'); if (status === STATUS_CONNECTED) { - badgeBorderColor = BackgroundColor.backgroundDefault; + badgeBorderColor = BorderColor.backgroundDefault; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: type 'string' can't be used to index type '{}' badgeBackgroundColor = BackgroundColor.successDefault; diff --git a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap index ad2dc490d7c0..9250404743fc 100644 --- a/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap +++ b/ui/components/multichain/pages/connections/__snapshots__/connections.test.tsx.snap @@ -171,7 +171,7 @@ exports[`Connections Content should render correctly 1`] = ` style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap index 7b0605b7ea60..a23161205c8e 100644 --- a/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap +++ b/ui/components/multichain/pages/send/__snapshots__/send.test.js.snap @@ -348,7 +348,7 @@ exports[`SendPage render and initialization should render correctly even when a style="bottom: -1px; right: 2px;" >
diff --git a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap index 71431a330f94..8ffdb6565c93 100644 --- a/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap +++ b/ui/components/multichain/pages/send/components/__snapshots__/your-accounts.test.tsx.snap @@ -122,7 +122,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -421,7 +421,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -720,7 +720,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1028,7 +1028,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1327,7 +1327,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
@@ -1639,7 +1639,7 @@ exports[`SendPageYourAccounts render renders correctly 1`] = ` style="bottom: -1px; right: 2px;" >
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 2359a5d9a5a0..e0efd4cd3fe6 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 @@ -24,7 +24,7 @@ exports[`TokenListItem handles clicking staking opens tab 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo
?
@@ -191,7 +191,7 @@ exports[`TokenListItem should display warning scam modal fallback when safechain class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
?
@@ -270,7 +270,7 @@ exports[`TokenListItem should render correctly 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
network logo
?
@@ -403,7 +403,7 @@ exports[`TokenListItem should render crypto balance with warning scam 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
?
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 40b91a001f17..d5ce348e0297 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -40,7 +40,6 @@ import { } from '../../component-library'; import { getMetaMetricsId, - getTestNetworkBackgroundColor, getParticipateInMetaMetrics, getDataCollectionForMarketing, getMarketData, @@ -228,7 +227,6 @@ export const TokenListItem = ({ ); // Used for badge icon const allNetworks = useSelector(getNetworkConfigurationsByChainId); - const testNetworkBackgroundColor = useSelector(getTestNetworkBackgroundColor); return ( } 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 162f017afd0a..2caa7f6eb5bb 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -198,7 +198,7 @@ exports[`AssetPage should render a native asset 1`] = ` class="mm-box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--rectangular-bottom-right" >
static-logo
static-logo
static-logo
From d510d5cab2e7e5265e9cf6580498a4a03ede660c Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 20 Dec 2024 15:45:29 +0000 Subject: [PATCH 19/71] feat(14507): improve error message for failed txn in activity details view (#29338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** When we encounter a failed transaction: - in activity list from home view, it renders as `failed`, hovering the status will render an error message - when clicking this activity, the popup view for txn details will render `failed` as well, with no hover effect - In the meanwhile, there's a new and more user friendly error banner showing for user when txn failed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29338?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/14507 Figma: https://www.figma.com/design/ZzVQ6iu13C67K807Z1bg5I/Smart-Transactions-1.0?node-id=4296-25303&t=ff749RbiH6F4IUqk-0 ## **Manual testing steps** 1. Render extension and test dapp 2. Trigger a failed txn from test dapp 3. Check activity for that txn 4. Check activity details by clicking item from step 3 and validate the banner, as well as hover ## **Screenshots/Recordings** ### **Before** Screenshot 2024-12-18 at 18 25 45 ### **After** Screenshot 2024-12-19 at 01 54 39 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../failing-contract.spec.js | 9 + .../transaction-list-item-details/index.scss | 7 +- ...transaction-list-item-details.component.js | 53 ++- ...action-list-item-details.component.test.js | 66 ++-- .../smart-transaction-list-item.component.js | 1 + .../transaction-list-item.component.js | 2 + .../transaction-status-label.test.js.snap | 10 +- .../transaction-status-label.js | 20 +- .../transaction-status-label.test.js | 370 +++++++----------- 10 files changed, 258 insertions(+), 283 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 30475784e64a..0ef2ec82406f 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6407,6 +6407,9 @@ "transactionFailed": { "message": "Transaction Failed" }, + "transactionFailedBannerMessage": { + "message": "This transaction would have cost you extra fees, so we stopped it. Your money is still in your wallet." + }, "transactionFee": { "message": "Transaction fee" }, diff --git a/test/e2e/tests/dapp-interactions/failing-contract.spec.js b/test/e2e/tests/dapp-interactions/failing-contract.spec.js index c05938d668e0..c02a279adb3f 100644 --- a/test/e2e/tests/dapp-interactions/failing-contract.spec.js +++ b/test/e2e/tests/dapp-interactions/failing-contract.spec.js @@ -72,6 +72,15 @@ describe('Failing contract interaction ', function () { css: '.activity-list-item .transaction-status-label', text: 'Failed', }); + // inspect transaction details + await driver.clickElement({ + css: '.activity-list-item .transaction-status-label', + text: 'Failed', + }); + await driver.waitForSelector('.transaction-list-item-details'); + await driver.waitForSelector( + '[data-testid="transaction-list-item-details-banner-error-message"]', + ); }, ); }); diff --git a/ui/components/app/transaction-list-item-details/index.scss b/ui/components/app/transaction-list-item-details/index.scss index 13adb780fa4d..34d4af1ea82e 100644 --- a/ui/components/app/transaction-list-item-details/index.scss +++ b/ui/components/app/transaction-list-item-details/index.scss @@ -49,6 +49,8 @@ display: flex; flex-direction: column; align-items: flex-end; + height: 42px; + justify-content: space-between; .btn-link { font-size: 12px; @@ -62,9 +64,10 @@ } &__operations { - margin: 0 0 16px 16px; + margin: 0 16px 16px 16px; display: flex; - justify-content: flex-end; + justify-content: space-between; + align-items: center; } &__header { diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 226a2a9113c0..f614a4dbc250 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -13,9 +13,19 @@ import Tooltip from '../../ui/tooltip'; import CancelButton from '../cancel-button'; import Popover from '../../ui/popover'; import { Box } from '../../component-library/box'; +import { Text } from '../../component-library/text'; +import { + BannerAlert, + BannerAlertSeverity, +} from '../../component-library/banner-alert'; +import { + TextVariant, + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + IconColor, + ///: END:ONLY_INCLUDE_IF +} from '../../../helpers/constants/design-system'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import { Icon, IconName, Text } from '../../component-library'; -import { IconColor } from '../../../helpers/constants/design-system'; +import { Icon, IconName } from '../../component-library'; ///: END:ONLY_INCLUDE_IF import { SECOND } from '../../../../shared/constants/time'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; @@ -55,6 +65,7 @@ export default class TransactionListItemDetails extends PureComponent { recipientNickname: PropTypes.string, transactionStatus: PropTypes.func, isCustomNetwork: PropTypes.bool, + showErrorBanner: PropTypes.bool, history: PropTypes.object, blockExplorerLinkText: PropTypes.object, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -204,6 +215,7 @@ export default class TransactionListItemDetails extends PureComponent { onClose, recipientNickname, showCancel, + showErrorBanner, transactionStatus: TransactionStatus, blockExplorerLinkText, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -220,6 +232,17 @@ export default class TransactionListItemDetails extends PureComponent {
+ {showErrorBanner && ( + + + {t('transactionFailedBannerMessage')} + + + )}
{showSpeedUp && ( )} @@ -258,22 +281,18 @@ export default class TransactionListItemDetails extends PureComponent { data-testid="transaction-list-item-details-tx-status" >
{t('status')}
-
- -
+
-
- -
+
{ +const render = (overrideProps) => { const rpcPrefs = { blockExplorerUrl: 'https://customblockexplorer.com/', }; @@ -71,7 +71,7 @@ const render = async (overrideProps) => { senderAddress: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', tryReverseResolveAddress: jest.fn(), transactionGroup, - transactionStatus: () =>
, + transactionStatus: () =>
, blockExplorerLinkText, rpcPrefs, ...overrideProps, @@ -79,59 +79,57 @@ const render = async (overrideProps) => { const mockStore = configureMockStore([thunk])(mockState); - let result; - - await act( - async () => - (result = renderWithProvider( - , - mockStore, - )), + const result = renderWithProvider( + , + mockStore, ); return result; }; describe('TransactionListItemDetails Component', () => { - it('should render title with title prop', async () => { - const { queryByText } = await render(); + describe('matches snapshot', () => { + it('for non-error details', async () => { + const { queryByText, queryByTestId } = render(); + expect(queryByText('Test Transaction Details')).toBeInTheDocument(); + expect( + queryByTestId('transaction-list-item-details-banner-error-message'), + ).not.toBeInTheDocument(); + }); - await waitFor(() => { + it('for error details', async () => { + const { queryByText, queryByTestId } = render({ showErrorBanner: true }); expect(queryByText('Test Transaction Details')).toBeInTheDocument(); + expect( + queryByTestId('transaction-list-item-details-banner-error-message'), + ).toBeInTheDocument(); }); }); - describe('Retry button', () => { - it('should render retry button with showRetry prop', async () => { - const { queryByTestId } = await render({ showRetry: true }); - + describe('Action buttons', () => { + it('renders retry button with showRetry prop', async () => { + const { queryByTestId } = render({ showRetry: true }); expect(queryByTestId('rety-button')).toBeInTheDocument(); }); - }); - - describe('Cancel button', () => { - it('should render cancel button with showCancel prop', async () => { - const { queryByTestId } = await render({ showCancel: true }); + it('renders cancel button with showCancel prop', async () => { + const { queryByTestId } = render({ showCancel: true }); expect(queryByTestId('cancel-button')).toBeInTheDocument(); }); - }); - - describe('Speedup button', () => { - it('should render speedup button with showSpeedUp prop', async () => { - const { queryByTestId } = await render({ showSpeedUp: true }); + it('renders speedup button with showSpeedUp prop', async () => { + const { queryByTestId } = render({ showSpeedUp: true }); expect(queryByTestId('speedup-button')).toBeInTheDocument(); }); }); describe('Institutional', () => { - it('should render correctly if custodyTransactionDeepLink has a url', async () => { + it('renders correctly if custodyTransactionDeepLink has a url', async () => { mockGetCustodianTransactionDeepLink = jest .fn() .mockReturnValue({ url: 'https://url.com' }); - await render({ showCancel: true }); + render({ showCancel: true }); await waitFor(() => { const custodianViewButton = document.querySelector( @@ -143,7 +141,7 @@ describe('TransactionListItemDetails Component', () => { }); }); - it('should render correctly if transactionNote is provided', async () => { + it('renders correctly if transactionNote is provided', async () => { const newTransaction = { ...transaction, metadata: { @@ -159,13 +157,11 @@ describe('TransactionListItemDetails Component', () => { initialTransaction: newTransaction, }; - const { queryByText } = await render({ + const { queryByText } = render({ transactionGroup: newTransactionGroup, }); - await waitFor(() => { - expect(queryByText('some note')).toBeInTheDocument(); - }); + expect(queryByText('some note')).toBeInTheDocument(); }); }); }); diff --git a/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js b/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js index 5c1658918dbe..547f2e460497 100644 --- a/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/smart-transaction-list-item.component.js @@ -125,6 +125,7 @@ export default function SmartTransactionListItem({ date={date} status={displayedStatusKey} statusOnly + shouldShowTooltip={false} /> )} /> diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index 09e8983b9196..5dc8cf48ef35 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -459,6 +459,7 @@ function TransactionListItemInner({ !hasCancelled && !isBridgeTx } + showErrorBanner={Boolean(error)} transactionStatus={() => (
`; -exports[`TransactionStatusLabel Component should render PENDING properly 1`] = ` +exports[`TransactionStatusLabel Component renders PENDING properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render QUEUED properly 1`] = ` +exports[`TransactionStatusLabel Component renders QUEUED properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render SIGNING if status is approved 1`] = ` +exports[`TransactionStatusLabel Component renders SIGNING properly and tooltip 1`] = `
`; -exports[`TransactionStatusLabel Component should render UNAPPROVED properly 1`] = ` +exports[`TransactionStatusLabel Component renders UNAPPROVED properly and tooltip 1`] = `
{statusText} + ) : ( +
+ {statusText} +
); } @@ -120,8 +131,13 @@ TransactionStatusLabel.propTypes = { error: PropTypes.object, isEarliestNonce: PropTypes.bool, statusOnly: PropTypes.bool, + shouldShowTooltip: PropTypes.bool, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) custodyStatus: PropTypes.string, custodyStatusDisplayText: PropTypes.string, ///: END:ONLY_INCLUDE_IF }; + +TransactionStatusLabel.defaultProps = { + shouldShowTooltip: true, +}; diff --git a/ui/components/app/transaction-status-label/transaction-status-label.test.js b/ui/components/app/transaction-status-label/transaction-status-label.test.js index 4fa8e832201f..663cb45e802e 100644 --- a/ui/components/app/transaction-status-label/transaction-status-label.test.js +++ b/ui/components/app/transaction-status-label/transaction-status-label.test.js @@ -7,103 +7,160 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods'; import TransactionStatusLabel from '.'; -describe('TransactionStatusLabel Component', () => { - const createMockStore = configureMockStore([thunk]); - const mockState = { - metamask: { - custodyStatusMaps: {}, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - 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', - }, +const TEST_ACCOUNT_ID = 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3'; +const TEST_ACCOUNT_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + +const createBasicAccount = (type = 'HD Key Tree') => ({ + address: TEST_ACCOUNT_ADDRESS, + id: TEST_ACCOUNT_ID, + metadata: { + name: 'Test Account', + keyring: { + type, }, - }; + }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, +}); - let store = createMockStore(mockState); - it('should render CONFIRMED properly', () => { - const confirmedProps = { - status: 'confirmed', - date: 'June 1', - }; +const createCustodyAccount = () => ({ + ...createBasicAccount('Custody - JSONRPC'), + metadata: { + name: 'Account 1', + keyring: { + type: 'Custody - JSONRPC', + }, + }, +}); - const { container } = renderWithProvider( - , - store, - ); +const createMockStateWithAccount = (account) => ({ + metamask: { + custodyStatusMaps: {}, + internalAccounts: { + accounts: { + [TEST_ACCOUNT_ID]: account, + }, + selectedAccount: TEST_ACCOUNT_ID, + }, + }, +}); - expect(container).toMatchSnapshot(); - }); +const createCustodyMockState = (account) => ({ + metamask: { + custodyStatusMaps: { + saturn: { + approved: { + shortText: 'Short Text Test', + longText: 'Long Text Test', + }, + }, + }, + internalAccounts: { + accounts: { + [TEST_ACCOUNT_ID]: account, + }, + selectedAccount: TEST_ACCOUNT_ID, + }, + keyrings: [ + { + type: 'Custody - JSONRPC', + accounts: [TEST_ACCOUNT_ADDRESS], + }, + ], + }, +}); - it('should render PENDING properly', () => { - const props = { +const statusTestCases = [ + { + name: 'CONFIRMED', + props: { status: 'confirmed', date: 'June 1' }, + }, + { + name: 'PENDING', + props: { date: 'June 1', status: TransactionStatus.submitted, isEarliestNonce: true, - }; - - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - }); - - it('should render QUEUED properly', () => { - const props = { + }, + }, + { + name: 'QUEUED', + props: { status: TransactionStatus.submitted, isEarliestNonce: false, - }; - - const { container } = renderWithProvider( - , - store, - ); - - expect(container).toMatchSnapshot(); - }); - - it('should render UNAPPROVED properly', () => { - const props = { + }, + }, + { + name: 'UNAPPROVED', + props: { status: TransactionStatus.unapproved, - }; + }, + }, + { + name: 'SIGNING', + props: { + status: TransactionStatus.approved, + }, + }, +]; - const { container } = renderWithProvider( - , - store, - ); +const errorTestCases = [ + { + name: 'error message', + props: { + status: 'approved', + custodyStatus: 'approved', + error: { message: 'An error occurred' }, + }, + expectedText: 'Error', + }, + { + name: 'error with aborted custody status', + props: { + status: 'approved', + custodyStatus: 'aborted', + error: { message: 'An error occurred' }, + custodyStatusDisplayText: 'Test', + shouldShowTooltip: true, + }, + expectedText: 'Test', + }, +]; + +describe('TransactionStatusLabel Component', () => { + const createMockStore = configureMockStore([thunk]); + let store; - expect(container).toMatchSnapshot(); + beforeEach(() => { + const basicAccount = createBasicAccount(); + const mockState = createMockStateWithAccount(basicAccount); + store = createMockStore(mockState); }); - it('should render SIGNING if status is approved', () => { - const props = { - status: TransactionStatus.approved, - }; + statusTestCases.forEach(({ name, props }) => { + it(`renders ${name} properly and tooltip`, () => { + const { container, queryByTestId } = renderWithProvider( + , + store, + ); + expect(container).toMatchSnapshot(); + expect(queryByTestId('transaction-status-label')).not.toBeInTheDocument(); + }); + }); - const { container } = renderWithProvider( - , + it('renders pure text for status when shouldShowTooltip is specified as false', () => { + const { queryByTestId } = renderWithProvider( + , store, ); - - expect(container).toMatchSnapshot(); + expect(queryByTestId('transaction-status-label')).toBeInTheDocument(); }); - it('should render statusText properly when is custodyStatusDisplayText is defined', () => { + it('renders statusText properly when is custodyStatusDisplayText is defined', () => { const props = { custodyStatusDisplayText: 'test', }; @@ -116,51 +173,15 @@ describe('TransactionStatusLabel Component', () => { expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); }); - it('should display the correct status text and tooltip', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; + it('displays correct text and tooltip', () => { const props = { status: 'approved', custodyStatus: 'approved', custodyStatusDisplayText: 'Test', }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; + const custodyAccount = createCustodyAccount(); + const customMockStore = createCustodyMockState(custodyAccount); store = createMockStore(customMockStore); const { getByText } = renderWithProvider( @@ -170,114 +191,19 @@ describe('TransactionStatusLabel Component', () => { expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); }); - it('should display the error message when there is an error', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; - const props = { - status: 'approved', - custodyStatus: 'approved', - error: { message: 'An error occurred' }, - }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; - store = createMockStore(customMockStore); - - const { getByText } = renderWithProvider( - , - store, - ); - - expect(getByText('Error')).toBeVisible(); - }); - - it('should display correctly the error message when there is an error and custodyStatus is aborted', () => { - const mockShortText = 'Short Text Test'; - const mockLongText = 'Long Text Test'; - const props = { - status: 'approved', - custodyStatus: 'aborted', - error: { message: 'An error occurred' }, - custodyStatusDisplayText: 'Test', - }; - const customMockStore = { - metamask: { - custodyStatusMaps: { - saturn: { - approved: { - shortText: mockShortText, - longText: mockLongText, - }, - }, - }, - internalAccounts: { - accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'Custody - JSONRPC', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - }, - keyrings: [ - { - type: 'Custody - JSONRPC', - accounts: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'], - }, - ], - }, - }; + errorTestCases.forEach(({ name, props, expectedText }) => { + it(`displays correctly the ${name}`, () => { + const custodyAccount = createCustodyAccount(); + const customMockStore = createCustodyMockState(custodyAccount); + store = createMockStore(customMockStore); - store = createMockStore(customMockStore); + const { getByText } = renderWithProvider( + , + store, + ); - const { getByText } = renderWithProvider( - , - store, - ); - - expect(getByText(props.custodyStatusDisplayText)).toBeVisible(); + expect(getByText(expectedText)).toBeVisible(); + }); }); }); From 695d0db025fff9d9b29d6ca2c03c42bfd58cf57e Mon Sep 17 00:00:00 2001 From: AugmentedMode <31675118+AugmentedMode@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:15:55 -0500 Subject: [PATCH 20/71] fix: Add main frame URL property to req object whenever req is triggered from an iframe (#29337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** See the attached issue in metamask planning for more details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29337?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to `https://develop.d3bkcslj57l47p.amplifyapp.com/` 2. Click on Proceed anyways (This phishing warning page here is expected) 3. Open the network tab to monitor network requests 4. Connect your wallet and click on a signature or transaction 5. Verify that mainFrameOrigin is included in the payload of the network request to the security alerts API Screenshot 2024-12-20 at 10 46 05 AM ## **Screenshots/Recordings** Below are screenshots demonstrating the behavior of a test HTML page I created: 1. In the first screenshot, before the iframe is loaded, the console shows only the origin of the main frame. 2. In the second screenshot, after clicking the button to load an iframe pointing to example.com, the solution correctly identifies both the mainFrameOrigin (main frame) and the origin (iframe). Screenshot 2024-12-18 at 10 24 48 PM Screenshot 2024-12-18 at 10 24 54 PM ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../lib/createMainFrameOriginMiddleware.ts | 24 +++++++++++++++++++ app/scripts/metamask-controller.js | 22 ++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/scripts/lib/createMainFrameOriginMiddleware.ts diff --git a/app/scripts/lib/createMainFrameOriginMiddleware.ts b/app/scripts/lib/createMainFrameOriginMiddleware.ts new file mode 100644 index 000000000000..bcbc2cb7d6fd --- /dev/null +++ b/app/scripts/lib/createMainFrameOriginMiddleware.ts @@ -0,0 +1,24 @@ +// Request and responses are currently untyped. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Returns a middleware that appends the mainFrameOrigin to request + * + * @param {{ mainFrameOrigin: string }} opts - The middleware options + * @returns {Function} + */ + +export default function createMainFrameOriginMiddleware({ + mainFrameOrigin, +}: { + mainFrameOrigin: string; +}) { + return function mainFrameOriginMiddleware( + req: any, + _res: any, + next: () => void, + ) { + req.mainFrameOrigin = mainFrameOrigin; + next(); + }; +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 933522c449f6..62fe3c942589 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -305,6 +305,7 @@ import { createUnsupportedMethodMiddleware, } from './lib/rpc-method-middleware'; import createOriginMiddleware from './lib/createOriginMiddleware'; +import createMainFrameOriginMiddleware from './lib/createMainFrameOriginMiddleware'; import createTabIdMiddleware from './lib/createTabIdMiddleware'; import { NetworkOrderController } from './controllers/network-order'; import { AccountOrderController } from './controllers/account-order'; @@ -5804,11 +5805,18 @@ export default class MetamaskController extends EventEmitter { tabId = sender.tab.id; } + let mainFrameOrigin = origin; + if (sender.tab && sender.tab.url) { + // If sender origin is an iframe, then get the top-level frame's origin + mainFrameOrigin = new URL(sender.tab.url).origin; + } + const engine = this.setupProviderEngineEip1193({ origin, sender, subjectType, tabId, + mainFrameOrigin, }); const dupeReqFilterStream = createDupeReqFilterStream(); @@ -5929,13 +5937,25 @@ export default class MetamaskController extends EventEmitter { * @param {MessageSender | SnapSender} options.sender - The sender object. * @param {string} options.subjectType - The type of the sender subject. * @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab + * @param {mainFrameOrigin} [options.mainFrameOrigin] - The origin of the main frame if the sender is an iframe */ - setupProviderEngineEip1193({ origin, subjectType, sender, tabId }) { + setupProviderEngineEip1193({ + origin, + subjectType, + sender, + tabId, + mainFrameOrigin, + }) { const engine = new JsonRpcEngine(); // Append origin to each request engine.push(createOriginMiddleware({ origin })); + // Append mainFrameOrigin to each request if present + if (mainFrameOrigin) { + engine.push(createMainFrameOriginMiddleware({ mainFrameOrigin })); + } + // Append selectedNetworkClientId to each request engine.push(createSelectedNetworkMiddleware(this.controllerMessenger)); From 547b264a3993aa4d40caad5d2993df2a1c7ca32e Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 20 Dec 2024 17:46:37 +0000 Subject: [PATCH 21/71] chore: Update to the latest transaction controller (#29395) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates from v42 to v42.1 in order to get the validation of the gas limit hexadecimal string properties. See https://github.com/MetaMask/core/pull/5093 for more details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29395?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3826 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- lavamoat/browserify/beta/policy.json | 17 ++++++++++++++++- lavamoat/browserify/flask/policy.json | 17 ++++++++++++++++- lavamoat/browserify/main/policy.json | 17 ++++++++++++++++- lavamoat/browserify/mmi/policy.json | 17 ++++++++++++++++- package.json | 2 +- yarn.lock | 24 ++++++++++++------------ 6 files changed, 77 insertions(+), 17 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index ee84b5c3e0e8..e55d57f5ec0f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1835,7 +1835,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2256,6 +2256,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 831feb96e1c5..5658498ad3a7 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1927,7 +1927,7 @@ "@metamask/network-controller": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/rpc-errors": true, - "@metamask/utils": true, + "@metamask/transaction-controller>@metamask/utils": true, "@metamask/name-controller>async-mutex": true, "bn.js": true, "browserify>buffer": true, @@ -2348,6 +2348,21 @@ "semver": true } }, + "@metamask/transaction-controller>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@ngraveio/bc-ur": { "packages": { "@ngraveio/bc-ur>@keystonehq/alias-sampling": true, diff --git a/package.json b/package.json index 4a7632d2d2d3..da93c6c75761 100644 --- a/package.json +++ b/package.json @@ -349,7 +349,7 @@ "@metamask/snaps-sdk": "^6.14.0", "@metamask/snaps-utils": "^8.7.0", "@metamask/solana-wallet-snap": "^1.0.4", - "@metamask/transaction-controller": "^42.0.0", + "@metamask/transaction-controller": "^42.1.0", "@metamask/user-operation-controller": "^21.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", diff --git a/yarn.lock b/yarn.lock index e00947e58386..f45eb5cf233e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5095,13 +5095,13 @@ __metadata: languageName: node linkType: hard -"@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" +"@metamask/base-controller@npm:^7.0.0, @metamask/base-controller@npm:^7.0.1, @metamask/base-controller@npm:^7.0.2, @metamask/base-controller@npm:^7.1.0": + version: 7.1.0 + resolution: "@metamask/base-controller@npm:7.1.0" dependencies: "@metamask/utils": "npm:^10.0.0" immer: "npm:^9.0.6" - checksum: 10/6f78ec5af840c9947aa8eac6e402df6469600260d613a92196daefd5b072097a176fe5da1c386f2d36853513254b74140d667d817a12880c46f088e18ff3606a + checksum: 10/5a0b50c1e096cbf6483e308eddb3ca2e5e1865b803b5dba778bf635ec59657290895e21ada71c7508d8e34ff9695a192a414fd75e287d290346359ef8e23960a languageName: node linkType: hard @@ -6446,9 +6446,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^42.0.0": - version: 42.0.0 - resolution: "@metamask/transaction-controller@npm:42.0.0" +"@metamask/transaction-controller@npm:^42.1.0": + version: 42.1.0 + resolution: "@metamask/transaction-controller@npm:42.1.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6456,13 +6456,13 @@ __metadata: "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/base-controller": "npm:^7.0.2" + "@metamask/base-controller": "npm:^7.1.0" "@metamask/controller-utils": "npm:^11.4.4" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" - "@metamask/rpc-errors": "npm:^7.0.1" - "@metamask/utils": "npm:^10.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.0.1" async-mutex: "npm:^0.5.0" bn.js: "npm:^5.2.1" eth-method-registry: "npm:^4.0.0" @@ -6476,7 +6476,7 @@ __metadata: "@metamask/eth-block-tracker": ">=9" "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/73c510803a720b4c1da0b82f1279a404a9b11c4ab76f8e5e4378c65d5d08bbb32c52062abfe319476cc3f5e2623a8987775c4524e55aa94002af73d73721b869 + checksum: 10/9f842e2b68e84cbffdda301a0e15faab08226fd8e22eb954690ed41df60fe92c24acffdd9186b4c9f1da911a368cbe22cdb9ee046fc02d079c53f76100c66755 languageName: node linkType: hard @@ -26683,7 +26683,7 @@ __metadata: "@metamask/solana-wallet-snap": "npm:^1.0.4" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" - "@metamask/transaction-controller": "npm:^42.0.0" + "@metamask/transaction-controller": "npm:^42.1.0" "@metamask/user-operation-controller": "npm:^21.0.0" "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" From a02799d97397fdfda492341566851203ae04027d Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 14:49:06 -0330 Subject: [PATCH 22/71] ci: Migrate dependency linting (#29370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate dependency/lockfile linting steps from CircleCI to GitHub actions. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29370?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** Review logs to ensure the same commands are run. Introduce errors on a branch from here to ensure the problems are caught. https://github.com/MetaMask/metamask-extension/pull/29391 ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 43 ------------------------ .github/workflows/main.yml | 14 ++++++++ .github/workflows/test-deps-audit.yml | 18 ++++++++++ .github/workflows/test-deps-depcheck.yml | 18 ++++++++++ .github/workflows/test-yarn-dedupe.yml | 18 ++++++++++ 5 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/test-deps-audit.yml create mode 100644 .github/workflows/test-deps-depcheck.yml create mode 100644 .github/workflows/test-yarn-dedupe.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 552aa3305509..5598e6450cfe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,15 +127,6 @@ workflows: - master requires: - prep-deps - - test-deps-audit: - requires: - - prep-deps - - test-deps-depcheck: - requires: - - prep-deps - - test-yarn-dedupe: - requires: - - prep-deps - validate-lavamoat-allow-scripts: requires: - prep-deps @@ -291,7 +282,6 @@ workflows: - prep-build-flask-mv2 - all-tests-pass: requires: - - test-deps-depcheck - validate-lavamoat-allow-scripts - validate-lavamoat-policy-build - validate-lavamoat-policy-webapp @@ -964,17 +954,6 @@ jobs: name: Rerun workflows from failed command: yarn ci-rerun-from-failed - test-yarn-dedupe: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Detect yarn lock deduplications - command: yarn dedupe --check - test-lint: executor: node-browsers-medium steps: @@ -1053,28 +1032,6 @@ jobs: name: Validate release candidate changelog command: .circleci/scripts/validate-changelog-in-rc.sh - test-deps-audit: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: yarn audit - command: yarn audit - - test-deps-depcheck: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: depcheck - command: yarn depcheck - test-e2e-chrome-webpack: executor: node-browsers-medium-plus parallelism: 20 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7907455701d..d8850502e7da 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,18 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + test-deps-audit: + name: Test deps audit + uses: ./.github/workflows/test-deps-audit.yml + + test-yarn-dedupe: + name: Test yarn dedupe + uses: ./.github/workflows/test-yarn-dedupe.yml + + test-deps-depcheck: + name: Test deps depcheck + uses: ./.github/workflows/test-deps-depcheck.yml + run-tests: name: Run tests uses: ./.github/workflows/run-tests.yml @@ -41,6 +53,8 @@ jobs: runs-on: ubuntu-latest needs: - check-workflows + - test-yarn-dedupe + - test-deps-depcheck - run-tests - wait-for-circleci-workflow-status outputs: diff --git a/.github/workflows/test-deps-audit.yml b/.github/workflows/test-deps-audit.yml new file mode 100644 index 000000000000..271746da2429 --- /dev/null +++ b/.github/workflows/test-deps-audit.yml @@ -0,0 +1,18 @@ +name: Test deps audit + +on: + workflow_call: + +jobs: + test-deps-audit: + name: Test deps audit + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Run audit + run: yarn audit diff --git a/.github/workflows/test-deps-depcheck.yml b/.github/workflows/test-deps-depcheck.yml new file mode 100644 index 000000000000..3860c485f25b --- /dev/null +++ b/.github/workflows/test-deps-depcheck.yml @@ -0,0 +1,18 @@ +name: Test deps depcheck + +on: + workflow_call: + +jobs: + test-deps-depcheck: + name: Test deps depcheck + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Run depcheck + run: yarn depcheck diff --git a/.github/workflows/test-yarn-dedupe.yml b/.github/workflows/test-yarn-dedupe.yml new file mode 100644 index 000000000000..40bda1dfb3d2 --- /dev/null +++ b/.github/workflows/test-yarn-dedupe.yml @@ -0,0 +1,18 @@ +name: Test yarn dedupe + +on: + workflow_call: + +jobs: + test-yarn-dedupe: + name: Test yarn dedupe + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Detect yarn lock deduplications + run: yarn dedupe --check From 6f11eda56785b6ca1bd253a6fc6a3498eef5bc5f Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 16:13:27 -0330 Subject: [PATCH 23/71] ci: Migrate lint CI steps (#29371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate lint steps from CircleCI to GitHub Actions. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29371?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** Branch from here, create a new draft PR, Introduce lint errors, then ensure the jobs fail. https://github.com/MetaMask/metamask-extension/pull/29390 ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 82 ---------------------- .github/workflows/main.yml | 20 ++++++ .github/workflows/test-lint-changelog.yml | 23 ++++++ .github/workflows/test-lint-lockfile.yml | 21 ++++++ .github/workflows/test-lint-shellcheck.yml | 15 ++++ .github/workflows/test-lint.yml | 21 ++++++ 6 files changed, 100 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/test-lint-changelog.yml create mode 100644 .github/workflows/test-lint-lockfile.yml create mode 100644 .github/workflows/test-lint-shellcheck.yml create mode 100644 .github/workflows/test-lint.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 5598e6450cfe..60bb80eaf449 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,10 +25,6 @@ executors: resource_class: medium+ environment: NODE_OPTIONS: --max_old_space_size=4096 - shellcheck: - docker: - - image: koalaman/shellcheck-alpine@sha256:dfaf08fab58c158549d3be64fb101c626abc5f16f341b569092577ae207db199 - resource_class: small playwright: docker: - image: mcr.microsoft.com/playwright:v1.44.1-focal @@ -184,16 +180,6 @@ workflows: - prep-build-ts-migration-dashboard: requires: - prep-deps - - test-lint: - requires: - - prep-deps - - test-lint-shellcheck - - test-lint-lockfile: - requires: - - prep-deps - - test-lint-changelog: - requires: - - prep-deps - test-e2e-chrome-webpack: <<: *main_master_rc_only requires: @@ -285,10 +271,6 @@ workflows: - validate-lavamoat-allow-scripts - validate-lavamoat-policy-build - validate-lavamoat-policy-webapp - - test-lint - - test-lint-shellcheck - - test-lint-lockfile - - test-lint-changelog - validate-source-maps - validate-source-maps-beta - validate-source-maps-flask @@ -954,20 +936,6 @@ jobs: name: Rerun workflows from failed command: yarn ci-rerun-from-failed - test-lint: - executor: node-browsers-medium - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Lint - command: yarn lint - - run: - name: Verify locales - command: yarn verify-locales --quiet - test-storybook: executor: node-browsers-medium-plus steps: @@ -982,56 +950,6 @@ jobs: name: Test Storybook command: yarn test-storybook:ci - test-lint-shellcheck: - executor: shellcheck - steps: - - checkout - - run: apk add --no-cache bash jq yarn - - run: - name: ShellCheck Lint - command: ./development/shellcheck.sh - - test-lint-lockfile: - executor: node-browsers-medium-plus - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: lockfile-lint - command: yarn lint:lockfile - - run: - name: check yarn resolutions - command: yarn --check-resolutions - - test-lint-changelog: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - when: - condition: - not: - matches: - pattern: /^Version-v(\d+)[.](\d+)[.](\d+)$/ - value: << pipeline.git.branch >> - steps: - - run: - name: Validate changelog - command: yarn lint:changelog - - when: - condition: - matches: - pattern: /^Version-v(\d+)[.](\d+)[.](\d+)$/ - value: << pipeline.git.branch >> - steps: - - run: - name: Validate release candidate changelog - command: .circleci/scripts/validate-changelog-in-rc.sh - test-e2e-chrome-webpack: executor: node-browsers-medium-plus parallelism: 20 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8850502e7da..ee29c54e94ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,22 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + test-lint-shellcheck: + name: Test lint shellcheck + uses: ./.github/workflows/test-lint-shellcheck.yml + + test-lint: + name: Test lint + uses: ./.github/workflows/test-lint.yml + + test-lint-changelog: + name: Test lint changelog + uses: ./.github/workflows/test-lint-changelog.yml + + test-lint-lockfile: + name: Test lint lockfile + uses: ./.github/workflows/test-lint-lockfile.yml + test-deps-audit: name: Test deps audit uses: ./.github/workflows/test-deps-audit.yml @@ -53,6 +69,10 @@ jobs: runs-on: ubuntu-latest needs: - check-workflows + - test-lint-shellcheck + - test-lint + - test-lint-changelog + - test-lint-lockfile - test-yarn-dedupe - test-deps-depcheck - run-tests diff --git a/.github/workflows/test-lint-changelog.yml b/.github/workflows/test-lint-changelog.yml new file mode 100644 index 000000000000..66c0219551f4 --- /dev/null +++ b/.github/workflows/test-lint-changelog.yml @@ -0,0 +1,23 @@ +name: Test lint changelog + +on: + workflow_call: + +jobs: + test-lint-changelog: + name: Test lint changelog + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Validate changelog + if: ${{ !startsWith(github.head_ref || github.ref_name, 'Version-v') }} + run: yarn lint:changelog + + - name: Validate release candidate changelog + if: ${{ startsWith(github.head_ref || github.ref_name, 'Version-v') }} + run: .circleci/scripts/validate-changelog-in-rc.sh diff --git a/.github/workflows/test-lint-lockfile.yml b/.github/workflows/test-lint-lockfile.yml new file mode 100644 index 000000000000..cc84318624ce --- /dev/null +++ b/.github/workflows/test-lint-lockfile.yml @@ -0,0 +1,21 @@ +name: Test lint lockfile + +on: + workflow_call: + +jobs: + test-lint-lockfile: + name: Test lint lockfile + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Lint lockfile + run: yarn lint:lockfile + + - name: Check yarn resolutions + run: yarn --check-resolutions diff --git a/.github/workflows/test-lint-shellcheck.yml b/.github/workflows/test-lint-shellcheck.yml new file mode 100644 index 000000000000..c4127902a2f4 --- /dev/null +++ b/.github/workflows/test-lint-shellcheck.yml @@ -0,0 +1,15 @@ +name: Test lint shellcheck + +on: + workflow_call: + +jobs: + test-lint-shellcheck: + name: Test lint shellcheck + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: ShellCheck Lint + run: ./development/shellcheck.sh diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml new file mode 100644 index 000000000000..df40a3a7ef27 --- /dev/null +++ b/.github/workflows/test-lint.yml @@ -0,0 +1,21 @@ +name: Test lint + +on: + workflow_call: + +jobs: + test-lint: + name: Test lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Lint + run: yarn lint + + - name: Verify locales + run: yarn verify-locales --quiet From ce8b502529b1131c65506d23a6ec7b5f68c5b526 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 17:23:21 -0330 Subject: [PATCH 24/71] ci: Migrate LavaMoat validation to GitHub Actions (#29369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate LavaMoat policy validation from CircleCI to GitHub actions. No functional changes. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29369?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** * Checkout this branch (`migrate-lavamoat-validation`), then from there create a new branch to test with * On this new branch, make a dependency change with a policy impact (e.g. add or remove a package, upgrade something, etc.), but make sure the build still passes (validation requires a passing build) * Create a draft PR, and verify that the policy validation fails * Use the `metamaskbot update-policies` bot command to update the policies, then verify the validation now succeeds. PR with errors - https://github.com/MetaMask/metamask-extension/pull/29396 Failure - https://github.com/MetaMask/metamask-extension/actions/runs/12434996100/job/34719873040?pr=29396 Passing - https://github.com/MetaMask/metamask-extension/actions/runs/12435253146/job/34720674397?pr=29396 ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 60 ------------------- .github/workflows/main.yml | 15 +++++ .../validate-lavamoat-allow-scripts.yml | 25 ++++++++ .../validate-lavamoat-policy-build.yml | 27 +++++++++ .../validate-lavamoat-policy-webapp.yml | 30 ++++++++++ 5 files changed, 97 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/validate-lavamoat-allow-scripts.yml create mode 100644 .github/workflows/validate-lavamoat-policy-build.yml create mode 100644 .github/workflows/validate-lavamoat-policy-webapp.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 60bb80eaf449..f800ab484ebe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -123,18 +123,6 @@ workflows: - master requires: - prep-deps - - validate-lavamoat-allow-scripts: - requires: - - prep-deps - - validate-lavamoat-policy-build: - requires: - - prep-deps - - validate-lavamoat-policy-webapp: - matrix: - parameters: - build-type: [main, beta, flask, mmi] - requires: - - prep-deps - prep-build-mmi: requires: - prep-deps @@ -268,9 +256,6 @@ workflows: - prep-build-flask-mv2 - all-tests-pass: requires: - - validate-lavamoat-allow-scripts - - validate-lavamoat-policy-build - - validate-lavamoat-policy-webapp - validate-source-maps - validate-source-maps-beta - validate-source-maps-flask @@ -481,51 +466,6 @@ jobs: at: . - run: yarn tsx .circleci/scripts/validate-locales-only.ts - validate-lavamoat-allow-scripts: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Validate allow-scripts config - command: yarn allow-scripts auto - - run: - name: Check working tree - command: .circleci/scripts/check-working-tree.sh - - validate-lavamoat-policy-build: - executor: node-browsers-medium - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Validate LavaMoat build policy - command: yarn lavamoat:build:auto - - run: - name: Check working tree - command: .circleci/scripts/check-working-tree.sh - - validate-lavamoat-policy-webapp: - executor: node-browsers-medium-plus - parameters: - build-type: - type: string - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Validate LavaMoat << parameters.build-type >> policy - command: yarn lavamoat:webapp:auto:ci '--build-types=<< parameters.build-type >>' - - run: - name: Check working tree - command: .circleci/scripts/check-working-tree.sh - prep-build: executor: node-linux-medium steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee29c54e94ac..6e5a5121f336 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,18 @@ jobs: name: Test deps depcheck uses: ./.github/workflows/test-deps-depcheck.yml + validate-lavamoat-allow-scripts: + name: Validate lavamoat allow scripts + uses: ./.github/workflows/validate-lavamoat-allow-scripts.yml + + validate-lavamoat-policy-build: + name: Validate lavamoat policy build + uses: ./.github/workflows/validate-lavamoat-policy-build.yml + + validate-lavamoat-policy-webapp: + name: Validate lavamoat policy webapp + uses: ./.github/workflows/validate-lavamoat-policy-webapp.yml + run-tests: name: Run tests uses: ./.github/workflows/run-tests.yml @@ -75,6 +87,9 @@ jobs: - test-lint-lockfile - test-yarn-dedupe - test-deps-depcheck + - validate-lavamoat-allow-scripts + - validate-lavamoat-policy-build + - validate-lavamoat-policy-webapp - run-tests - wait-for-circleci-workflow-status outputs: diff --git a/.github/workflows/validate-lavamoat-allow-scripts.yml b/.github/workflows/validate-lavamoat-allow-scripts.yml new file mode 100644 index 000000000000..637a2d9aeb54 --- /dev/null +++ b/.github/workflows/validate-lavamoat-allow-scripts.yml @@ -0,0 +1,25 @@ +name: Validate lavamoat allow scripts + +on: + workflow_call: + +jobs: + validate-lavamoat-allow-scripts: + name: Validate lavamoat allow scripts + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Validate allow-scripts config + run: yarn allow-scripts auto + + - name: Check working tree + run: | + if ! git diff --exit-code; then + echo "::error::Working tree dirty." + exit 1 + fi diff --git a/.github/workflows/validate-lavamoat-policy-build.yml b/.github/workflows/validate-lavamoat-policy-build.yml new file mode 100644 index 000000000000..4524cc26a546 --- /dev/null +++ b/.github/workflows/validate-lavamoat-policy-build.yml @@ -0,0 +1,27 @@ +name: Validate lavamoat policy build + +on: + workflow_call: + +jobs: + validate-lavamoat-policy-build: + name: Validate lavamoat policy build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Validate lavamoat build policy + run: yarn lavamoat:build:auto + env: + INFURA_PROJECT_ID: 00000000000 + + - name: Check working tree + run: | + if ! git diff --exit-code; then + echo "::error::Working tree dirty." + exit 1 + fi diff --git a/.github/workflows/validate-lavamoat-policy-webapp.yml b/.github/workflows/validate-lavamoat-policy-webapp.yml new file mode 100644 index 000000000000..37ff9ede00fc --- /dev/null +++ b/.github/workflows/validate-lavamoat-policy-webapp.yml @@ -0,0 +1,30 @@ +name: Validate lavamoat policy webapp + +on: + workflow_call: + +jobs: + validate-lavamoat-policy-webapp: + name: Validate lavamoat policy webapp + runs-on: ubuntu-latest + strategy: + matrix: + build-type: [main, beta, flask, mmi] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Validate lavamoat ${{ matrix.build-type }} policy + run: yarn lavamoat:webapp:auto:ci --build-types=${{ matrix.build-type }} + env: + INFURA_PROJECT_ID: 00000000000 + + - name: Check working tree + run: | + if ! git diff --exit-code; then + echo "::error::Working tree dirty." + exit 1 + fi From f64a2d003e6e69be0305c43534334461fadc6e7e Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 20 Dec 2024 20:55:52 +0000 Subject: [PATCH 25/71] fix: hide first interaction alert if token transfer recipient is internal account (#29389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Hide the first-time interaction alert if the transaction is a token transfer, and the recipient is an internal account. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29389?quickstart=1) ## **Related issues** Fixes: #29225 ## **Manual testing steps** Create token transfer to internal account and verify no alert is displayed. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../info/hooks/useTransferRecipient.test.ts | 74 +++++++++++++++++++ .../info/hooks/useTransferRecipient.ts | 20 +++++ .../transaction-flow-section.tsx | 15 +--- .../useFirstTimeInteractionAlert.test.ts | 67 ++++++++++------- .../useFirstTimeInteractionAlert.ts | 6 +- 5 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.test.ts create mode 100644 ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.ts diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.test.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.test.ts new file mode 100644 index 000000000000..26d007ba148b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.test.ts @@ -0,0 +1,74 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { renderHookWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; +import { getMockConfirmStateForTransaction } from '../../../../../../../test/data/confirmations/helper'; +import { genUnapprovedContractInteractionConfirmation } from '../../../../../../../test/data/confirmations/contract-interaction'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../../test/data/confirmations/token-transfer'; +import { useTransferRecipient } from './useTransferRecipient'; + +const ADDRESS_MOCK = '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09B'; +const ADDRESS_2_MOCK = '0x2e0D7E8c45221FcA00d74a3609A0f7097035d09C'; + +const TRANSACTION_METADATA_MOCK = + genUnapprovedContractInteractionConfirmation() as TransactionMeta; + +function runHook(transaction?: TransactionMeta) { + const state = transaction + ? getMockConfirmStateForTransaction(transaction) + : {}; + + const { result } = renderHookWithConfirmContextProvider( + useTransferRecipient, + state, + ); + + return result.current as string | undefined; +} + +describe('useTransferRecipient', () => { + it('returns undefined if no transaction', () => { + expect(runHook()).toBeUndefined(); + }); + + it('returns parameter to address if simple send', () => { + expect( + runHook({ + ...TRANSACTION_METADATA_MOCK, + type: TransactionType.simpleSend, + txParams: { + ...TRANSACTION_METADATA_MOCK.txParams, + to: ADDRESS_MOCK, + }, + }), + ).toBe(ADDRESS_MOCK); + }); + + it('returns data to address if token data', () => { + expect( + runHook({ + ...TRANSACTION_METADATA_MOCK, + txParams: { + ...TRANSACTION_METADATA_MOCK.txParams, + to: ADDRESS_2_MOCK, + data: genUnapprovedTokenTransferConfirmation().txParams.data, + }, + }), + ).toBe(ADDRESS_MOCK); + }); + + it('returns parameter to address if token data but type is simple send', () => { + expect( + runHook({ + ...TRANSACTION_METADATA_MOCK, + type: TransactionType.simpleSend, + txParams: { + ...TRANSACTION_METADATA_MOCK.txParams, + to: ADDRESS_2_MOCK, + data: genUnapprovedTokenTransferConfirmation().txParams.data, + }, + }), + ).toBe(ADDRESS_2_MOCK); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.ts b/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.ts new file mode 100644 index 000000000000..fcfd98fedaf4 --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/hooks/useTransferRecipient.ts @@ -0,0 +1,20 @@ +import { + TransactionMeta, + TransactionType, +} from '@metamask/transaction-controller'; +import { useConfirmContext } from '../../../../context/confirm'; +import { useTokenTransactionData } from './useTokenTransactionData'; + +export function useTransferRecipient() { + const { currentConfirmation: transactionMetadata } = + useConfirmContext(); + + const transactionData = useTokenTransactionData(); + const transactionType = transactionMetadata?.type; + const transactionTo = transactionMetadata?.txParams?.to; + const transferTo = transactionData?.args?._to as string | undefined; + + return transactionType === TransactionType.simpleSend + ? transactionTo + : transferTo; +} 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 index fe9b9f319c9f..5ed2b103b809 100644 --- 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 @@ -1,7 +1,4 @@ -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; import React from 'react'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; import { @@ -22,7 +19,7 @@ import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/in import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; import { useConfirmContext } from '../../../../context/confirm'; -import { useTokenTransactionData } from '../hooks/useTokenTransactionData'; +import { useTransferRecipient } from '../hooks/useTransferRecipient'; export const TransactionFlowSection = () => { const t = useI18nContext(); @@ -30,13 +27,7 @@ export const TransactionFlowSection = () => { const { currentConfirmation: transactionMeta } = useConfirmContext(); - const parsedTransactionData = useTokenTransactionData(); - - const recipientAddress = - transactionMeta.type === TransactionType.simpleSend - ? transactionMeta.txParams.to - : parsedTransactionData?.args?._to; - + const recipientAddress = useTransferRecipient(); const { chainId } = transactionMeta; return ( diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.test.ts b/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.test.ts index 93da09a9674e..7fe4ceb1d2e2 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.test.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.test.ts @@ -1,17 +1,19 @@ -import { ApprovalType } from '@metamask/controller-utils'; import { TransactionMeta, TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; -import { getMockConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { getMockConfirmStateForTransaction } from '../../../../../../test/data/confirmations/helper'; import { renderHookWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { Severity } from '../../../../../helpers/constants/design-system'; import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; +import { genUnapprovedTokenTransferConfirmation } from '../../../../../../test/data/confirmations/token-transfer'; import { useFirstTimeInteractionAlert } from './useFirstTimeInteractionAlert'; -const ACCOUNT_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; +const ACCOUNT_ADDRESS_MOCK = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; +const ACCOUNT_ADDRESS_2_MOCK = '0x2e0d7e8c45221fca00d74a3609a0f7097035d09b'; +const CONTRACT_ADDRESS_MOCK = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7be'; const TRANSACTION_ID_MOCK = '123-456'; const TRANSACTION_META_MOCK = { @@ -21,7 +23,7 @@ const TRANSACTION_META_MOCK = { status: TransactionStatus.unapproved, type: TransactionType.contractInteraction, txParams: { - from: ACCOUNT_ADDRESS, + from: ACCOUNT_ADDRESS_MOCK, }, time: new Date().getTime() - 10000, } as TransactionMeta; @@ -33,28 +35,20 @@ function runHook({ currentConfirmation?: TransactionMeta; internalAccountAddresses?: string[]; } = {}) { - const pendingApprovals = currentConfirmation - ? { - [currentConfirmation.id as string]: { - id: currentConfirmation.id, - type: ApprovalType.Transaction, - }, - } - : {}; - - const transactions = currentConfirmation ? [currentConfirmation] : []; - const internalAccounts = { accounts: internalAccountAddresses?.map((address) => ({ address })) ?? [], }; - const state = getMockConfirmState({ - metamask: { - internalAccounts, - pendingApprovals, - transactions, - }, - }); + const state = currentConfirmation + ? getMockConfirmStateForTransaction( + currentConfirmation as TransactionMeta, + { + metamask: { + internalAccounts, + }, + }, + ) + : {}; const response = renderHookWithConfirmContextProvider( useFirstTimeInteractionAlert, @@ -101,15 +95,35 @@ describe('useFirstTimeInteractionAlert', () => { const firstTimeConfirmation = { ...TRANSACTION_META_MOCK, isFirstTimeInteraction: true, + type: TransactionType.simpleSend, + txParams: { + ...TRANSACTION_META_MOCK.txParams, + to: ACCOUNT_ADDRESS_2_MOCK, + }, + }; + expect( + runHook({ + currentConfirmation: firstTimeConfirmation, + internalAccountAddresses: [ACCOUNT_ADDRESS_2_MOCK], + }), + ).toEqual([]); + }); + + it('returns no alerts if token transfer recipient is internal account', () => { + const firstTimeConfirmation = { + ...TRANSACTION_META_MOCK, + isFirstTimeInteraction: true, + type: TransactionType.tokenMethodTransfer, txParams: { ...TRANSACTION_META_MOCK.txParams, - to: ACCOUNT_ADDRESS, + to: CONTRACT_ADDRESS_MOCK, + data: genUnapprovedTokenTransferConfirmation().txParams.data, }, }; expect( runHook({ currentConfirmation: firstTimeConfirmation, - internalAccountAddresses: [ACCOUNT_ADDRESS], + internalAccountAddresses: [ACCOUNT_ADDRESS_2_MOCK], }), ).toEqual([]); }); @@ -118,15 +132,16 @@ describe('useFirstTimeInteractionAlert', () => { const firstTimeConfirmation = { ...TRANSACTION_META_MOCK, isFirstTimeInteraction: true, + type: TransactionType.simpleSend, txParams: { ...TRANSACTION_META_MOCK.txParams, - to: ACCOUNT_ADDRESS.toLowerCase(), + to: ACCOUNT_ADDRESS_2_MOCK.toLowerCase(), }, }; expect( runHook({ currentConfirmation: firstTimeConfirmation, - internalAccountAddresses: [ACCOUNT_ADDRESS.toUpperCase()], + internalAccountAddresses: [ACCOUNT_ADDRESS_2_MOCK.toUpperCase()], }), ).toEqual([]); }); diff --git a/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.ts b/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.ts index c74552575667..f83f5d1ce30e 100644 --- a/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.ts +++ b/ui/pages/confirmations/hooks/alerts/transactions/useFirstTimeInteractionAlert.ts @@ -8,14 +8,14 @@ import { Severity } from '../../../../../helpers/constants/design-system'; import { RowAlertKey } from '../../../../../components/app/confirm/info/row/constants'; import { useConfirmContext } from '../../../context/confirm'; import { getInternalAccounts } from '../../../../../selectors'; +import { useTransferRecipient } from '../../../components/confirm/info/hooks/useTransferRecipient'; export function useFirstTimeInteractionAlert(): Alert[] { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); const internalAccounts = useSelector(getInternalAccounts); - - const { txParams, isFirstTimeInteraction } = currentConfirmation ?? {}; - const { to } = txParams ?? {}; + const to = useTransferRecipient(); + const { isFirstTimeInteraction } = currentConfirmation ?? {}; const isInternalAccount = internalAccounts.some( (account) => account.address?.toLowerCase() === to?.toLowerCase(), From 57d564d6603f00d7d7a568efd742412810846489 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 20 Dec 2024 19:47:24 -0330 Subject: [PATCH 26/71] chore: Remove broken MV3 perf stats (#29408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove broken MV3 reports. These reports relied upon data from the `mv3-perf-stats` E2E test job, which itself relied upon the `user-data-dir` chromedriver setting removed in #24696. They have been broken since that PR. These reports were very useful in prioritizing MV3 work at the time, but we haven't needed them recently. The `mv3-stats` E2E test suite has also been removed (this is an older version of `mv3-perf-stats` that has been unused for even longer), along with the charts that were used for this report. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29408?quickstart=1) ## **Related issues** Relates to https://github.com/MetaMask/metamask-extension/issues/28572 ## **Manual testing steps** Check that the `metamaskbot` comment no longer has the links to these broken reports. They look like this: - mv3: Background Module Init Stats - mv3: UI Init Stats - mv3: Module Load Stats ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 42 - .prettierignore | 1 - .vscode/cspell.json | 1 - .../charts/flamegraph/chart/index.html | 167 - .../flamegraph/lib/d3-flamegraph-tooltip.js | 3117 --------- .../charts/flamegraph/lib/d3-flamegraph.css | 46 - .../charts/flamegraph/lib/d3-flamegraph.js | 5719 ----------------- development/charts/table/index.html | 67 - development/charts/table/jquery.min.js | 18 - development/metamaskbot-build-announce.js | 9 - package.json | 1 - test/e2e/mv3-perf-stats/bundle-size.js | 140 - test/e2e/mv3-perf-stats/index.js | 2 - test/e2e/mv3-perf-stats/init-load-stats.js | 118 - test/e2e/mv3-stats.js | 115 - 15 files changed, 9563 deletions(-) delete mode 100644 development/charts/flamegraph/chart/index.html delete mode 100644 development/charts/flamegraph/lib/d3-flamegraph-tooltip.js delete mode 100644 development/charts/flamegraph/lib/d3-flamegraph.css delete mode 100644 development/charts/flamegraph/lib/d3-flamegraph.js delete mode 100644 development/charts/table/index.html delete mode 100644 development/charts/table/jquery.min.js delete mode 100755 test/e2e/mv3-perf-stats/bundle-size.js delete mode 100644 test/e2e/mv3-perf-stats/index.js delete mode 100755 test/e2e/mv3-perf-stats/init-load-stats.js delete mode 100755 test/e2e/mv3-stats.js diff --git a/.circleci/config.yml b/.circleci/config.yml index f800ab484ebe..607571e36eb1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -278,9 +278,6 @@ workflows: - user-actions-benchmark: requires: - prep-build-test - - stats-module-load-init: - requires: - - prep-build-test - job-publish-prerelease: requires: - prep-deps @@ -294,7 +291,6 @@ workflows: - prep-build-ts-migration-dashboard - benchmark - user-actions-benchmark - - stats-module-load-init - all-tests-pass - job-publish-release: filters: @@ -1271,44 +1267,6 @@ jobs: paths: - test-artifacts - stats-module-load-init: - executor: node-browsers-small - steps: - - run: *shallow-git-clone-and-enable-vnc - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Move test build to dist - command: mv ./dist-test ./dist - - run: - name: Move test zips to builds - command: mv ./builds-test ./builds - - run: - name: Run page load benchmark - command: | - mkdir -p test-artifacts/chrome/ - cp -R development/charts/flamegraph test-artifacts/chrome/initialisation - cp -R development/charts/flamegraph/chart test-artifacts/chrome/initialisation/background - cp -R development/charts/flamegraph/chart test-artifacts/chrome/initialisation/ui - cp -R development/charts/table test-artifacts/chrome/load_time - - run: - name: Run page load benchmark - command: yarn mv3:stats:chrome --out test-artifacts/chrome - - run: - name: Install jq - command: sudo apt install jq -y - - run: - name: Record bundle size at commit - command: ./.circleci/scripts/bundle-stats-commit.sh - - store_artifacts: - path: test-artifacts - destination: test-artifacts - - persist_to_workspace: - root: . - paths: - - test-artifacts - job-publish-prerelease: executor: node-browsers-medium steps: diff --git a/.prettierignore b/.prettierignore index d8d8cfe4a15c..6f500515e7c6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,7 +6,6 @@ node_modules/**/* /app/vendor/** /builds/**/* /coverage/**/* -/development/charts/** /development/chromereload.js /development/ts-migration-dashboard/filesToConvert.json /development/ts-migration-dashboard/build/** diff --git a/.vscode/cspell.json b/.vscode/cspell.json index f962a85ef3ad..a8c5ea9d864e 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -47,7 +47,6 @@ "devcontainers", "endregion", "ensdomains", - "flamegraph", "FONTCONFIG", "hardfork", "hexstring", diff --git a/development/charts/flamegraph/chart/index.html b/development/charts/flamegraph/chart/index.html deleted file mode 100644 index ce53076ad9e4..000000000000 --- a/development/charts/flamegraph/chart/index.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - Performance Measurements - - - - - -
-
- -

d3-flame-graph

-
-
-
-
-
- - - - - - - - - - - diff --git a/development/charts/flamegraph/lib/d3-flamegraph-tooltip.js b/development/charts/flamegraph/lib/d3-flamegraph-tooltip.js deleted file mode 100644 index cc042a0f281b..000000000000 --- a/development/charts/flamegraph/lib/d3-flamegraph-tooltip.js +++ /dev/null @@ -1,3117 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else if(typeof exports === 'object') - exports["flamegraph"] = factory(); - else - root["flamegraph"] = root["flamegraph"] || {}, root["flamegraph"]["tooltip"] = factory(); -})(self, function() { -return /******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ // The require scope -/******/ var __webpack_require__ = {}; -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/define property getters */ -/******/ (() => { -/******/ // define getter functions for harmony exports -/******/ __webpack_require__.d = (exports, definition) => { -/******/ for(var key in definition) { -/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { -/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); -/******/ } -/******/ } -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/hasOwnProperty shorthand */ -/******/ (() => { -/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) -/******/ })(); -/******/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// ESM COMPAT FLAG -__webpack_require__.r(__webpack_exports__); - -// EXPORTS -__webpack_require__.d(__webpack_exports__, { - "defaultFlamegraphTooltip": () => (/* binding */ defaultFlamegraphTooltip) -}); - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selector.js -function none() {} - -/* harmony default export */ function selector(selector) { - return selector == null ? none : function() { - return this.querySelector(selector); - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/select.js - - - -/* harmony default export */ function selection_select(select) { - if (typeof select !== "function") select = selector(select); - - for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) { - if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) { - if ("__data__" in node) subnode.__data__ = node.__data__; - subgroup[i] = subnode; - } - } - } - - return new Selection(subgroups, this._parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/array.js -// Given something array like (or null), returns something that is strictly an -// array. This is used to ensure that array-like objects passed to d3.selectAll -// or selection.selectAll are converted into proper arrays when creating a -// selection; we don’t ever want to create a selection backed by a live -// HTMLCollection or NodeList. However, note that selection.selectAll will use a -// static NodeList as a group, since it safely derived from querySelectorAll. -function array(x) { - return x == null ? [] : Array.isArray(x) ? x : Array.from(x); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selectorAll.js -function empty() { - return []; -} - -/* harmony default export */ function selectorAll(selector) { - return selector == null ? empty : function() { - return this.querySelectorAll(selector); - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectAll.js - - - - -function arrayAll(select) { - return function() { - return array(select.apply(this, arguments)); - }; -} - -/* harmony default export */ function selectAll(select) { - if (typeof select === "function") select = arrayAll(select); - else select = selectorAll(select); - - for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if (node = group[i]) { - subgroups.push(select.call(node, node.__data__, i, group)); - parents.push(node); - } - } - } - - return new Selection(subgroups, parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/matcher.js -/* harmony default export */ function matcher(selector) { - return function() { - return this.matches(selector); - }; -} - -function childMatcher(selector) { - return function(node) { - return node.matches(selector); - }; -} - - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectChild.js - - -var find = Array.prototype.find; - -function childFind(match) { - return function() { - return find.call(this.children, match); - }; -} - -function childFirst() { - return this.firstElementChild; -} - -/* harmony default export */ function selectChild(match) { - return this.select(match == null ? childFirst - : childFind(typeof match === "function" ? match : childMatcher(match))); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectChildren.js - - -var filter = Array.prototype.filter; - -function children() { - return Array.from(this.children); -} - -function childrenFilter(match) { - return function() { - return filter.call(this.children, match); - }; -} - -/* harmony default export */ function selectChildren(match) { - return this.selectAll(match == null ? children - : childrenFilter(typeof match === "function" ? match : childMatcher(match))); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/filter.js - - - -/* harmony default export */ function selection_filter(match) { - if (typeof match !== "function") match = matcher(match); - - for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) { - if ((node = group[i]) && match.call(node, node.__data__, i, group)) { - subgroup.push(node); - } - } - } - - return new Selection(subgroups, this._parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/sparse.js -/* harmony default export */ function sparse(update) { - return new Array(update.length); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/enter.js - - - -/* harmony default export */ function enter() { - return new Selection(this._enter || this._groups.map(sparse), this._parents); -} - -function EnterNode(parent, datum) { - this.ownerDocument = parent.ownerDocument; - this.namespaceURI = parent.namespaceURI; - this._next = null; - this._parent = parent; - this.__data__ = datum; -} - -EnterNode.prototype = { - constructor: EnterNode, - appendChild: function(child) { return this._parent.insertBefore(child, this._next); }, - insertBefore: function(child, next) { return this._parent.insertBefore(child, next); }, - querySelector: function(selector) { return this._parent.querySelector(selector); }, - querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); } -}; - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/constant.js -/* harmony default export */ function src_constant(x) { - return function() { - return x; - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/data.js - - - - -function bindIndex(parent, group, enter, update, exit, data) { - var i = 0, - node, - groupLength = group.length, - dataLength = data.length; - - // Put any non-null nodes that fit into update. - // Put any null nodes into enter. - // Put any remaining data into enter. - for (; i < dataLength; ++i) { - if (node = group[i]) { - node.__data__ = data[i]; - update[i] = node; - } else { - enter[i] = new EnterNode(parent, data[i]); - } - } - - // Put any non-null nodes that don’t fit into exit. - for (; i < groupLength; ++i) { - if (node = group[i]) { - exit[i] = node; - } - } -} - -function bindKey(parent, group, enter, update, exit, data, key) { - var i, - node, - nodeByKeyValue = new Map, - groupLength = group.length, - dataLength = data.length, - keyValues = new Array(groupLength), - keyValue; - - // Compute the key for each node. - // If multiple nodes have the same key, the duplicates are added to exit. - for (i = 0; i < groupLength; ++i) { - if (node = group[i]) { - keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + ""; - if (nodeByKeyValue.has(keyValue)) { - exit[i] = node; - } else { - nodeByKeyValue.set(keyValue, node); - } - } - } - - // Compute the key for each datum. - // If there a node associated with this key, join and add it to update. - // If there is not (or the key is a duplicate), add it to enter. - for (i = 0; i < dataLength; ++i) { - keyValue = key.call(parent, data[i], i, data) + ""; - if (node = nodeByKeyValue.get(keyValue)) { - update[i] = node; - node.__data__ = data[i]; - nodeByKeyValue.delete(keyValue); - } else { - enter[i] = new EnterNode(parent, data[i]); - } - } - - // Add any remaining nodes that were not bound to data to exit. - for (i = 0; i < groupLength; ++i) { - if ((node = group[i]) && (nodeByKeyValue.get(keyValues[i]) === node)) { - exit[i] = node; - } - } -} - -function datum(node) { - return node.__data__; -} - -/* harmony default export */ function data(value, key) { - if (!arguments.length) return Array.from(this, datum); - - var bind = key ? bindKey : bindIndex, - parents = this._parents, - groups = this._groups; - - if (typeof value !== "function") value = src_constant(value); - - for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) { - var parent = parents[j], - group = groups[j], - groupLength = group.length, - data = arraylike(value.call(parent, parent && parent.__data__, j, parents)), - dataLength = data.length, - enterGroup = enter[j] = new Array(dataLength), - updateGroup = update[j] = new Array(dataLength), - exitGroup = exit[j] = new Array(groupLength); - - bind(parent, group, enterGroup, updateGroup, exitGroup, data, key); - - // Now connect the enter nodes to their following update node, such that - // appendChild can insert the materialized enter node before this node, - // rather than at the end of the parent node. - for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) { - if (previous = enterGroup[i0]) { - if (i0 >= i1) i1 = i0 + 1; - while (!(next = updateGroup[i1]) && ++i1 < dataLength); - previous._next = next || null; - } - } - } - - update = new Selection(update, parents); - update._enter = enter; - update._exit = exit; - return update; -} - -// Given some data, this returns an array-like view of it: an object that -// exposes a length property and allows numeric indexing. Note that unlike -// selectAll, this isn’t worried about “live” collections because the resulting -// array will only be used briefly while data is being bound. (It is possible to -// cause the data to change while iterating by using a key function, but please -// don’t; we’d rather avoid a gratuitous copy.) -function arraylike(data) { - return typeof data === "object" && "length" in data - ? data // Array, TypedArray, NodeList, array-like - : Array.from(data); // Map, Set, iterable, string, or anything else -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/exit.js - - - -/* harmony default export */ function exit() { - return new Selection(this._exit || this._groups.map(sparse), this._parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/join.js -/* harmony default export */ function join(onenter, onupdate, onexit) { - var enter = this.enter(), update = this, exit = this.exit(); - if (typeof onenter === "function") { - enter = onenter(enter); - if (enter) enter = enter.selection(); - } else { - enter = enter.append(onenter + ""); - } - if (onupdate != null) { - update = onupdate(update); - if (update) update = update.selection(); - } - if (onexit == null) exit.remove(); else onexit(exit); - return enter && update ? enter.merge(update).order() : update; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/merge.js - - -/* harmony default export */ function merge(context) { - var selection = context.selection ? context.selection() : context; - - for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) { - for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) { - if (node = group0[i] || group1[i]) { - merge[i] = node; - } - } - } - - for (; j < m0; ++j) { - merges[j] = groups0[j]; - } - - return new Selection(merges, this._parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/order.js -/* harmony default export */ function order() { - - for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) { - for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) { - if (node = group[i]) { - if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next); - next = node; - } - } - } - - return this; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/sort.js - - -/* harmony default export */ function sort(compare) { - if (!compare) compare = ascending; - - function compareNode(a, b) { - return a && b ? compare(a.__data__, b.__data__) : !a - !b; - } - - for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) { - if (node = group[i]) { - sortgroup[i] = node; - } - } - sortgroup.sort(compareNode); - } - - return new Selection(sortgroups, this._parents).order(); -} - -function ascending(a, b) { - return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/call.js -/* harmony default export */ function call() { - var callback = arguments[0]; - arguments[0] = this; - callback.apply(null, arguments); - return this; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/nodes.js -/* harmony default export */ function nodes() { - return Array.from(this); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/node.js -/* harmony default export */ function node() { - - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length; i < n; ++i) { - var node = group[i]; - if (node) return node; - } - } - - return null; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/size.js -/* harmony default export */ function size() { - let size = 0; - for (const node of this) ++size; // eslint-disable-line no-unused-vars - return size; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/empty.js -/* harmony default export */ function selection_empty() { - return !this.node(); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/each.js -/* harmony default export */ function each(callback) { - - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) { - if (node = group[i]) callback.call(node, node.__data__, i, group); - } - } - - return this; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/namespaces.js -var xhtml = "http://www.w3.org/1999/xhtml"; - -/* harmony default export */ const namespaces = ({ - svg: "http://www.w3.org/2000/svg", - xhtml: xhtml, - xlink: "http://www.w3.org/1999/xlink", - xml: "http://www.w3.org/XML/1998/namespace", - xmlns: "http://www.w3.org/2000/xmlns/" -}); - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/namespace.js - - -/* harmony default export */ function namespace(name) { - var prefix = name += "", i = prefix.indexOf(":"); - if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1); - return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/attr.js - - -function attrRemove(name) { - return function() { - this.removeAttribute(name); - }; -} - -function attrRemoveNS(fullname) { - return function() { - this.removeAttributeNS(fullname.space, fullname.local); - }; -} - -function attrConstant(name, value) { - return function() { - this.setAttribute(name, value); - }; -} - -function attrConstantNS(fullname, value) { - return function() { - this.setAttributeNS(fullname.space, fullname.local, value); - }; -} - -function attrFunction(name, value) { - return function() { - var v = value.apply(this, arguments); - if (v == null) this.removeAttribute(name); - else this.setAttribute(name, v); - }; -} - -function attrFunctionNS(fullname, value) { - return function() { - var v = value.apply(this, arguments); - if (v == null) this.removeAttributeNS(fullname.space, fullname.local); - else this.setAttributeNS(fullname.space, fullname.local, v); - }; -} - -/* harmony default export */ function attr(name, value) { - var fullname = namespace(name); - - if (arguments.length < 2) { - var node = this.node(); - return fullname.local - ? node.getAttributeNS(fullname.space, fullname.local) - : node.getAttribute(fullname); - } - - return this.each((value == null - ? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === "function" - ? (fullname.local ? attrFunctionNS : attrFunction) - : (fullname.local ? attrConstantNS : attrConstant)))(fullname, value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/window.js -/* harmony default export */ function src_window(node) { - return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node - || (node.document && node) // node is a Window - || node.defaultView; // node is a Document -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/style.js - - -function styleRemove(name) { - return function() { - this.style.removeProperty(name); - }; -} - -function styleConstant(name, value, priority) { - return function() { - this.style.setProperty(name, value, priority); - }; -} - -function styleFunction(name, value, priority) { - return function() { - var v = value.apply(this, arguments); - if (v == null) this.style.removeProperty(name); - else this.style.setProperty(name, v, priority); - }; -} - -/* harmony default export */ function style(name, value, priority) { - return arguments.length > 1 - ? this.each((value == null - ? styleRemove : typeof value === "function" - ? styleFunction - : styleConstant)(name, value, priority == null ? "" : priority)) - : styleValue(this.node(), name); -} - -function styleValue(node, name) { - return node.style.getPropertyValue(name) - || src_window(node).getComputedStyle(node, null).getPropertyValue(name); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/property.js -function propertyRemove(name) { - return function() { - delete this[name]; - }; -} - -function propertyConstant(name, value) { - return function() { - this[name] = value; - }; -} - -function propertyFunction(name, value) { - return function() { - var v = value.apply(this, arguments); - if (v == null) delete this[name]; - else this[name] = v; - }; -} - -/* harmony default export */ function property(name, value) { - return arguments.length > 1 - ? this.each((value == null - ? propertyRemove : typeof value === "function" - ? propertyFunction - : propertyConstant)(name, value)) - : this.node()[name]; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/classed.js -function classArray(string) { - return string.trim().split(/^|\s+/); -} - -function classList(node) { - return node.classList || new ClassList(node); -} - -function ClassList(node) { - this._node = node; - this._names = classArray(node.getAttribute("class") || ""); -} - -ClassList.prototype = { - add: function(name) { - var i = this._names.indexOf(name); - if (i < 0) { - this._names.push(name); - this._node.setAttribute("class", this._names.join(" ")); - } - }, - remove: function(name) { - var i = this._names.indexOf(name); - if (i >= 0) { - this._names.splice(i, 1); - this._node.setAttribute("class", this._names.join(" ")); - } - }, - contains: function(name) { - return this._names.indexOf(name) >= 0; - } -}; - -function classedAdd(node, names) { - var list = classList(node), i = -1, n = names.length; - while (++i < n) list.add(names[i]); -} - -function classedRemove(node, names) { - var list = classList(node), i = -1, n = names.length; - while (++i < n) list.remove(names[i]); -} - -function classedTrue(names) { - return function() { - classedAdd(this, names); - }; -} - -function classedFalse(names) { - return function() { - classedRemove(this, names); - }; -} - -function classedFunction(names, value) { - return function() { - (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names); - }; -} - -/* harmony default export */ function classed(name, value) { - var names = classArray(name + ""); - - if (arguments.length < 2) { - var list = classList(this.node()), i = -1, n = names.length; - while (++i < n) if (!list.contains(names[i])) return false; - return true; - } - - return this.each((typeof value === "function" - ? classedFunction : value - ? classedTrue - : classedFalse)(names, value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/text.js -function textRemove() { - this.textContent = ""; -} - -function textConstant(value) { - return function() { - this.textContent = value; - }; -} - -function textFunction(value) { - return function() { - var v = value.apply(this, arguments); - this.textContent = v == null ? "" : v; - }; -} - -/* harmony default export */ function selection_text(value) { - return arguments.length - ? this.each(value == null - ? textRemove : (typeof value === "function" - ? textFunction - : textConstant)(value)) - : this.node().textContent; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/html.js -function htmlRemove() { - this.innerHTML = ""; -} - -function htmlConstant(value) { - return function() { - this.innerHTML = value; - }; -} - -function htmlFunction(value) { - return function() { - var v = value.apply(this, arguments); - this.innerHTML = v == null ? "" : v; - }; -} - -/* harmony default export */ function html(value) { - return arguments.length - ? this.each(value == null - ? htmlRemove : (typeof value === "function" - ? htmlFunction - : htmlConstant)(value)) - : this.node().innerHTML; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/raise.js -function raise() { - if (this.nextSibling) this.parentNode.appendChild(this); -} - -/* harmony default export */ function selection_raise() { - return this.each(raise); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/lower.js -function lower() { - if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild); -} - -/* harmony default export */ function selection_lower() { - return this.each(lower); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/creator.js - - - -function creatorInherit(name) { - return function() { - var document = this.ownerDocument, - uri = this.namespaceURI; - return uri === xhtml && document.documentElement.namespaceURI === xhtml - ? document.createElement(name) - : document.createElementNS(uri, name); - }; -} - -function creatorFixed(fullname) { - return function() { - return this.ownerDocument.createElementNS(fullname.space, fullname.local); - }; -} - -/* harmony default export */ function creator(name) { - var fullname = namespace(name); - return (fullname.local - ? creatorFixed - : creatorInherit)(fullname); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/append.js - - -/* harmony default export */ function append(name) { - var create = typeof name === "function" ? name : creator(name); - return this.select(function() { - return this.appendChild(create.apply(this, arguments)); - }); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/insert.js - - - -function constantNull() { - return null; -} - -/* harmony default export */ function insert(name, before) { - var create = typeof name === "function" ? name : creator(name), - select = before == null ? constantNull : typeof before === "function" ? before : selector(before); - return this.select(function() { - return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null); - }); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/remove.js -function remove() { - var parent = this.parentNode; - if (parent) parent.removeChild(this); -} - -/* harmony default export */ function selection_remove() { - return this.each(remove); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/clone.js -function selection_cloneShallow() { - var clone = this.cloneNode(false), parent = this.parentNode; - return parent ? parent.insertBefore(clone, this.nextSibling) : clone; -} - -function selection_cloneDeep() { - var clone = this.cloneNode(true), parent = this.parentNode; - return parent ? parent.insertBefore(clone, this.nextSibling) : clone; -} - -/* harmony default export */ function clone(deep) { - return this.select(deep ? selection_cloneDeep : selection_cloneShallow); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/datum.js -/* harmony default export */ function selection_datum(value) { - return arguments.length - ? this.property("__data__", value) - : this.node().__data__; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/on.js -function contextListener(listener) { - return function(event) { - listener.call(this, event, this.__data__); - }; -} - -function parseTypenames(typenames) { - return typenames.trim().split(/^|\s+/).map(function(t) { - var name = "", i = t.indexOf("."); - if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i); - return {type: t, name: name}; - }); -} - -function onRemove(typename) { - return function() { - var on = this.__on; - if (!on) return; - for (var j = 0, i = -1, m = on.length, o; j < m; ++j) { - if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) { - this.removeEventListener(o.type, o.listener, o.options); - } else { - on[++i] = o; - } - } - if (++i) on.length = i; - else delete this.__on; - }; -} - -function onAdd(typename, value, options) { - return function() { - var on = this.__on, o, listener = contextListener(value); - if (on) for (var j = 0, m = on.length; j < m; ++j) { - if ((o = on[j]).type === typename.type && o.name === typename.name) { - this.removeEventListener(o.type, o.listener, o.options); - this.addEventListener(o.type, o.listener = listener, o.options = options); - o.value = value; - return; - } - } - this.addEventListener(typename.type, listener, options); - o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options}; - if (!on) this.__on = [o]; - else on.push(o); - }; -} - -/* harmony default export */ function on(typename, value, options) { - var typenames = parseTypenames(typename + ""), i, n = typenames.length, t; - - if (arguments.length < 2) { - var on = this.node().__on; - if (on) for (var j = 0, m = on.length, o; j < m; ++j) { - for (i = 0, o = on[j]; i < n; ++i) { - if ((t = typenames[i]).type === o.type && t.name === o.name) { - return o.value; - } - } - } - return; - } - - on = value ? onAdd : onRemove; - for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options)); - return this; -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/dispatch.js - - -function dispatchEvent(node, type, params) { - var window = src_window(node), - event = window.CustomEvent; - - if (typeof event === "function") { - event = new event(type, params); - } else { - event = window.document.createEvent("Event"); - if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail; - else event.initEvent(type, false, false); - } - - node.dispatchEvent(event); -} - -function dispatchConstant(type, params) { - return function() { - return dispatchEvent(this, type, params); - }; -} - -function dispatchFunction(type, params) { - return function() { - return dispatchEvent(this, type, params.apply(this, arguments)); - }; -} - -/* harmony default export */ function dispatch(type, params) { - return this.each((typeof params === "function" - ? dispatchFunction - : dispatchConstant)(type, params)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/iterator.js -/* harmony default export */ function* iterator() { - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) { - if (node = group[i]) yield node; - } - } -} - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/index.js - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -var root = [null]; - -function Selection(groups, parents) { - this._groups = groups; - this._parents = parents; -} - -function selection() { - return new Selection([[document.documentElement]], root); -} - -function selection_selection() { - return this; -} - -Selection.prototype = selection.prototype = { - constructor: Selection, - select: selection_select, - selectAll: selectAll, - selectChild: selectChild, - selectChildren: selectChildren, - filter: selection_filter, - data: data, - enter: enter, - exit: exit, - join: join, - merge: merge, - selection: selection_selection, - order: order, - sort: sort, - call: call, - nodes: nodes, - node: node, - size: size, - empty: selection_empty, - each: each, - attr: attr, - style: style, - property: property, - classed: classed, - text: selection_text, - html: html, - raise: selection_raise, - lower: selection_lower, - append: append, - insert: insert, - remove: selection_remove, - clone: clone, - datum: selection_datum, - on: on, - dispatch: dispatch, - [Symbol.iterator]: iterator -}; - -/* harmony default export */ const src_selection = (selection); - -;// CONCATENATED MODULE: ../node_modules/d3-selection/src/select.js - - -/* harmony default export */ function src_select(selector) { - return typeof selector === "string" - ? new Selection([[document.querySelector(selector)]], [document.documentElement]) - : new Selection([[selector]], root); -} - -;// CONCATENATED MODULE: ../node_modules/d3-dispatch/src/dispatch.js -var noop = {value: () => {}}; - -function dispatch_dispatch() { - for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) { - if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t); - _[t] = []; - } - return new Dispatch(_); -} - -function Dispatch(_) { - this._ = _; -} - -function dispatch_parseTypenames(typenames, types) { - return typenames.trim().split(/^|\s+/).map(function(t) { - var name = "", i = t.indexOf("."); - if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i); - if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t); - return {type: t, name: name}; - }); -} - -Dispatch.prototype = dispatch_dispatch.prototype = { - constructor: Dispatch, - on: function(typename, callback) { - var _ = this._, - T = dispatch_parseTypenames(typename + "", _), - t, - i = -1, - n = T.length; - - // If no callback was specified, return the callback of the given type and name. - if (arguments.length < 2) { - while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t; - return; - } - - // If a type was specified, set the callback for the given type and name. - // Otherwise, if a null callback was specified, remove callbacks of the given name. - if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback); - while (++i < n) { - if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback); - else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null); - } - - return this; - }, - copy: function() { - var copy = {}, _ = this._; - for (var t in _) copy[t] = _[t].slice(); - return new Dispatch(copy); - }, - call: function(type, that) { - if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2]; - if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); - for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); - }, - apply: function(type, that, args) { - if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); - for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); - } -}; - -function get(type, name) { - for (var i = 0, n = type.length, c; i < n; ++i) { - if ((c = type[i]).name === name) { - return c.value; - } - } -} - -function set(type, name, callback) { - for (var i = 0, n = type.length; i < n; ++i) { - if (type[i].name === name) { - type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1)); - break; - } - } - if (callback != null) type.push({name: name, value: callback}); - return type; -} - -/* harmony default export */ const src_dispatch = (dispatch_dispatch); - -;// CONCATENATED MODULE: ../node_modules/d3-timer/src/timer.js -var timer_frame = 0, // is an animation frame pending? - timeout = 0, // is a timeout pending? - interval = 0, // are any timers active? - pokeDelay = 1000, // how frequently we check for clock skew - taskHead, - taskTail, - clockLast = 0, - clockNow = 0, - clockSkew = 0, - clock = typeof performance === "object" && performance.now ? performance : Date, - setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); }; - -function now() { - return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew); -} - -function clearNow() { - clockNow = 0; -} - -function Timer() { - this._call = - this._time = - this._next = null; -} - -Timer.prototype = timer.prototype = { - constructor: Timer, - restart: function(callback, delay, time) { - if (typeof callback !== "function") throw new TypeError("callback is not a function"); - time = (time == null ? now() : +time) + (delay == null ? 0 : +delay); - if (!this._next && taskTail !== this) { - if (taskTail) taskTail._next = this; - else taskHead = this; - taskTail = this; - } - this._call = callback; - this._time = time; - sleep(); - }, - stop: function() { - if (this._call) { - this._call = null; - this._time = Infinity; - sleep(); - } - } -}; - -function timer(callback, delay, time) { - var t = new Timer; - t.restart(callback, delay, time); - return t; -} - -function timerFlush() { - now(); // Get the current time, if not already set. - ++timer_frame; // Pretend we’ve set an alarm, if we haven’t already. - var t = taskHead, e; - while (t) { - if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e); - t = t._next; - } - --timer_frame; -} - -function wake() { - clockNow = (clockLast = clock.now()) + clockSkew; - timer_frame = timeout = 0; - try { - timerFlush(); - } finally { - timer_frame = 0; - nap(); - clockNow = 0; - } -} - -function poke() { - var now = clock.now(), delay = now - clockLast; - if (delay > pokeDelay) clockSkew -= delay, clockLast = now; -} - -function nap() { - var t0, t1 = taskHead, t2, time = Infinity; - while (t1) { - if (t1._call) { - if (time > t1._time) time = t1._time; - t0 = t1, t1 = t1._next; - } else { - t2 = t1._next, t1._next = null; - t1 = t0 ? t0._next = t2 : taskHead = t2; - } - } - taskTail = t0; - sleep(time); -} - -function sleep(time) { - if (timer_frame) return; // Soonest alarm already set, or will be. - if (timeout) timeout = clearTimeout(timeout); - var delay = time - clockNow; // Strictly less than if we recomputed clockNow. - if (delay > 24) { - if (time < Infinity) timeout = setTimeout(wake, time - clock.now() - clockSkew); - if (interval) interval = clearInterval(interval); - } else { - if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay); - timer_frame = 1, setFrame(wake); - } -} - -;// CONCATENATED MODULE: ../node_modules/d3-timer/src/timeout.js - - -/* harmony default export */ function src_timeout(callback, delay, time) { - var t = new Timer; - delay = delay == null ? 0 : +delay; - t.restart(elapsed => { - t.stop(); - callback(elapsed + delay); - }, delay, time); - return t; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/schedule.js - - - -var emptyOn = src_dispatch("start", "end", "cancel", "interrupt"); -var emptyTween = []; - -var CREATED = 0; -var SCHEDULED = 1; -var STARTING = 2; -var STARTED = 3; -var RUNNING = 4; -var ENDING = 5; -var ENDED = 6; - -/* harmony default export */ function schedule(node, name, id, index, group, timing) { - var schedules = node.__transition; - if (!schedules) node.__transition = {}; - else if (id in schedules) return; - create(node, id, { - name: name, - index: index, // For context during callback. - group: group, // For context during callback. - on: emptyOn, - tween: emptyTween, - time: timing.time, - delay: timing.delay, - duration: timing.duration, - ease: timing.ease, - timer: null, - state: CREATED - }); -} - -function init(node, id) { - var schedule = schedule_get(node, id); - if (schedule.state > CREATED) throw new Error("too late; already scheduled"); - return schedule; -} - -function schedule_set(node, id) { - var schedule = schedule_get(node, id); - if (schedule.state > STARTED) throw new Error("too late; already running"); - return schedule; -} - -function schedule_get(node, id) { - var schedule = node.__transition; - if (!schedule || !(schedule = schedule[id])) throw new Error("transition not found"); - return schedule; -} - -function create(node, id, self) { - var schedules = node.__transition, - tween; - - // Initialize the self timer when the transition is created. - // Note the actual delay is not known until the first callback! - schedules[id] = self; - self.timer = timer(schedule, 0, self.time); - - function schedule(elapsed) { - self.state = SCHEDULED; - self.timer.restart(start, self.delay, self.time); - - // If the elapsed delay is less than our first sleep, start immediately. - if (self.delay <= elapsed) start(elapsed - self.delay); - } - - function start(elapsed) { - var i, j, n, o; - - // If the state is not SCHEDULED, then we previously errored on start. - if (self.state !== SCHEDULED) return stop(); - - for (i in schedules) { - o = schedules[i]; - if (o.name !== self.name) continue; - - // While this element already has a starting transition during this frame, - // defer starting an interrupting transition until that transition has a - // chance to tick (and possibly end); see d3/d3-transition#54! - if (o.state === STARTED) return src_timeout(start); - - // Interrupt the active transition, if any. - if (o.state === RUNNING) { - o.state = ENDED; - o.timer.stop(); - o.on.call("interrupt", node, node.__data__, o.index, o.group); - delete schedules[i]; - } - - // Cancel any pre-empted transitions. - else if (+i < id) { - o.state = ENDED; - o.timer.stop(); - o.on.call("cancel", node, node.__data__, o.index, o.group); - delete schedules[i]; - } - } - - // Defer the first tick to end of the current frame; see d3/d3#1576. - // Note the transition may be canceled after start and before the first tick! - // Note this must be scheduled before the start event; see d3/d3-transition#16! - // Assuming this is successful, subsequent callbacks go straight to tick. - src_timeout(function() { - if (self.state === STARTED) { - self.state = RUNNING; - self.timer.restart(tick, self.delay, self.time); - tick(elapsed); - } - }); - - // Dispatch the start event. - // Note this must be done before the tween are initialized. - self.state = STARTING; - self.on.call("start", node, node.__data__, self.index, self.group); - if (self.state !== STARTING) return; // interrupted - self.state = STARTED; - - // Initialize the tween, deleting null tween. - tween = new Array(n = self.tween.length); - for (i = 0, j = -1; i < n; ++i) { - if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) { - tween[++j] = o; - } - } - tween.length = j + 1; - } - - function tick(elapsed) { - var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1), - i = -1, - n = tween.length; - - while (++i < n) { - tween[i].call(node, t); - } - - // Dispatch the end event. - if (self.state === ENDING) { - self.on.call("end", node, node.__data__, self.index, self.group); - stop(); - } - } - - function stop() { - self.state = ENDED; - self.timer.stop(); - delete schedules[id]; - for (var i in schedules) return; // eslint-disable-line no-unused-vars - delete node.__transition; - } -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/interrupt.js - - -/* harmony default export */ function interrupt(node, name) { - var schedules = node.__transition, - schedule, - active, - empty = true, - i; - - if (!schedules) return; - - name = name == null ? null : name + ""; - - for (i in schedules) { - if ((schedule = schedules[i]).name !== name) { empty = false; continue; } - active = schedule.state > STARTING && schedule.state < ENDING; - schedule.state = ENDED; - schedule.timer.stop(); - schedule.on.call(active ? "interrupt" : "cancel", node, node.__data__, schedule.index, schedule.group); - delete schedules[i]; - } - - if (empty) delete node.__transition; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/interrupt.js - - -/* harmony default export */ function selection_interrupt(name) { - return this.each(function() { - interrupt(this, name); - }); -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/number.js -/* harmony default export */ function number(a, b) { - return a = +a, b = +b, function(t) { - return a * (1 - t) + b * t; - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/decompose.js -var degrees = 180 / Math.PI; - -var identity = { - translateX: 0, - translateY: 0, - rotate: 0, - skewX: 0, - scaleX: 1, - scaleY: 1 -}; - -/* harmony default export */ function decompose(a, b, c, d, e, f) { - var scaleX, scaleY, skewX; - if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX; - if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX; - if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY; - if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX; - return { - translateX: e, - translateY: f, - rotate: Math.atan2(b, a) * degrees, - skewX: Math.atan(skewX) * degrees, - scaleX: scaleX, - scaleY: scaleY - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/parse.js - - -var svgNode; - -/* eslint-disable no-undef */ -function parseCss(value) { - const m = new (typeof DOMMatrix === "function" ? DOMMatrix : WebKitCSSMatrix)(value + ""); - return m.isIdentity ? identity : decompose(m.a, m.b, m.c, m.d, m.e, m.f); -} - -function parseSvg(value) { - if (value == null) return identity; - if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svgNode.setAttribute("transform", value); - if (!(value = svgNode.transform.baseVal.consolidate())) return identity; - value = value.matrix; - return decompose(value.a, value.b, value.c, value.d, value.e, value.f); -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/index.js - - - -function interpolateTransform(parse, pxComma, pxParen, degParen) { - - function pop(s) { - return s.length ? s.pop() + " " : ""; - } - - function translate(xa, ya, xb, yb, s, q) { - if (xa !== xb || ya !== yb) { - var i = s.push("translate(", null, pxComma, null, pxParen); - q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)}); - } else if (xb || yb) { - s.push("translate(" + xb + pxComma + yb + pxParen); - } - } - - function rotate(a, b, s, q) { - if (a !== b) { - if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path - q.push({i: s.push(pop(s) + "rotate(", null, degParen) - 2, x: number(a, b)}); - } else if (b) { - s.push(pop(s) + "rotate(" + b + degParen); - } - } - - function skewX(a, b, s, q) { - if (a !== b) { - q.push({i: s.push(pop(s) + "skewX(", null, degParen) - 2, x: number(a, b)}); - } else if (b) { - s.push(pop(s) + "skewX(" + b + degParen); - } - } - - function scale(xa, ya, xb, yb, s, q) { - if (xa !== xb || ya !== yb) { - var i = s.push(pop(s) + "scale(", null, ",", null, ")"); - q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)}); - } else if (xb !== 1 || yb !== 1) { - s.push(pop(s) + "scale(" + xb + "," + yb + ")"); - } - } - - return function(a, b) { - var s = [], // string constants and placeholders - q = []; // number interpolators - a = parse(a), b = parse(b); - translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q); - rotate(a.rotate, b.rotate, s, q); - skewX(a.skewX, b.skewX, s, q); - scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q); - a = b = null; // gc - return function(t) { - var i = -1, n = q.length, o; - while (++i < n) s[(o = q[i]).i] = o.x(t); - return s.join(""); - }; - }; -} - -var interpolateTransformCss = interpolateTransform(parseCss, "px, ", "px)", "deg)"); -var interpolateTransformSvg = interpolateTransform(parseSvg, ", ", ")", ")"); - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/tween.js - - -function tweenRemove(id, name) { - var tween0, tween1; - return function() { - var schedule = schedule_set(this, id), - tween = schedule.tween; - - // If this node shared tween with the previous node, - // just assign the updated shared tween and we’re done! - // Otherwise, copy-on-write. - if (tween !== tween0) { - tween1 = tween0 = tween; - for (var i = 0, n = tween1.length; i < n; ++i) { - if (tween1[i].name === name) { - tween1 = tween1.slice(); - tween1.splice(i, 1); - break; - } - } - } - - schedule.tween = tween1; - }; -} - -function tweenFunction(id, name, value) { - var tween0, tween1; - if (typeof value !== "function") throw new Error; - return function() { - var schedule = schedule_set(this, id), - tween = schedule.tween; - - // If this node shared tween with the previous node, - // just assign the updated shared tween and we’re done! - // Otherwise, copy-on-write. - if (tween !== tween0) { - tween1 = (tween0 = tween).slice(); - for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) { - if (tween1[i].name === name) { - tween1[i] = t; - break; - } - } - if (i === n) tween1.push(t); - } - - schedule.tween = tween1; - }; -} - -/* harmony default export */ function tween(name, value) { - var id = this._id; - - name += ""; - - if (arguments.length < 2) { - var tween = schedule_get(this.node(), id).tween; - for (var i = 0, n = tween.length, t; i < n; ++i) { - if ((t = tween[i]).name === name) { - return t.value; - } - } - return null; - } - - return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value)); -} - -function tweenValue(transition, name, value) { - var id = transition._id; - - transition.each(function() { - var schedule = schedule_set(this, id); - (schedule.value || (schedule.value = {}))[name] = value.apply(this, arguments); - }); - - return function(node) { - return schedule_get(node, id).value[name]; - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-color/src/define.js -/* harmony default export */ function src_define(constructor, factory, prototype) { - constructor.prototype = factory.prototype = prototype; - prototype.constructor = constructor; -} - -function extend(parent, definition) { - var prototype = Object.create(parent.prototype); - for (var key in definition) prototype[key] = definition[key]; - return prototype; -} - -;// CONCATENATED MODULE: ../node_modules/d3-color/src/color.js - - -function Color() {} - -var darker = 0.7; -var brighter = 1 / darker; - -var reI = "\\s*([+-]?\\d+)\\s*", - reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*", - reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*", - reHex = /^#([0-9a-f]{3,8})$/, - reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"), - reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"), - reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"), - reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"), - reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"), - reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$"); - -var named = { - aliceblue: 0xf0f8ff, - antiquewhite: 0xfaebd7, - aqua: 0x00ffff, - aquamarine: 0x7fffd4, - azure: 0xf0ffff, - beige: 0xf5f5dc, - bisque: 0xffe4c4, - black: 0x000000, - blanchedalmond: 0xffebcd, - blue: 0x0000ff, - blueviolet: 0x8a2be2, - brown: 0xa52a2a, - burlywood: 0xdeb887, - cadetblue: 0x5f9ea0, - chartreuse: 0x7fff00, - chocolate: 0xd2691e, - coral: 0xff7f50, - cornflowerblue: 0x6495ed, - cornsilk: 0xfff8dc, - crimson: 0xdc143c, - cyan: 0x00ffff, - darkblue: 0x00008b, - darkcyan: 0x008b8b, - darkgoldenrod: 0xb8860b, - darkgray: 0xa9a9a9, - darkgreen: 0x006400, - darkgrey: 0xa9a9a9, - darkkhaki: 0xbdb76b, - darkmagenta: 0x8b008b, - darkolivegreen: 0x556b2f, - darkorange: 0xff8c00, - darkorchid: 0x9932cc, - darkred: 0x8b0000, - darksalmon: 0xe9967a, - darkseagreen: 0x8fbc8f, - darkslateblue: 0x483d8b, - darkslategray: 0x2f4f4f, - darkslategrey: 0x2f4f4f, - darkturquoise: 0x00ced1, - darkviolet: 0x9400d3, - deeppink: 0xff1493, - deepskyblue: 0x00bfff, - dimgray: 0x696969, - dimgrey: 0x696969, - dodgerblue: 0x1e90ff, - firebrick: 0xb22222, - floralwhite: 0xfffaf0, - forestgreen: 0x228b22, - fuchsia: 0xff00ff, - gainsboro: 0xdcdcdc, - ghostwhite: 0xf8f8ff, - gold: 0xffd700, - goldenrod: 0xdaa520, - gray: 0x808080, - green: 0x008000, - greenyellow: 0xadff2f, - grey: 0x808080, - honeydew: 0xf0fff0, - hotpink: 0xff69b4, - indianred: 0xcd5c5c, - indigo: 0x4b0082, - ivory: 0xfffff0, - khaki: 0xf0e68c, - lavender: 0xe6e6fa, - lavenderblush: 0xfff0f5, - lawngreen: 0x7cfc00, - lemonchiffon: 0xfffacd, - lightblue: 0xadd8e6, - lightcoral: 0xf08080, - lightcyan: 0xe0ffff, - lightgoldenrodyellow: 0xfafad2, - lightgray: 0xd3d3d3, - lightgreen: 0x90ee90, - lightgrey: 0xd3d3d3, - lightpink: 0xffb6c1, - lightsalmon: 0xffa07a, - lightseagreen: 0x20b2aa, - lightskyblue: 0x87cefa, - lightslategray: 0x778899, - lightslategrey: 0x778899, - lightsteelblue: 0xb0c4de, - lightyellow: 0xffffe0, - lime: 0x00ff00, - limegreen: 0x32cd32, - linen: 0xfaf0e6, - magenta: 0xff00ff, - maroon: 0x800000, - mediumaquamarine: 0x66cdaa, - mediumblue: 0x0000cd, - mediumorchid: 0xba55d3, - mediumpurple: 0x9370db, - mediumseagreen: 0x3cb371, - mediumslateblue: 0x7b68ee, - mediumspringgreen: 0x00fa9a, - mediumturquoise: 0x48d1cc, - mediumvioletred: 0xc71585, - midnightblue: 0x191970, - mintcream: 0xf5fffa, - mistyrose: 0xffe4e1, - moccasin: 0xffe4b5, - navajowhite: 0xffdead, - navy: 0x000080, - oldlace: 0xfdf5e6, - olive: 0x808000, - olivedrab: 0x6b8e23, - orange: 0xffa500, - orangered: 0xff4500, - orchid: 0xda70d6, - palegoldenrod: 0xeee8aa, - palegreen: 0x98fb98, - paleturquoise: 0xafeeee, - palevioletred: 0xdb7093, - papayawhip: 0xffefd5, - peachpuff: 0xffdab9, - peru: 0xcd853f, - pink: 0xffc0cb, - plum: 0xdda0dd, - powderblue: 0xb0e0e6, - purple: 0x800080, - rebeccapurple: 0x663399, - red: 0xff0000, - rosybrown: 0xbc8f8f, - royalblue: 0x4169e1, - saddlebrown: 0x8b4513, - salmon: 0xfa8072, - sandybrown: 0xf4a460, - seagreen: 0x2e8b57, - seashell: 0xfff5ee, - sienna: 0xa0522d, - silver: 0xc0c0c0, - skyblue: 0x87ceeb, - slateblue: 0x6a5acd, - slategray: 0x708090, - slategrey: 0x708090, - snow: 0xfffafa, - springgreen: 0x00ff7f, - steelblue: 0x4682b4, - tan: 0xd2b48c, - teal: 0x008080, - thistle: 0xd8bfd8, - tomato: 0xff6347, - turquoise: 0x40e0d0, - violet: 0xee82ee, - wheat: 0xf5deb3, - white: 0xffffff, - whitesmoke: 0xf5f5f5, - yellow: 0xffff00, - yellowgreen: 0x9acd32 -}; - -src_define(Color, color, { - copy: function(channels) { - return Object.assign(new this.constructor, this, channels); - }, - displayable: function() { - return this.rgb().displayable(); - }, - hex: color_formatHex, // Deprecated! Use color.formatHex. - formatHex: color_formatHex, - formatHsl: color_formatHsl, - formatRgb: color_formatRgb, - toString: color_formatRgb -}); - -function color_formatHex() { - return this.rgb().formatHex(); -} - -function color_formatHsl() { - return hslConvert(this).formatHsl(); -} - -function color_formatRgb() { - return this.rgb().formatRgb(); -} - -function color(format) { - var m, l; - format = (format + "").trim().toLowerCase(); - return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000 - : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00 - : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000 - : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000 - : null) // invalid hex - : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0) - : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%) - : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1) - : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1) - : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%) - : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1) - : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins - : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0) - : null; -} - -function rgbn(n) { - return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1); -} - -function rgba(r, g, b, a) { - if (a <= 0) r = g = b = NaN; - return new Rgb(r, g, b, a); -} - -function rgbConvert(o) { - if (!(o instanceof Color)) o = color(o); - if (!o) return new Rgb; - o = o.rgb(); - return new Rgb(o.r, o.g, o.b, o.opacity); -} - -function color_rgb(r, g, b, opacity) { - return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity); -} - -function Rgb(r, g, b, opacity) { - this.r = +r; - this.g = +g; - this.b = +b; - this.opacity = +opacity; -} - -src_define(Rgb, color_rgb, extend(Color, { - brighter: function(k) { - k = k == null ? brighter : Math.pow(brighter, k); - return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); - }, - darker: function(k) { - k = k == null ? darker : Math.pow(darker, k); - return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); - }, - rgb: function() { - return this; - }, - displayable: function() { - return (-0.5 <= this.r && this.r < 255.5) - && (-0.5 <= this.g && this.g < 255.5) - && (-0.5 <= this.b && this.b < 255.5) - && (0 <= this.opacity && this.opacity <= 1); - }, - hex: rgb_formatHex, // Deprecated! Use color.formatHex. - formatHex: rgb_formatHex, - formatRgb: rgb_formatRgb, - toString: rgb_formatRgb -})); - -function rgb_formatHex() { - return "#" + hex(this.r) + hex(this.g) + hex(this.b); -} - -function rgb_formatRgb() { - var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); - return (a === 1 ? "rgb(" : "rgba(") - + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", " - + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", " - + Math.max(0, Math.min(255, Math.round(this.b) || 0)) - + (a === 1 ? ")" : ", " + a + ")"); -} - -function hex(value) { - value = Math.max(0, Math.min(255, Math.round(value) || 0)); - return (value < 16 ? "0" : "") + value.toString(16); -} - -function hsla(h, s, l, a) { - if (a <= 0) h = s = l = NaN; - else if (l <= 0 || l >= 1) h = s = NaN; - else if (s <= 0) h = NaN; - return new Hsl(h, s, l, a); -} - -function hslConvert(o) { - if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity); - if (!(o instanceof Color)) o = color(o); - if (!o) return new Hsl; - if (o instanceof Hsl) return o; - o = o.rgb(); - var r = o.r / 255, - g = o.g / 255, - b = o.b / 255, - min = Math.min(r, g, b), - max = Math.max(r, g, b), - h = NaN, - s = max - min, - l = (max + min) / 2; - if (s) { - if (r === max) h = (g - b) / s + (g < b) * 6; - else if (g === max) h = (b - r) / s + 2; - else h = (r - g) / s + 4; - s /= l < 0.5 ? max + min : 2 - max - min; - h *= 60; - } else { - s = l > 0 && l < 1 ? 0 : h; - } - return new Hsl(h, s, l, o.opacity); -} - -function hsl(h, s, l, opacity) { - return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity); -} - -function Hsl(h, s, l, opacity) { - this.h = +h; - this.s = +s; - this.l = +l; - this.opacity = +opacity; -} - -src_define(Hsl, hsl, extend(Color, { - brighter: function(k) { - k = k == null ? brighter : Math.pow(brighter, k); - return new Hsl(this.h, this.s, this.l * k, this.opacity); - }, - darker: function(k) { - k = k == null ? darker : Math.pow(darker, k); - return new Hsl(this.h, this.s, this.l * k, this.opacity); - }, - rgb: function() { - var h = this.h % 360 + (this.h < 0) * 360, - s = isNaN(h) || isNaN(this.s) ? 0 : this.s, - l = this.l, - m2 = l + (l < 0.5 ? l : 1 - l) * s, - m1 = 2 * l - m2; - return new Rgb( - hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2), - hsl2rgb(h, m1, m2), - hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2), - this.opacity - ); - }, - displayable: function() { - return (0 <= this.s && this.s <= 1 || isNaN(this.s)) - && (0 <= this.l && this.l <= 1) - && (0 <= this.opacity && this.opacity <= 1); - }, - formatHsl: function() { - var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); - return (a === 1 ? "hsl(" : "hsla(") - + (this.h || 0) + ", " - + (this.s || 0) * 100 + "%, " - + (this.l || 0) * 100 + "%" - + (a === 1 ? ")" : ", " + a + ")"); - } -})); - -/* From FvD 13.37, CSS Color Module Level 3 */ -function hsl2rgb(h, m1, m2) { - return (h < 60 ? m1 + (m2 - m1) * h / 60 - : h < 180 ? m2 - : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60 - : m1) * 255; -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/basis.js -function basis(t1, v0, v1, v2, v3) { - var t2 = t1 * t1, t3 = t2 * t1; - return ((1 - 3 * t1 + 3 * t2 - t3) * v0 - + (4 - 6 * t2 + 3 * t3) * v1 - + (1 + 3 * t1 + 3 * t2 - 3 * t3) * v2 - + t3 * v3) / 6; -} - -/* harmony default export */ function src_basis(values) { - var n = values.length - 1; - return function(t) { - var i = t <= 0 ? (t = 0) : t >= 1 ? (t = 1, n - 1) : Math.floor(t * n), - v1 = values[i], - v2 = values[i + 1], - v0 = i > 0 ? values[i - 1] : 2 * v1 - v2, - v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1; - return basis((t - i / n) * n, v0, v1, v2, v3); - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/basisClosed.js - - -/* harmony default export */ function basisClosed(values) { - var n = values.length; - return function(t) { - var i = Math.floor(((t %= 1) < 0 ? ++t : t) * n), - v0 = values[(i + n - 1) % n], - v1 = values[i % n], - v2 = values[(i + 1) % n], - v3 = values[(i + 2) % n]; - return basis((t - i / n) * n, v0, v1, v2, v3); - }; -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/constant.js -/* harmony default export */ const d3_interpolate_src_constant = (x => () => x); - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/color.js - - -function linear(a, d) { - return function(t) { - return a + t * d; - }; -} - -function exponential(a, b, y) { - return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) { - return Math.pow(a + t * b, y); - }; -} - -function hue(a, b) { - var d = b - a; - return d ? linear(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) : constant(isNaN(a) ? b : a); -} - -function gamma(y) { - return (y = +y) === 1 ? nogamma : function(a, b) { - return b - a ? exponential(a, b, y) : d3_interpolate_src_constant(isNaN(a) ? b : a); - }; -} - -function nogamma(a, b) { - var d = b - a; - return d ? linear(a, d) : d3_interpolate_src_constant(isNaN(a) ? b : a); -} - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/rgb.js - - - - - -/* harmony default export */ const rgb = ((function rgbGamma(y) { - var color = gamma(y); - - function rgb(start, end) { - var r = color((start = color_rgb(start)).r, (end = color_rgb(end)).r), - g = color(start.g, end.g), - b = color(start.b, end.b), - opacity = nogamma(start.opacity, end.opacity); - return function(t) { - start.r = r(t); - start.g = g(t); - start.b = b(t); - start.opacity = opacity(t); - return start + ""; - }; - } - - rgb.gamma = rgbGamma; - - return rgb; -})(1)); - -function rgbSpline(spline) { - return function(colors) { - var n = colors.length, - r = new Array(n), - g = new Array(n), - b = new Array(n), - i, color; - for (i = 0; i < n; ++i) { - color = color_rgb(colors[i]); - r[i] = color.r || 0; - g[i] = color.g || 0; - b[i] = color.b || 0; - } - r = spline(r); - g = spline(g); - b = spline(b); - color.opacity = 1; - return function(t) { - color.r = r(t); - color.g = g(t); - color.b = b(t); - return color + ""; - }; - }; -} - -var rgbBasis = rgbSpline(src_basis); -var rgbBasisClosed = rgbSpline(basisClosed); - -;// CONCATENATED MODULE: ../node_modules/d3-interpolate/src/string.js - - -var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, - reB = new RegExp(reA.source, "g"); - -function zero(b) { - return function() { - return b; - }; -} - -function one(b) { - return function(t) { - return b(t) + ""; - }; -} - -/* harmony default export */ function string(a, b) { - var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b - am, // current match in a - bm, // current match in b - bs, // string preceding current number in b, if any - i = -1, // index in s - s = [], // string constants and placeholders - q = []; // number interpolators - - // Coerce inputs to strings. - a = a + "", b = b + ""; - - // Interpolate pairs of numbers in a & b. - while ((am = reA.exec(a)) - && (bm = reB.exec(b))) { - if ((bs = bm.index) > bi) { // a string precedes the next number in b - bs = b.slice(bi, bs); - if (s[i]) s[i] += bs; // coalesce with previous string - else s[++i] = bs; - } - if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match - if (s[i]) s[i] += bm; // coalesce with previous string - else s[++i] = bm; - } else { // interpolate non-matching numbers - s[++i] = null; - q.push({i: i, x: number(am, bm)}); - } - bi = reB.lastIndex; - } - - // Add remains of b. - if (bi < b.length) { - bs = b.slice(bi); - if (s[i]) s[i] += bs; // coalesce with previous string - else s[++i] = bs; - } - - // Special optimization for only a single match. - // Otherwise, interpolate each of the numbers and rejoin the string. - return s.length < 2 ? (q[0] - ? one(q[0].x) - : zero(b)) - : (b = q.length, function(t) { - for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); - return s.join(""); - }); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/interpolate.js - - - -/* harmony default export */ function interpolate(a, b) { - var c; - return (typeof b === "number" ? number - : b instanceof color ? rgb - : (c = color(b)) ? (b = c, rgb) - : string)(a, b); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/attr.js - - - - - -function attr_attrRemove(name) { - return function() { - this.removeAttribute(name); - }; -} - -function attr_attrRemoveNS(fullname) { - return function() { - this.removeAttributeNS(fullname.space, fullname.local); - }; -} - -function attr_attrConstant(name, interpolate, value1) { - var string00, - string1 = value1 + "", - interpolate0; - return function() { - var string0 = this.getAttribute(name); - return string0 === string1 ? null - : string0 === string00 ? interpolate0 - : interpolate0 = interpolate(string00 = string0, value1); - }; -} - -function attr_attrConstantNS(fullname, interpolate, value1) { - var string00, - string1 = value1 + "", - interpolate0; - return function() { - var string0 = this.getAttributeNS(fullname.space, fullname.local); - return string0 === string1 ? null - : string0 === string00 ? interpolate0 - : interpolate0 = interpolate(string00 = string0, value1); - }; -} - -function attr_attrFunction(name, interpolate, value) { - var string00, - string10, - interpolate0; - return function() { - var string0, value1 = value(this), string1; - if (value1 == null) return void this.removeAttribute(name); - string0 = this.getAttribute(name); - string1 = value1 + ""; - return string0 === string1 ? null - : string0 === string00 && string1 === string10 ? interpolate0 - : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1)); - }; -} - -function attr_attrFunctionNS(fullname, interpolate, value) { - var string00, - string10, - interpolate0; - return function() { - var string0, value1 = value(this), string1; - if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local); - string0 = this.getAttributeNS(fullname.space, fullname.local); - string1 = value1 + ""; - return string0 === string1 ? null - : string0 === string00 && string1 === string10 ? interpolate0 - : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1)); - }; -} - -/* harmony default export */ function transition_attr(name, value) { - var fullname = namespace(name), i = fullname === "transform" ? interpolateTransformSvg : interpolate; - return this.attrTween(name, typeof value === "function" - ? (fullname.local ? attr_attrFunctionNS : attr_attrFunction)(fullname, i, tweenValue(this, "attr." + name, value)) - : value == null ? (fullname.local ? attr_attrRemoveNS : attr_attrRemove)(fullname) - : (fullname.local ? attr_attrConstantNS : attr_attrConstant)(fullname, i, value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/attrTween.js - - -function attrInterpolate(name, i) { - return function(t) { - this.setAttribute(name, i.call(this, t)); - }; -} - -function attrInterpolateNS(fullname, i) { - return function(t) { - this.setAttributeNS(fullname.space, fullname.local, i.call(this, t)); - }; -} - -function attrTweenNS(fullname, value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i); - return t0; - } - tween._value = value; - return tween; -} - -function attrTween(name, value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i); - return t0; - } - tween._value = value; - return tween; -} - -/* harmony default export */ function transition_attrTween(name, value) { - var key = "attr." + name; - if (arguments.length < 2) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== "function") throw new Error; - var fullname = namespace(name); - return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/delay.js - - -function delayFunction(id, value) { - return function() { - init(this, id).delay = +value.apply(this, arguments); - }; -} - -function delayConstant(id, value) { - return value = +value, function() { - init(this, id).delay = value; - }; -} - -/* harmony default export */ function delay(value) { - var id = this._id; - - return arguments.length - ? this.each((typeof value === "function" - ? delayFunction - : delayConstant)(id, value)) - : schedule_get(this.node(), id).delay; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/duration.js - - -function durationFunction(id, value) { - return function() { - schedule_set(this, id).duration = +value.apply(this, arguments); - }; -} - -function durationConstant(id, value) { - return value = +value, function() { - schedule_set(this, id).duration = value; - }; -} - -/* harmony default export */ function duration(value) { - var id = this._id; - - return arguments.length - ? this.each((typeof value === "function" - ? durationFunction - : durationConstant)(id, value)) - : schedule_get(this.node(), id).duration; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/ease.js - - -function easeConstant(id, value) { - if (typeof value !== "function") throw new Error; - return function() { - schedule_set(this, id).ease = value; - }; -} - -/* harmony default export */ function ease(value) { - var id = this._id; - - return arguments.length - ? this.each(easeConstant(id, value)) - : schedule_get(this.node(), id).ease; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/easeVarying.js - - -function easeVarying(id, value) { - return function() { - var v = value.apply(this, arguments); - if (typeof v !== "function") throw new Error; - schedule_set(this, id).ease = v; - }; -} - -/* harmony default export */ function transition_easeVarying(value) { - if (typeof value !== "function") throw new Error; - return this.each(easeVarying(this._id, value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/filter.js - - - -/* harmony default export */ function transition_filter(match) { - if (typeof match !== "function") match = matcher(match); - - for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) { - if ((node = group[i]) && match.call(node, node.__data__, i, group)) { - subgroup.push(node); - } - } - } - - return new Transition(subgroups, this._parents, this._name, this._id); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/merge.js - - -/* harmony default export */ function transition_merge(transition) { - if (transition._id !== this._id) throw new Error; - - for (var groups0 = this._groups, groups1 = transition._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) { - for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) { - if (node = group0[i] || group1[i]) { - merge[i] = node; - } - } - } - - for (; j < m0; ++j) { - merges[j] = groups0[j]; - } - - return new Transition(merges, this._parents, this._name, this._id); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/on.js - - -function start(name) { - return (name + "").trim().split(/^|\s+/).every(function(t) { - var i = t.indexOf("."); - if (i >= 0) t = t.slice(0, i); - return !t || t === "start"; - }); -} - -function onFunction(id, name, listener) { - var on0, on1, sit = start(name) ? init : schedule_set; - return function() { - var schedule = sit(this, id), - on = schedule.on; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener); - - schedule.on = on1; - }; -} - -/* harmony default export */ function transition_on(name, listener) { - var id = this._id; - - return arguments.length < 2 - ? schedule_get(this.node(), id).on.on(name) - : this.each(onFunction(id, name, listener)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/remove.js -function removeFunction(id) { - return function() { - var parent = this.parentNode; - for (var i in this.__transition) if (+i !== id) return; - if (parent) parent.removeChild(this); - }; -} - -/* harmony default export */ function transition_remove() { - return this.on("end.remove", removeFunction(this._id)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/select.js - - - - -/* harmony default export */ function transition_select(select) { - var name = this._name, - id = this._id; - - if (typeof select !== "function") select = selector(select); - - for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) { - if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) { - if ("__data__" in node) subnode.__data__ = node.__data__; - subgroup[i] = subnode; - schedule(subgroup[i], name, id, i, subgroup, schedule_get(node, id)); - } - } - } - - return new Transition(subgroups, this._parents, name, id); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/selectAll.js - - - - -/* harmony default export */ function transition_selectAll(select) { - var name = this._name, - id = this._id; - - if (typeof select !== "function") select = selectorAll(select); - - for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if (node = group[i]) { - for (var children = select.call(node, node.__data__, i, group), child, inherit = schedule_get(node, id), k = 0, l = children.length; k < l; ++k) { - if (child = children[k]) { - schedule(child, name, id, k, children, inherit); - } - } - subgroups.push(children); - parents.push(node); - } - } - } - - return new Transition(subgroups, parents, name, id); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/selection.js - - -var selection_Selection = src_selection.prototype.constructor; - -/* harmony default export */ function transition_selection() { - return new selection_Selection(this._groups, this._parents); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/style.js - - - - - - -function styleNull(name, interpolate) { - var string00, - string10, - interpolate0; - return function() { - var string0 = styleValue(this, name), - string1 = (this.style.removeProperty(name), styleValue(this, name)); - return string0 === string1 ? null - : string0 === string00 && string1 === string10 ? interpolate0 - : interpolate0 = interpolate(string00 = string0, string10 = string1); - }; -} - -function style_styleRemove(name) { - return function() { - this.style.removeProperty(name); - }; -} - -function style_styleConstant(name, interpolate, value1) { - var string00, - string1 = value1 + "", - interpolate0; - return function() { - var string0 = styleValue(this, name); - return string0 === string1 ? null - : string0 === string00 ? interpolate0 - : interpolate0 = interpolate(string00 = string0, value1); - }; -} - -function style_styleFunction(name, interpolate, value) { - var string00, - string10, - interpolate0; - return function() { - var string0 = styleValue(this, name), - value1 = value(this), - string1 = value1 + ""; - if (value1 == null) string1 = value1 = (this.style.removeProperty(name), styleValue(this, name)); - return string0 === string1 ? null - : string0 === string00 && string1 === string10 ? interpolate0 - : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1)); - }; -} - -function styleMaybeRemove(id, name) { - var on0, on1, listener0, key = "style." + name, event = "end." + key, remove; - return function() { - var schedule = schedule_set(this, id), - on = schedule.on, - listener = schedule.value[key] == null ? remove || (remove = style_styleRemove(name)) : undefined; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener); - - schedule.on = on1; - }; -} - -/* harmony default export */ function transition_style(name, value, priority) { - var i = (name += "") === "transform" ? interpolateTransformCss : interpolate; - return value == null ? this - .styleTween(name, styleNull(name, i)) - .on("end.style." + name, style_styleRemove(name)) - : typeof value === "function" ? this - .styleTween(name, style_styleFunction(name, i, tweenValue(this, "style." + name, value))) - .each(styleMaybeRemove(this._id, name)) - : this - .styleTween(name, style_styleConstant(name, i, value), priority) - .on("end.style." + name, null); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/styleTween.js -function styleInterpolate(name, i, priority) { - return function(t) { - this.style.setProperty(name, i.call(this, t), priority); - }; -} - -function styleTween(name, value, priority) { - var t, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority); - return t; - } - tween._value = value; - return tween; -} - -/* harmony default export */ function transition_styleTween(name, value, priority) { - var key = "style." + (name += ""); - if (arguments.length < 2) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== "function") throw new Error; - return this.tween(key, styleTween(name, value, priority == null ? "" : priority)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/text.js - - -function text_textConstant(value) { - return function() { - this.textContent = value; - }; -} - -function text_textFunction(value) { - return function() { - var value1 = value(this); - this.textContent = value1 == null ? "" : value1; - }; -} - -/* harmony default export */ function transition_text(value) { - return this.tween("text", typeof value === "function" - ? text_textFunction(tweenValue(this, "text", value)) - : text_textConstant(value == null ? "" : value + "")); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/textTween.js -function textInterpolate(i) { - return function(t) { - this.textContent = i.call(this, t); - }; -} - -function textTween(value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && textInterpolate(i); - return t0; - } - tween._value = value; - return tween; -} - -/* harmony default export */ function transition_textTween(value) { - var key = "text"; - if (arguments.length < 1) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== "function") throw new Error; - return this.tween(key, textTween(value)); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/transition.js - - - -/* harmony default export */ function transition() { - var name = this._name, - id0 = this._id, - id1 = newId(); - - for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if (node = group[i]) { - var inherit = schedule_get(node, id0); - schedule(node, name, id1, i, group, { - time: inherit.time + inherit.delay + inherit.duration, - delay: 0, - duration: inherit.duration, - ease: inherit.ease - }); - } - } - } - - return new Transition(groups, this._parents, name, id1); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/end.js - - -/* harmony default export */ function end() { - var on0, on1, that = this, id = that._id, size = that.size(); - return new Promise(function(resolve, reject) { - var cancel = {value: reject}, - end = {value: function() { if (--size === 0) resolve(); }}; - - that.each(function() { - var schedule = schedule_set(this, id), - on = schedule.on; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0) { - on1 = (on0 = on).copy(); - on1._.cancel.push(cancel); - on1._.interrupt.push(cancel); - on1._.end.push(end); - } - - schedule.on = on1; - }); - - // The selection was empty, resolve end immediately - if (size === 0) resolve(); - }); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/index.js - - - - - - - - - - - - - - - - - - - - - - -var id = 0; - -function Transition(groups, parents, name, id) { - this._groups = groups; - this._parents = parents; - this._name = name; - this._id = id; -} - -function transition_transition(name) { - return src_selection().transition(name); -} - -function newId() { - return ++id; -} - -var selection_prototype = src_selection.prototype; - -Transition.prototype = transition_transition.prototype = { - constructor: Transition, - select: transition_select, - selectAll: transition_selectAll, - selectChild: selection_prototype.selectChild, - selectChildren: selection_prototype.selectChildren, - filter: transition_filter, - merge: transition_merge, - selection: transition_selection, - transition: transition, - call: selection_prototype.call, - nodes: selection_prototype.nodes, - node: selection_prototype.node, - size: selection_prototype.size, - empty: selection_prototype.empty, - each: selection_prototype.each, - on: transition_on, - attr: transition_attr, - attrTween: transition_attrTween, - style: transition_style, - styleTween: transition_styleTween, - text: transition_text, - textTween: transition_textTween, - remove: transition_remove, - tween: tween, - delay: delay, - duration: duration, - ease: ease, - easeVarying: transition_easeVarying, - end: end, - [Symbol.iterator]: selection_prototype[Symbol.iterator] -}; - -;// CONCATENATED MODULE: ../node_modules/d3-ease/src/cubic.js -function cubicIn(t) { - return t * t * t; -} - -function cubicOut(t) { - return --t * t * t + 1; -} - -function cubicInOut(t) { - return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2; -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/transition.js - - - - - -var defaultTiming = { - time: null, // Set on use. - delay: 0, - duration: 250, - ease: cubicInOut -}; - -function inherit(node, id) { - var timing; - while (!(timing = node.__transition) || !(timing = timing[id])) { - if (!(node = node.parentNode)) { - throw new Error(`transition ${id} not found`); - } - } - return timing; -} - -/* harmony default export */ function selection_transition(name) { - var id, - timing; - - if (name instanceof Transition) { - id = name._id, name = name._name; - } else { - id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + ""; - } - - for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if (node = group[i]) { - schedule(node, name, id, i, group, timing || inherit(node, id)); - } - } - } - - return new Transition(groups, this._parents, name, id); -} - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/index.js - - - - -src_selection.prototype.interrupt = selection_interrupt; -src_selection.prototype.transition = selection_transition; - -;// CONCATENATED MODULE: ../node_modules/d3-transition/src/index.js - - - - - -;// CONCATENATED MODULE: ./tooltip.js -/* global event */ - - - - - - -function defaultLabel (d) { - return d.data.name -} - -function defaultFlamegraphTooltip () { - var rootElement = src_select('body') - var tooltip = null - // Function to get HTML content from data. - var html = defaultLabel - // Function to get text content from data. - var text = defaultLabel - // Whether to use d3's .html() to set content, otherwise use .text(). - var contentIsHTML = false - - function tip () { - tooltip = rootElement - .append('div') - .style('display', 'none') - .style('position', 'absolute') - .style('opacity', 0) - .style('pointer-events', 'none') - .attr('class', 'd3-flame-graph-tip') - } - - tip.show = function (d) { - tooltip - .style('display', 'block') - .style('left', event.pageX + 5 + 'px') - .style('top', event.pageY + 5 + 'px') - .transition() - .duration(200) - .style('opacity', 1) - .style('pointer-events', 'all') - - if (contentIsHTML) { - tooltip.html(html(d)) - } else { - tooltip.text(text(d)) - } - - return tip - } - - tip.hide = function () { - tooltip - .style('display', 'none') - .transition() - .duration(200) - .style('opacity', 0) - .style('pointer-events', 'none') - - return tip - } - - /** - * Gets/sets a function converting the d3 data into the tooltip's textContent. - * - * Cannot be combined with tip.html(). - */ - tip.text = function (_) { - if (!arguments.length) return text - text = _ - contentIsHTML = false - return tip - } - - /** - * Gets/sets a function converting the d3 data into the tooltip's innerHTML. - * - * Cannot be combined with tip.text(). - * - * @deprecated prefer tip.text(). - */ - tip.html = function (_) { - if (!arguments.length) return html - html = _ - contentIsHTML = true - return tip - } - - tip.destroy = function () { - tooltip.remove() - } - - return tip -} - -/******/ return __webpack_exports__; -/******/ })() -; -}); \ No newline at end of file diff --git a/development/charts/flamegraph/lib/d3-flamegraph.css b/development/charts/flamegraph/lib/d3-flamegraph.css deleted file mode 100644 index fa6f345ff7a9..000000000000 --- a/development/charts/flamegraph/lib/d3-flamegraph.css +++ /dev/null @@ -1,46 +0,0 @@ -.d3-flame-graph rect { - stroke: #EEEEEE; - fill-opacity: .8; -} - -.d3-flame-graph rect:hover { - stroke: #474747; - stroke-width: 0.5; - cursor: pointer; -} - -.d3-flame-graph-label { - pointer-events: none; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - font-size: 12px; - font-family: Verdana; - margin-left: 4px; - margin-right: 4px; - line-height: 1.5; - padding: 0 0 0; - font-weight: 400; - color: black; - text-align: left; -} - -.d3-flame-graph .fade { - opacity: 0.6 !important; -} - -.d3-flame-graph .title { - font-size: 20px; - font-family: Verdana; -} - -.d3-flame-graph-tip { - background-color: black; - border: none; - border-radius: 3px; - padding: 5px 10px 5px 10px; - min-width: 250px; - text-align: left; - color: white; - z-index: 10; -} \ No newline at end of file diff --git a/development/charts/flamegraph/lib/d3-flamegraph.js b/development/charts/flamegraph/lib/d3-flamegraph.js deleted file mode 100644 index eabb2c44972c..000000000000 --- a/development/charts/flamegraph/lib/d3-flamegraph.js +++ /dev/null @@ -1,5719 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if (typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if (typeof define === 'function' && define.amd) define([], factory); - else if (typeof exports === 'object') exports['flamegraph'] = factory(); - else root['flamegraph'] = factory(); -})(self, function () { - return /******/ (() => { - // webpackBootstrap - /******/ 'use strict'; // The require scope - /******/ /******/ var __webpack_require__ = {}; /* webpack/runtime/define property getters */ - /******/ - /************************************************************************/ - /******/ /******/ (() => { - /******/ // define getter functions for harmony exports - /******/ __webpack_require__.d = (exports, definition) => { - /******/ for (var key in definition) { - /******/ if ( - __webpack_require__.o(definition, key) && - !__webpack_require__.o(exports, key) - ) { - /******/ Object.defineProperty(exports, key, { - enumerable: true, - get: definition[key], - }); - /******/ - } - /******/ - } - /******/ - }; - /******/ - })(); /* webpack/runtime/hasOwnProperty shorthand */ - /******/ - /******/ /******/ (() => { - /******/ __webpack_require__.o = (obj, prop) => - Object.prototype.hasOwnProperty.call(obj, prop); - /******/ - })(); - /******/ - /************************************************************************/ - var __webpack_exports__ = {}; - - // EXPORTS - __webpack_require__.d(__webpack_exports__, { - default: () => /* binding */ flamegraph, - }); // CONCATENATED MODULE: ../node_modules/d3-selection/src/selector.js - - function none() {} - - /* harmony default export */ function selector(selector) { - return selector == null - ? none - : function () { - return this.querySelector(selector); - }; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/select.js - - /* harmony default export */ function selection_select(select) { - if (typeof select !== 'function') select = selector(select); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = new Array(m), - j = 0; - j < m; - ++j - ) { - for ( - var group = groups[j], - n = group.length, - subgroup = (subgroups[j] = new Array(n)), - node, - subnode, - i = 0; - i < n; - ++i - ) { - if ( - (node = group[i]) && - (subnode = select.call(node, node.__data__, i, group)) - ) { - if ('__data__' in node) subnode.__data__ = node.__data__; - subgroup[i] = subnode; - } - } - } - - return new Selection(subgroups, this._parents); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/array.js - - // Given something array like (or null), returns something that is strictly an - // array. This is used to ensure that array-like objects passed to d3.selectAll - // or selection.selectAll are converted into proper arrays when creating a - // selection; we don’t ever want to create a selection backed by a live - // HTMLCollection or NodeList. However, note that selection.selectAll will use a - // static NodeList as a group, since it safely derived from querySelectorAll. - function array(x) { - return x == null ? [] : Array.isArray(x) ? x : Array.from(x); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selectorAll.js - - function empty() { - return []; - } - - /* harmony default export */ function selectorAll(selector) { - return selector == null - ? empty - : function () { - return this.querySelectorAll(selector); - }; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectAll.js - - function arrayAll(select) { - return function () { - return array(select.apply(this, arguments)); - }; - } - - /* harmony default export */ function selectAll(select) { - if (typeof select === 'function') select = arrayAll(select); - else select = selectorAll(select); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = [], - parents = [], - j = 0; - j < m; - ++j - ) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if ((node = group[i])) { - subgroups.push(select.call(node, node.__data__, i, group)); - parents.push(node); - } - } - } - - return new Selection(subgroups, parents); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/matcher.js - - /* harmony default export */ function matcher(selector) { - return function () { - return this.matches(selector); - }; - } - - function childMatcher(selector) { - return function (node) { - return node.matches(selector); - }; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectChild.js - - var find = Array.prototype.find; - - function childFind(match) { - return function () { - return find.call(this.children, match); - }; - } - - function childFirst() { - return this.firstElementChild; - } - - /* harmony default export */ function selectChild(match) { - return this.select( - match == null - ? childFirst - : childFind( - typeof match === 'function' ? match : childMatcher(match), - ), - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/selectChildren.js - - var filter = Array.prototype.filter; - - function children() { - return Array.from(this.children); - } - - function childrenFilter(match) { - return function () { - return filter.call(this.children, match); - }; - } - - /* harmony default export */ function selectChildren(match) { - return this.selectAll( - match == null - ? children - : childrenFilter( - typeof match === 'function' ? match : childMatcher(match), - ), - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/filter.js - - /* harmony default export */ function selection_filter(match) { - if (typeof match !== 'function') match = matcher(match); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = new Array(m), - j = 0; - j < m; - ++j - ) { - for ( - var group = groups[j], - n = group.length, - subgroup = (subgroups[j] = []), - node, - i = 0; - i < n; - ++i - ) { - if ((node = group[i]) && match.call(node, node.__data__, i, group)) { - subgroup.push(node); - } - } - } - - return new Selection(subgroups, this._parents); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/sparse.js - - /* harmony default export */ function sparse(update) { - return new Array(update.length); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/enter.js - - /* harmony default export */ function enter() { - return new Selection( - this._enter || this._groups.map(sparse), - this._parents, - ); - } - - function EnterNode(parent, datum) { - this.ownerDocument = parent.ownerDocument; - this.namespaceURI = parent.namespaceURI; - this._next = null; - this._parent = parent; - this.__data__ = datum; - } - - EnterNode.prototype = { - constructor: EnterNode, - appendChild: function (child) { - return this._parent.insertBefore(child, this._next); - }, - insertBefore: function (child, next) { - return this._parent.insertBefore(child, next); - }, - querySelector: function (selector) { - return this._parent.querySelector(selector); - }, - querySelectorAll: function (selector) { - return this._parent.querySelectorAll(selector); - }, - }; // CONCATENATED MODULE: ../node_modules/d3-selection/src/constant.js - - /* harmony default export */ function src_constant(x) { - return function () { - return x; - }; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/data.js - - function bindIndex(parent, group, enter, update, exit, data) { - var i = 0, - node, - groupLength = group.length, - dataLength = data.length; - - // Put any non-null nodes that fit into update. - // Put any null nodes into enter. - // Put any remaining data into enter. - for (; i < dataLength; ++i) { - if ((node = group[i])) { - node.__data__ = data[i]; - update[i] = node; - } else { - enter[i] = new EnterNode(parent, data[i]); - } - } - - // Put any non-null nodes that don’t fit into exit. - for (; i < groupLength; ++i) { - if ((node = group[i])) { - exit[i] = node; - } - } - } - - function bindKey(parent, group, enter, update, exit, data, key) { - var i, - node, - nodeByKeyValue = new Map(), - groupLength = group.length, - dataLength = data.length, - keyValues = new Array(groupLength), - keyValue; - - // Compute the key for each node. - // If multiple nodes have the same key, the duplicates are added to exit. - for (i = 0; i < groupLength; ++i) { - if ((node = group[i])) { - keyValues[i] = keyValue = - key.call(node, node.__data__, i, group) + ''; - if (nodeByKeyValue.has(keyValue)) { - exit[i] = node; - } else { - nodeByKeyValue.set(keyValue, node); - } - } - } - - // Compute the key for each datum. - // If there a node associated with this key, join and add it to update. - // If there is not (or the key is a duplicate), add it to enter. - for (i = 0; i < dataLength; ++i) { - keyValue = key.call(parent, data[i], i, data) + ''; - if ((node = nodeByKeyValue.get(keyValue))) { - update[i] = node; - node.__data__ = data[i]; - nodeByKeyValue.delete(keyValue); - } else { - enter[i] = new EnterNode(parent, data[i]); - } - } - - // Add any remaining nodes that were not bound to data to exit. - for (i = 0; i < groupLength; ++i) { - if ((node = group[i]) && nodeByKeyValue.get(keyValues[i]) === node) { - exit[i] = node; - } - } - } - - function datum(node) { - return node.__data__; - } - - /* harmony default export */ function data(value, key) { - if (!arguments.length) return Array.from(this, datum); - - var bind = key ? bindKey : bindIndex, - parents = this._parents, - groups = this._groups; - - if (typeof value !== 'function') value = src_constant(value); - - for ( - var m = groups.length, - update = new Array(m), - enter = new Array(m), - exit = new Array(m), - j = 0; - j < m; - ++j - ) { - var parent = parents[j], - group = groups[j], - groupLength = group.length, - data = arraylike( - value.call(parent, parent && parent.__data__, j, parents), - ), - dataLength = data.length, - enterGroup = (enter[j] = new Array(dataLength)), - updateGroup = (update[j] = new Array(dataLength)), - exitGroup = (exit[j] = new Array(groupLength)); - - bind(parent, group, enterGroup, updateGroup, exitGroup, data, key); - - // Now connect the enter nodes to their following update node, such that - // appendChild can insert the materialized enter node before this node, - // rather than at the end of the parent node. - for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) { - if ((previous = enterGroup[i0])) { - if (i0 >= i1) i1 = i0 + 1; - while (!(next = updateGroup[i1]) && ++i1 < dataLength); - previous._next = next || null; - } - } - } - - update = new Selection(update, parents); - update._enter = enter; - update._exit = exit; - return update; - } - - // Given some data, this returns an array-like view of it: an object that - // exposes a length property and allows numeric indexing. Note that unlike - // selectAll, this isn’t worried about “live” collections because the resulting - // array will only be used briefly while data is being bound. (It is possible to - // cause the data to change while iterating by using a key function, but please - // don’t; we’d rather avoid a gratuitous copy.) - function arraylike(data) { - return typeof data === 'object' && 'length' in data - ? data // Array, TypedArray, NodeList, array-like - : Array.from(data); // Map, Set, iterable, string, or anything else - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/exit.js - - /* harmony default export */ function exit() { - return new Selection( - this._exit || this._groups.map(sparse), - this._parents, - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/join.js - - /* harmony default export */ function join(onenter, onupdate, onexit) { - var enter = this.enter(), - update = this, - exit = this.exit(); - if (typeof onenter === 'function') { - enter = onenter(enter); - if (enter) enter = enter.selection(); - } else { - enter = enter.append(onenter + ''); - } - if (onupdate != null) { - update = onupdate(update); - if (update) update = update.selection(); - } - if (onexit == null) exit.remove(); - else onexit(exit); - return enter && update ? enter.merge(update).order() : update; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/merge.js - - /* harmony default export */ function merge(context) { - var selection = context.selection ? context.selection() : context; - - for ( - var groups0 = this._groups, - groups1 = selection._groups, - m0 = groups0.length, - m1 = groups1.length, - m = Math.min(m0, m1), - merges = new Array(m0), - j = 0; - j < m; - ++j - ) { - for ( - var group0 = groups0[j], - group1 = groups1[j], - n = group0.length, - merge = (merges[j] = new Array(n)), - node, - i = 0; - i < n; - ++i - ) { - if ((node = group0[i] || group1[i])) { - merge[i] = node; - } - } - } - - for (; j < m0; ++j) { - merges[j] = groups0[j]; - } - - return new Selection(merges, this._parents); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/order.js - - /* harmony default export */ function order() { - for (var groups = this._groups, j = -1, m = groups.length; ++j < m; ) { - for ( - var group = groups[j], i = group.length - 1, next = group[i], node; - --i >= 0; - - ) { - if ((node = group[i])) { - if (next && node.compareDocumentPosition(next) ^ 4) - next.parentNode.insertBefore(node, next); - next = node; - } - } - } - - return this; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/sort.js - - /* harmony default export */ function sort(compare) { - if (!compare) compare = ascending; - - function compareNode(a, b) { - return a && b ? compare(a.__data__, b.__data__) : !a - !b; - } - - for ( - var groups = this._groups, - m = groups.length, - sortgroups = new Array(m), - j = 0; - j < m; - ++j - ) { - for ( - var group = groups[j], - n = group.length, - sortgroup = (sortgroups[j] = new Array(n)), - node, - i = 0; - i < n; - ++i - ) { - if ((node = group[i])) { - sortgroup[i] = node; - } - } - sortgroup.sort(compareNode); - } - - return new Selection(sortgroups, this._parents).order(); - } - - function ascending(a, b) { - return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/call.js - - /* harmony default export */ function call() { - var callback = arguments[0]; - arguments[0] = this; - callback.apply(null, arguments); - return this; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/nodes.js - - /* harmony default export */ function nodes() { - return Array.from(this); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/node.js - - /* harmony default export */ function node() { - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length; i < n; ++i) { - var node = group[i]; - if (node) return node; - } - } - - return null; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/size.js - - /* harmony default export */ function size() { - let size = 0; - for (const node of this) ++size; // eslint-disable-line no-unused-vars - return size; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/empty.js - - /* harmony default export */ function selection_empty() { - return !this.node(); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/each.js - - /* harmony default export */ function each(callback) { - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) { - if ((node = group[i])) callback.call(node, node.__data__, i, group); - } - } - - return this; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/namespaces.js - - var xhtml = 'http://www.w3.org/1999/xhtml'; - - /* harmony default export */ const namespaces = { - svg: 'http://www.w3.org/2000/svg', - xhtml: xhtml, - xlink: 'http://www.w3.org/1999/xlink', - xml: 'http://www.w3.org/XML/1998/namespace', - xmlns: 'http://www.w3.org/2000/xmlns/', - }; // CONCATENATED MODULE: ../node_modules/d3-selection/src/namespace.js - - /* harmony default export */ function namespace(name) { - var prefix = (name += ''), - i = prefix.indexOf(':'); - if (i >= 0 && (prefix = name.slice(0, i)) !== 'xmlns') - name = name.slice(i + 1); - return namespaces.hasOwnProperty(prefix) - ? { space: namespaces[prefix], local: name } - : name; // eslint-disable-line no-prototype-builtins - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/attr.js - - function attrRemove(name) { - return function () { - this.removeAttribute(name); - }; - } - - function attrRemoveNS(fullname) { - return function () { - this.removeAttributeNS(fullname.space, fullname.local); - }; - } - - function attrConstant(name, value) { - return function () { - this.setAttribute(name, value); - }; - } - - function attrConstantNS(fullname, value) { - return function () { - this.setAttributeNS(fullname.space, fullname.local, value); - }; - } - - function attrFunction(name, value) { - return function () { - var v = value.apply(this, arguments); - if (v == null) this.removeAttribute(name); - else this.setAttribute(name, v); - }; - } - - function attrFunctionNS(fullname, value) { - return function () { - var v = value.apply(this, arguments); - if (v == null) this.removeAttributeNS(fullname.space, fullname.local); - else this.setAttributeNS(fullname.space, fullname.local, v); - }; - } - - /* harmony default export */ function attr(name, value) { - var fullname = namespace(name); - - if (arguments.length < 2) { - var node = this.node(); - return fullname.local - ? node.getAttributeNS(fullname.space, fullname.local) - : node.getAttribute(fullname); - } - - return this.each( - (value == null - ? fullname.local - ? attrRemoveNS - : attrRemove - : typeof value === 'function' - ? fullname.local - ? attrFunctionNS - : attrFunction - : fullname.local - ? attrConstantNS - : attrConstant)(fullname, value), - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/window.js - - /* harmony default export */ function src_window(node) { - return ( - (node.ownerDocument && node.ownerDocument.defaultView) || // node is a Node - (node.document && node) || // node is a Window - node.defaultView - ); // node is a Document - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/style.js - - function styleRemove(name) { - return function () { - this.style.removeProperty(name); - }; - } - - function styleConstant(name, value, priority) { - return function () { - this.style.setProperty(name, value, priority); - }; - } - - function styleFunction(name, value, priority) { - return function () { - var v = value.apply(this, arguments); - if (v == null) this.style.removeProperty(name); - else this.style.setProperty(name, v, priority); - }; - } - - /* harmony default export */ function style(name, value, priority) { - return arguments.length > 1 - ? this.each( - (value == null - ? styleRemove - : typeof value === 'function' - ? styleFunction - : styleConstant)(name, value, priority == null ? '' : priority), - ) - : styleValue(this.node(), name); - } - - function styleValue(node, name) { - return ( - node.style.getPropertyValue(name) || - src_window(node).getComputedStyle(node, null).getPropertyValue(name) - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/property.js - - function propertyRemove(name) { - return function () { - delete this[name]; - }; - } - - function propertyConstant(name, value) { - return function () { - this[name] = value; - }; - } - - function propertyFunction(name, value) { - return function () { - var v = value.apply(this, arguments); - if (v == null) delete this[name]; - else this[name] = v; - }; - } - - /* harmony default export */ function property(name, value) { - return arguments.length > 1 - ? this.each( - (value == null - ? propertyRemove - : typeof value === 'function' - ? propertyFunction - : propertyConstant)(name, value), - ) - : this.node()[name]; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/classed.js - - function classArray(string) { - return string.trim().split(/^|\s+/); - } - - function classList(node) { - return node.classList || new ClassList(node); - } - - function ClassList(node) { - this._node = node; - this._names = classArray(node.getAttribute('class') || ''); - } - - ClassList.prototype = { - add: function (name) { - var i = this._names.indexOf(name); - if (i < 0) { - this._names.push(name); - this._node.setAttribute('class', this._names.join(' ')); - } - }, - remove: function (name) { - var i = this._names.indexOf(name); - if (i >= 0) { - this._names.splice(i, 1); - this._node.setAttribute('class', this._names.join(' ')); - } - }, - contains: function (name) { - return this._names.indexOf(name) >= 0; - }, - }; - - function classedAdd(node, names) { - var list = classList(node), - i = -1, - n = names.length; - while (++i < n) list.add(names[i]); - } - - function classedRemove(node, names) { - var list = classList(node), - i = -1, - n = names.length; - while (++i < n) list.remove(names[i]); - } - - function classedTrue(names) { - return function () { - classedAdd(this, names); - }; - } - - function classedFalse(names) { - return function () { - classedRemove(this, names); - }; - } - - function classedFunction(names, value) { - return function () { - (value.apply(this, arguments) ? classedAdd : classedRemove)( - this, - names, - ); - }; - } - - /* harmony default export */ function classed(name, value) { - var names = classArray(name + ''); - - if (arguments.length < 2) { - var list = classList(this.node()), - i = -1, - n = names.length; - while (++i < n) if (!list.contains(names[i])) return false; - return true; - } - - return this.each( - (typeof value === 'function' - ? classedFunction - : value - ? classedTrue - : classedFalse)(names, value), - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/text.js - - function textRemove() { - this.textContent = ''; - } - - function textConstant(value) { - return function () { - this.textContent = value; - }; - } - - function textFunction(value) { - return function () { - var v = value.apply(this, arguments); - this.textContent = v == null ? '' : v; - }; - } - - /* harmony default export */ function selection_text(value) { - return arguments.length - ? this.each( - value == null - ? textRemove - : (typeof value === 'function' ? textFunction : textConstant)( - value, - ), - ) - : this.node().textContent; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/html.js - - function htmlRemove() { - this.innerHTML = ''; - } - - function htmlConstant(value) { - return function () { - this.innerHTML = value; - }; - } - - function htmlFunction(value) { - return function () { - var v = value.apply(this, arguments); - this.innerHTML = v == null ? '' : v; - }; - } - - /* harmony default export */ function html(value) { - return arguments.length - ? this.each( - value == null - ? htmlRemove - : (typeof value === 'function' ? htmlFunction : htmlConstant)( - value, - ), - ) - : this.node().innerHTML; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/raise.js - - function raise() { - if (this.nextSibling) this.parentNode.appendChild(this); - } - - /* harmony default export */ function selection_raise() { - return this.each(raise); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/lower.js - - function lower() { - if (this.previousSibling) - this.parentNode.insertBefore(this, this.parentNode.firstChild); - } - - /* harmony default export */ function selection_lower() { - return this.each(lower); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/creator.js - - function creatorInherit(name) { - return function () { - var document = this.ownerDocument, - uri = this.namespaceURI; - return uri === xhtml && document.documentElement.namespaceURI === xhtml - ? document.createElement(name) - : document.createElementNS(uri, name); - }; - } - - function creatorFixed(fullname) { - return function () { - return this.ownerDocument.createElementNS( - fullname.space, - fullname.local, - ); - }; - } - - /* harmony default export */ function creator(name) { - var fullname = namespace(name); - return (fullname.local ? creatorFixed : creatorInherit)(fullname); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/append.js - - /* harmony default export */ function append(name) { - var create = typeof name === 'function' ? name : creator(name); - return this.select(function () { - return this.appendChild(create.apply(this, arguments)); - }); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/insert.js - - function constantNull() { - return null; - } - - /* harmony default export */ function insert(name, before) { - var create = typeof name === 'function' ? name : creator(name), - select = - before == null - ? constantNull - : typeof before === 'function' - ? before - : selector(before); - return this.select(function () { - return this.insertBefore( - create.apply(this, arguments), - select.apply(this, arguments) || null, - ); - }); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/remove.js - - function remove() { - var parent = this.parentNode; - if (parent) parent.removeChild(this); - } - - /* harmony default export */ function selection_remove() { - return this.each(remove); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/clone.js - - function selection_cloneShallow() { - var clone = this.cloneNode(false), - parent = this.parentNode; - return parent ? parent.insertBefore(clone, this.nextSibling) : clone; - } - - function selection_cloneDeep() { - var clone = this.cloneNode(true), - parent = this.parentNode; - return parent ? parent.insertBefore(clone, this.nextSibling) : clone; - } - - /* harmony default export */ function clone(deep) { - return this.select(deep ? selection_cloneDeep : selection_cloneShallow); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/datum.js - - /* harmony default export */ function selection_datum(value) { - return arguments.length - ? this.property('__data__', value) - : this.node().__data__; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/on.js - - function contextListener(listener) { - return function (event) { - listener.call(this, event, this.__data__); - }; - } - - function parseTypenames(typenames) { - return typenames - .trim() - .split(/^|\s+/) - .map(function (t) { - var name = '', - i = t.indexOf('.'); - if (i >= 0) (name = t.slice(i + 1)), (t = t.slice(0, i)); - return { type: t, name: name }; - }); - } - - function onRemove(typename) { - return function () { - var on = this.__on; - if (!on) return; - for (var j = 0, i = -1, m = on.length, o; j < m; ++j) { - if ( - ((o = on[j]), - (!typename.type || o.type === typename.type) && - o.name === typename.name) - ) { - this.removeEventListener(o.type, o.listener, o.options); - } else { - on[++i] = o; - } - } - if (++i) on.length = i; - else delete this.__on; - }; - } - - function onAdd(typename, value, options) { - return function () { - var on = this.__on, - o, - listener = contextListener(value); - if (on) - for (var j = 0, m = on.length; j < m; ++j) { - if ( - (o = on[j]).type === typename.type && - o.name === typename.name - ) { - this.removeEventListener(o.type, o.listener, o.options); - this.addEventListener( - o.type, - (o.listener = listener), - (o.options = options), - ); - o.value = value; - return; - } - } - this.addEventListener(typename.type, listener, options); - o = { - type: typename.type, - name: typename.name, - value: value, - listener: listener, - options: options, - }; - if (!on) this.__on = [o]; - else on.push(o); - }; - } - - /* harmony default export */ function on(typename, value, options) { - var typenames = parseTypenames(typename + ''), - i, - n = typenames.length, - t; - - if (arguments.length < 2) { - var on = this.node().__on; - if (on) - for (var j = 0, m = on.length, o; j < m; ++j) { - for (i = 0, o = on[j]; i < n; ++i) { - if ((t = typenames[i]).type === o.type && t.name === o.name) { - return o.value; - } - } - } - return; - } - - on = value ? onAdd : onRemove; - for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options)); - return this; - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/dispatch.js - - function dispatchEvent(node, type, params) { - var window = src_window(node), - event = window.CustomEvent; - - if (typeof event === 'function') { - event = new event(type, params); - } else { - event = window.document.createEvent('Event'); - if (params) - event.initEvent(type, params.bubbles, params.cancelable), - (event.detail = params.detail); - else event.initEvent(type, false, false); - } - - node.dispatchEvent(event); - } - - function dispatchConstant(type, params) { - return function () { - return dispatchEvent(this, type, params); - }; - } - - function dispatchFunction(type, params) { - return function () { - return dispatchEvent(this, type, params.apply(this, arguments)); - }; - } - - /* harmony default export */ function dispatch(type, params) { - return this.each( - (typeof params === 'function' ? dispatchFunction : dispatchConstant)( - type, - params, - ), - ); - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/iterator.js - - /* harmony default export */ function* iterator() { - for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { - for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) { - if ((node = group[i])) yield node; - } - } - } // CONCATENATED MODULE: ../node_modules/d3-selection/src/selection/index.js - - var root = [null]; - - function Selection(groups, parents) { - this._groups = groups; - this._parents = parents; - } - - function selection() { - return new Selection([[document.documentElement]], root); - } - - function selection_selection() { - return this; - } - - Selection.prototype = selection.prototype = { - constructor: Selection, - select: selection_select, - selectAll: selectAll, - selectChild: selectChild, - selectChildren: selectChildren, - filter: selection_filter, - data: data, - enter: enter, - exit: exit, - join: join, - merge: merge, - selection: selection_selection, - order: order, - sort: sort, - call: call, - nodes: nodes, - node: node, - size: size, - empty: selection_empty, - each: each, - attr: attr, - style: style, - property: property, - classed: classed, - text: selection_text, - html: html, - raise: selection_raise, - lower: selection_lower, - append: append, - insert: insert, - remove: selection_remove, - clone: clone, - datum: selection_datum, - on: on, - dispatch: dispatch, - [Symbol.iterator]: iterator, - }; - - /* harmony default export */ const src_selection = selection; // CONCATENATED MODULE: ../node_modules/d3-selection/src/select.js - - /* harmony default export */ function src_select(selector) { - return typeof selector === 'string' - ? new Selection( - [[document.querySelector(selector)]], - [document.documentElement], - ) - : new Selection([[selector]], root); - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatDecimal.js - - /* harmony default export */ function formatDecimal(x) { - return Math.abs((x = Math.round(x))) >= 1e21 - ? x.toLocaleString('en').replace(/,/g, '') - : x.toString(10); - } - - // Computes the decimal coefficient and exponent of the specified number x with - // significant digits p, where x is positive and p is in [1, 21] or undefined. - // For example, formatDecimalParts(1.23) returns ["123", 0]. - function formatDecimalParts(x, p) { - if ( - (i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf( - 'e', - )) < 0 - ) - return null; // NaN, ±Infinity - var i, - coefficient = x.slice(0, i); - - // The string returned by toExponential either has the form \d\.\d+e[-+]\d+ - // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3). - return [ - coefficient.length > 1 - ? coefficient[0] + coefficient.slice(2) - : coefficient, - +x.slice(i + 1), - ]; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/exponent.js - - /* harmony default export */ function exponent(x) { - return (x = formatDecimalParts(Math.abs(x))), x ? x[1] : NaN; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatGroup.js - - /* harmony default export */ function formatGroup(grouping, thousands) { - return function (value, width) { - var i = value.length, - t = [], - j = 0, - g = grouping[0], - length = 0; - - while (i > 0 && g > 0) { - if (length + g + 1 > width) g = Math.max(1, width - length); - t.push(value.substring((i -= g), i + g)); - if ((length += g + 1) > width) break; - g = grouping[(j = (j + 1) % grouping.length)]; - } - - return t.reverse().join(thousands); - }; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatNumerals.js - - /* harmony default export */ function formatNumerals(numerals) { - return function (value) { - return value.replace(/[0-9]/g, function (i) { - return numerals[+i]; - }); - }; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatSpecifier.js - - // [[fill]align][sign][symbol][0][width][,][.precision][~][type] - var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i; - - function formatSpecifier(specifier) { - if (!(match = re.exec(specifier))) - throw new Error('invalid format: ' + specifier); - var match; - return new FormatSpecifier({ - fill: match[1], - align: match[2], - sign: match[3], - symbol: match[4], - zero: match[5], - width: match[6], - comma: match[7], - precision: match[8] && match[8].slice(1), - trim: match[9], - type: match[10], - }); - } - - formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof - - function FormatSpecifier(specifier) { - this.fill = specifier.fill === undefined ? ' ' : specifier.fill + ''; - this.align = specifier.align === undefined ? '>' : specifier.align + ''; - this.sign = specifier.sign === undefined ? '-' : specifier.sign + ''; - this.symbol = specifier.symbol === undefined ? '' : specifier.symbol + ''; - this.zero = !!specifier.zero; - this.width = specifier.width === undefined ? undefined : +specifier.width; - this.comma = !!specifier.comma; - this.precision = - specifier.precision === undefined ? undefined : +specifier.precision; - this.trim = !!specifier.trim; - this.type = specifier.type === undefined ? '' : specifier.type + ''; - } - - FormatSpecifier.prototype.toString = function () { - return ( - this.fill + - this.align + - this.sign + - this.symbol + - (this.zero ? '0' : '') + - (this.width === undefined ? '' : Math.max(1, this.width | 0)) + - (this.comma ? ',' : '') + - (this.precision === undefined - ? '' - : '.' + Math.max(0, this.precision | 0)) + - (this.trim ? '~' : '') + - this.type - ); - }; // CONCATENATED MODULE: ../node_modules/d3-format/src/formatTrim.js - - // Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k. - /* harmony default export */ function formatTrim(s) { - out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) { - switch (s[i]) { - case '.': - i0 = i1 = i; - break; - case '0': - if (i0 === 0) i0 = i; - i1 = i; - break; - default: - if (!+s[i]) break out; - if (i0 > 0) i0 = 0; - break; - } - } - return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatPrefixAuto.js - - var prefixExponent; - - /* harmony default export */ function formatPrefixAuto(x, p) { - var d = formatDecimalParts(x, p); - if (!d) return x + ''; - var coefficient = d[0], - exponent = d[1], - i = - exponent - - (prefixExponent = - Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + - 1, - n = coefficient.length; - return i === n - ? coefficient - : i > n - ? coefficient + new Array(i - n + 1).join('0') - : i > 0 - ? coefficient.slice(0, i) + '.' + coefficient.slice(i) - : '0.' + - new Array(1 - i).join('0') + - formatDecimalParts(x, Math.max(0, p + i - 1))[0]; // less than 1y! - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatRounded.js - - /* harmony default export */ function formatRounded(x, p) { - var d = formatDecimalParts(x, p); - if (!d) return x + ''; - var coefficient = d[0], - exponent = d[1]; - return exponent < 0 - ? '0.' + new Array(-exponent).join('0') + coefficient - : coefficient.length > exponent + 1 - ? coefficient.slice(0, exponent + 1) + - '.' + - coefficient.slice(exponent + 1) - : coefficient + new Array(exponent - coefficient.length + 2).join('0'); - } // CONCATENATED MODULE: ../node_modules/d3-format/src/formatTypes.js - - /* harmony default export */ const formatTypes = { - '%': (x, p) => (x * 100).toFixed(p), - b: (x) => Math.round(x).toString(2), - c: (x) => x + '', - d: formatDecimal, - e: (x, p) => x.toExponential(p), - f: (x, p) => x.toFixed(p), - g: (x, p) => x.toPrecision(p), - o: (x) => Math.round(x).toString(8), - p: (x, p) => formatRounded(x * 100, p), - r: formatRounded, - s: formatPrefixAuto, - X: (x) => Math.round(x).toString(16).toUpperCase(), - x: (x) => Math.round(x).toString(16), - }; // CONCATENATED MODULE: ../node_modules/d3-format/src/identity.js - - /* harmony default export */ function identity(x) { - return x; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/locale.js - - var map = Array.prototype.map, - prefixes = [ - 'y', - 'z', - 'a', - 'f', - 'p', - 'n', - 'µ', - 'm', - '', - 'k', - 'M', - 'G', - 'T', - 'P', - 'E', - 'Z', - 'Y', - ]; - - /* harmony default export */ function locale(locale) { - var group = - locale.grouping === undefined || locale.thousands === undefined - ? identity - : formatGroup( - map.call(locale.grouping, Number), - locale.thousands + '', - ), - currencyPrefix = - locale.currency === undefined ? '' : locale.currency[0] + '', - currencySuffix = - locale.currency === undefined ? '' : locale.currency[1] + '', - decimal = locale.decimal === undefined ? '.' : locale.decimal + '', - numerals = - locale.numerals === undefined - ? identity - : formatNumerals(map.call(locale.numerals, String)), - percent = locale.percent === undefined ? '%' : locale.percent + '', - minus = locale.minus === undefined ? '−' : locale.minus + '', - nan = locale.nan === undefined ? 'NaN' : locale.nan + ''; - - function newFormat(specifier) { - specifier = formatSpecifier(specifier); - - var fill = specifier.fill, - align = specifier.align, - sign = specifier.sign, - symbol = specifier.symbol, - zero = specifier.zero, - width = specifier.width, - comma = specifier.comma, - precision = specifier.precision, - trim = specifier.trim, - type = specifier.type; - - // The "n" type is an alias for ",g". - if (type === 'n') (comma = true), (type = 'g'); - // The "" type, and any invalid type, is an alias for ".12~g". - else if (!formatTypes[type]) - precision === undefined && (precision = 12), - (trim = true), - (type = 'g'); - - // If zero fill is specified, padding goes after sign and before digits. - if (zero || (fill === '0' && align === '=')) - (zero = true), (fill = '0'), (align = '='); - - // Compute the prefix and suffix. - // For SI-prefix, the suffix is lazily computed. - var prefix = - symbol === '$' - ? currencyPrefix - : symbol === '#' && /[boxX]/.test(type) - ? '0' + type.toLowerCase() - : '', - suffix = - symbol === '$' ? currencySuffix : /[%p]/.test(type) ? percent : ''; - - // What format function should we use? - // Is this an integer type? - // Can this type generate exponential notation? - var formatType = formatTypes[type], - maybeSuffix = /[defgprs%]/.test(type); - - // Set the default precision if not specified, - // or clamp the specified precision to the supported range. - // For significant precision, it must be in [1, 21]. - // For fixed precision, it must be in [0, 20]. - precision = - precision === undefined - ? 6 - : /[gprs]/.test(type) - ? Math.max(1, Math.min(21, precision)) - : Math.max(0, Math.min(20, precision)); - - function format(value) { - var valuePrefix = prefix, - valueSuffix = suffix, - i, - n, - c; - - if (type === 'c') { - valueSuffix = formatType(value) + valueSuffix; - value = ''; - } else { - value = +value; - - // Determine the sign. -0 is not less than 0, but 1 / -0 is! - var valueNegative = value < 0 || 1 / value < 0; - - // Perform the initial formatting. - value = isNaN(value) ? nan : formatType(Math.abs(value), precision); - - // Trim insignificant zeros. - if (trim) value = formatTrim(value); - - // If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign. - if (valueNegative && +value === 0 && sign !== '+') - valueNegative = false; - - // Compute the prefix and suffix. - valuePrefix = - (valueNegative - ? sign === '(' - ? sign - : minus - : sign === '-' || sign === '(' - ? '' - : sign) + valuePrefix; - valueSuffix = - (type === 's' ? prefixes[8 + prefixExponent / 3] : '') + - valueSuffix + - (valueNegative && sign === '(' ? ')' : ''); - - // Break the formatted value into the integer “value” part that can be - // grouped, and fractional or exponential “suffix” part that is not. - if (maybeSuffix) { - (i = -1), (n = value.length); - while (++i < n) { - if (((c = value.charCodeAt(i)), 48 > c || c > 57)) { - valueSuffix = - (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + - valueSuffix; - value = value.slice(0, i); - break; - } - } - } - } - - // If the fill character is not "0", grouping is applied before padding. - if (comma && !zero) value = group(value, Infinity); - - // Compute the padding. - var length = valuePrefix.length + value.length + valueSuffix.length, - padding = - length < width ? new Array(width - length + 1).join(fill) : ''; - - // If the fill character is "0", grouping is applied after padding. - if (comma && zero) - (value = group( - padding + value, - padding.length ? width - valueSuffix.length : Infinity, - )), - (padding = ''); - - // Reconstruct the final output based on the desired alignment. - switch (align) { - case '<': - value = valuePrefix + value + valueSuffix + padding; - break; - case '=': - value = valuePrefix + padding + value + valueSuffix; - break; - case '^': - value = - padding.slice(0, (length = padding.length >> 1)) + - valuePrefix + - value + - valueSuffix + - padding.slice(length); - break; - default: - value = padding + valuePrefix + value + valueSuffix; - break; - } - - return numerals(value); - } - - format.toString = function () { - return specifier + ''; - }; - - return format; - } - - function formatPrefix(specifier, value) { - var f = newFormat( - ((specifier = formatSpecifier(specifier)), - (specifier.type = 'f'), - specifier), - ), - e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3, - k = Math.pow(10, -e), - prefix = prefixes[8 + e / 3]; - return function (value) { - return f(k * value) + prefix; - }; - } - - return { - format: newFormat, - formatPrefix: formatPrefix, - }; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/defaultLocale.js - - var defaultLocale_locale; - var format; - var formatPrefix; - - defaultLocale({ - thousands: ',', - grouping: [3], - currency: ['$', ''], - }); - - function defaultLocale(definition) { - defaultLocale_locale = locale(definition); - format = defaultLocale_locale.format; - formatPrefix = defaultLocale_locale.formatPrefix; - return defaultLocale_locale; - } // CONCATENATED MODULE: ../node_modules/d3-array/src/ascending.js - - function ascending_ascending(a, b) { - return a == null || b == null - ? NaN - : a < b - ? -1 - : a > b - ? 1 - : a >= b - ? 0 - : NaN; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/treemap/round.js - - /* harmony default export */ function treemap_round(node) { - node.x0 = Math.round(node.x0); - node.y0 = Math.round(node.y0); - node.x1 = Math.round(node.x1); - node.y1 = Math.round(node.y1); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/treemap/dice.js - - /* harmony default export */ function dice(parent, x0, y0, x1, y1) { - var nodes = parent.children, - node, - i = -1, - n = nodes.length, - k = parent.value && (x1 - x0) / parent.value; - - while (++i < n) { - (node = nodes[i]), (node.y0 = y0), (node.y1 = y1); - (node.x0 = x0), (node.x1 = x0 += node.value * k); - } - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/partition.js - - /* harmony default export */ function partition() { - var dx = 1, - dy = 1, - padding = 0, - round = false; - - function partition(root) { - var n = root.height + 1; - root.x0 = root.y0 = padding; - root.x1 = dx; - root.y1 = dy / n; - root.eachBefore(positionNode(dy, n)); - if (round) root.eachBefore(treemap_round); - return root; - } - - function positionNode(dy, n) { - return function (node) { - if (node.children) { - dice( - node, - node.x0, - (dy * (node.depth + 1)) / n, - node.x1, - (dy * (node.depth + 2)) / n, - ); - } - var x0 = node.x0, - y0 = node.y0, - x1 = node.x1 - padding, - y1 = node.y1 - padding; - if (x1 < x0) x0 = x1 = (x0 + x1) / 2; - if (y1 < y0) y0 = y1 = (y0 + y1) / 2; - node.x0 = x0; - node.y0 = y0; - node.x1 = x1; - node.y1 = y1; - }; - } - - partition.round = function (x) { - return arguments.length ? ((round = !!x), partition) : round; - }; - - partition.size = function (x) { - return arguments.length - ? ((dx = +x[0]), (dy = +x[1]), partition) - : [dx, dy]; - }; - - partition.padding = function (x) { - return arguments.length ? ((padding = +x), partition) : padding; - }; - - return partition; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/count.js - - function count(node) { - var sum = 0, - children = node.children, - i = children && children.length; - if (!i) sum = 1; - else while (--i >= 0) sum += children[i].value; - node.value = sum; - } - - /* harmony default export */ function hierarchy_count() { - return this.eachAfter(count); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/each.js - - /* harmony default export */ function hierarchy_each(callback, that) { - let index = -1; - for (const node of this) { - callback.call(that, node, ++index, this); - } - return this; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/eachBefore.js - - /* harmony default export */ function eachBefore(callback, that) { - var node = this, - nodes = [node], - children, - i, - index = -1; - while ((node = nodes.pop())) { - callback.call(that, node, ++index, this); - if ((children = node.children)) { - for (i = children.length - 1; i >= 0; --i) { - nodes.push(children[i]); - } - } - } - return this; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/eachAfter.js - - /* harmony default export */ function eachAfter(callback, that) { - var node = this, - nodes = [node], - next = [], - children, - i, - n, - index = -1; - while ((node = nodes.pop())) { - next.push(node); - if ((children = node.children)) { - for (i = 0, n = children.length; i < n; ++i) { - nodes.push(children[i]); - } - } - } - while ((node = next.pop())) { - callback.call(that, node, ++index, this); - } - return this; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/find.js - - /* harmony default export */ function hierarchy_find(callback, that) { - let index = -1; - for (const node of this) { - if (callback.call(that, node, ++index, this)) { - return node; - } - } - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/sum.js - - /* harmony default export */ function sum(value) { - return this.eachAfter(function (node) { - var sum = +value(node.data) || 0, - children = node.children, - i = children && children.length; - while (--i >= 0) sum += children[i].value; - node.value = sum; - }); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/sort.js - - /* harmony default export */ function hierarchy_sort(compare) { - return this.eachBefore(function (node) { - if (node.children) { - node.children.sort(compare); - } - }); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/path.js - - /* harmony default export */ function path(end) { - var start = this, - ancestor = leastCommonAncestor(start, end), - nodes = [start]; - while (start !== ancestor) { - start = start.parent; - nodes.push(start); - } - var k = nodes.length; - while (end !== ancestor) { - nodes.splice(k, 0, end); - end = end.parent; - } - return nodes; - } - - function leastCommonAncestor(a, b) { - if (a === b) return a; - var aNodes = a.ancestors(), - bNodes = b.ancestors(), - c = null; - a = aNodes.pop(); - b = bNodes.pop(); - while (a === b) { - c = a; - a = aNodes.pop(); - b = bNodes.pop(); - } - return c; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/ancestors.js - - /* harmony default export */ function ancestors() { - var node = this, - nodes = [node]; - while ((node = node.parent)) { - nodes.push(node); - } - return nodes; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/descendants.js - - /* harmony default export */ function descendants() { - return Array.from(this); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/leaves.js - - /* harmony default export */ function leaves() { - var leaves = []; - this.eachBefore(function (node) { - if (!node.children) { - leaves.push(node); - } - }); - return leaves; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/links.js - - /* harmony default export */ function links() { - var root = this, - links = []; - root.each(function (node) { - if (node !== root) { - // Don’t include the root’s parent, if any. - links.push({ source: node.parent, target: node }); - } - }); - return links; - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/iterator.js - - /* harmony default export */ function* hierarchy_iterator() { - var node = this, - current, - next = [node], - children, - i, - n; - do { - (current = next.reverse()), (next = []); - while ((node = current.pop())) { - yield node; - if ((children = node.children)) { - for (i = 0, n = children.length; i < n; ++i) { - next.push(children[i]); - } - } - } - } while (next.length); - } // CONCATENATED MODULE: ../node_modules/d3-hierarchy/src/hierarchy/index.js - - function hierarchy(data, children) { - if (data instanceof Map) { - data = [undefined, data]; - if (children === undefined) children = mapChildren; - } else if (children === undefined) { - children = objectChildren; - } - - var root = new Node(data), - node, - nodes = [root], - child, - childs, - i, - n; - - while ((node = nodes.pop())) { - if ( - (childs = children(node.data)) && - (n = (childs = Array.from(childs)).length) - ) { - node.children = childs; - for (i = n - 1; i >= 0; --i) { - nodes.push((child = childs[i] = new Node(childs[i]))); - child.parent = node; - child.depth = node.depth + 1; - } - } - } - - return root.eachBefore(computeHeight); - } - - function node_copy() { - return hierarchy(this).eachBefore(copyData); - } - - function objectChildren(d) { - return d.children; - } - - function mapChildren(d) { - return Array.isArray(d) ? d[1] : null; - } - - function copyData(node) { - if (node.data.value !== undefined) node.value = node.data.value; - node.data = node.data.data; - } - - function computeHeight(node) { - var height = 0; - do node.height = height; - while ((node = node.parent) && node.height < ++height); - } - - function Node(data) { - this.data = data; - this.depth = this.height = 0; - this.parent = null; - } - - Node.prototype = hierarchy.prototype = { - constructor: Node, - count: hierarchy_count, - each: hierarchy_each, - eachAfter: eachAfter, - eachBefore: eachBefore, - find: hierarchy_find, - sum: sum, - sort: hierarchy_sort, - path: path, - ancestors: ancestors, - descendants: descendants, - leaves: leaves, - links: links, - copy: node_copy, - [Symbol.iterator]: hierarchy_iterator, - }; // CONCATENATED MODULE: ../node_modules/d3-array/src/ticks.js - - var e10 = Math.sqrt(50), - e5 = Math.sqrt(10), - e2 = Math.sqrt(2); - - function ticks(start, stop, count) { - var reverse, - i = -1, - n, - ticks, - step; - - (stop = +stop), (start = +start), (count = +count); - if (start === stop && count > 0) return [start]; - if ((reverse = stop < start)) (n = start), (start = stop), (stop = n); - if ((step = tickIncrement(start, stop, count)) === 0 || !isFinite(step)) - return []; - - if (step > 0) { - let r0 = Math.round(start / step), - r1 = Math.round(stop / step); - if (r0 * step < start) ++r0; - if (r1 * step > stop) --r1; - ticks = new Array((n = r1 - r0 + 1)); - while (++i < n) ticks[i] = (r0 + i) * step; - } else { - step = -step; - let r0 = Math.round(start * step), - r1 = Math.round(stop * step); - if (r0 / step < start) ++r0; - if (r1 / step > stop) --r1; - ticks = new Array((n = r1 - r0 + 1)); - while (++i < n) ticks[i] = (r0 + i) / step; - } - - if (reverse) ticks.reverse(); - - return ticks; - } - - function tickIncrement(start, stop, count) { - var step = (stop - start) / Math.max(0, count), - power = Math.floor(Math.log(step) / Math.LN10), - error = step / Math.pow(10, power); - return power >= 0 - ? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * - Math.pow(10, power) - : -Math.pow(10, -power) / - (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1); - } - - function tickStep(start, stop, count) { - var step0 = Math.abs(stop - start) / Math.max(0, count), - step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), - error = step0 / step1; - if (error >= e10) step1 *= 10; - else if (error >= e5) step1 *= 5; - else if (error >= e2) step1 *= 2; - return stop < start ? -step1 : step1; - } // CONCATENATED MODULE: ../node_modules/d3-array/src/bisector.js - - function bisector(f) { - let delta = f; - let compare1 = f; - let compare2 = f; - - if (f.length !== 2) { - delta = (d, x) => f(d) - x; - compare1 = ascending_ascending; - compare2 = (d, x) => ascending_ascending(f(d), x); - } - - function left(a, x, lo = 0, hi = a.length) { - if (lo < hi) { - if (compare1(x, x) !== 0) return hi; - do { - const mid = (lo + hi) >>> 1; - if (compare2(a[mid], x) < 0) lo = mid + 1; - else hi = mid; - } while (lo < hi); - } - return lo; - } - - function right(a, x, lo = 0, hi = a.length) { - if (lo < hi) { - if (compare1(x, x) !== 0) return hi; - do { - const mid = (lo + hi) >>> 1; - if (compare2(a[mid], x) <= 0) lo = mid + 1; - else hi = mid; - } while (lo < hi); - } - return lo; - } - - function center(a, x, lo = 0, hi = a.length) { - const i = left(a, x, lo, hi - 1); - return i > lo && delta(a[i - 1], x) > -delta(a[i], x) ? i - 1 : i; - } - - return { left, center, right }; - } // CONCATENATED MODULE: ../node_modules/d3-array/src/number.js - - function number(x) { - return x === null ? NaN : +x; - } - - function* numbers(values, valueof) { - if (valueof === undefined) { - for (let value of values) { - if (value != null && (value = +value) >= value) { - yield value; - } - } - } else { - let index = -1; - for (let value of values) { - if ( - (value = valueof(value, ++index, values)) != null && - (value = +value) >= value - ) { - yield value; - } - } - } - } // CONCATENATED MODULE: ../node_modules/d3-array/src/bisect.js - - const ascendingBisect = bisector(ascending_ascending); - const bisectRight = ascendingBisect.right; - const bisectLeft = ascendingBisect.left; - const bisectCenter = bisector(number).center; - /* harmony default export */ const bisect = bisectRight; // CONCATENATED MODULE: ../node_modules/d3-color/src/define.js - - /* harmony default export */ function src_define( - constructor, - factory, - prototype, - ) { - constructor.prototype = factory.prototype = prototype; - prototype.constructor = constructor; - } - - function extend(parent, definition) { - var prototype = Object.create(parent.prototype); - for (var key in definition) prototype[key] = definition[key]; - return prototype; - } // CONCATENATED MODULE: ../node_modules/d3-color/src/color.js - - function Color() {} - - var darker = 0.7; - var brighter = 1 / darker; - - var reI = '\\s*([+-]?\\d+)\\s*', - reN = '\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*', - reP = '\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*', - reHex = /^#([0-9a-f]{3,8})$/, - reRgbInteger = new RegExp('^rgb\\(' + [reI, reI, reI] + '\\)$'), - reRgbPercent = new RegExp('^rgb\\(' + [reP, reP, reP] + '\\)$'), - reRgbaInteger = new RegExp('^rgba\\(' + [reI, reI, reI, reN] + '\\)$'), - reRgbaPercent = new RegExp('^rgba\\(' + [reP, reP, reP, reN] + '\\)$'), - reHslPercent = new RegExp('^hsl\\(' + [reN, reP, reP] + '\\)$'), - reHslaPercent = new RegExp('^hsla\\(' + [reN, reP, reP, reN] + '\\)$'); - - var named = { - aliceblue: 0xf0f8ff, - antiquewhite: 0xfaebd7, - aqua: 0x00ffff, - aquamarine: 0x7fffd4, - azure: 0xf0ffff, - beige: 0xf5f5dc, - bisque: 0xffe4c4, - black: 0x000000, - blanchedalmond: 0xffebcd, - blue: 0x0000ff, - blueviolet: 0x8a2be2, - brown: 0xa52a2a, - burlywood: 0xdeb887, - cadetblue: 0x5f9ea0, - chartreuse: 0x7fff00, - chocolate: 0xd2691e, - coral: 0xff7f50, - cornflowerblue: 0x6495ed, - cornsilk: 0xfff8dc, - crimson: 0xdc143c, - cyan: 0x00ffff, - darkblue: 0x00008b, - darkcyan: 0x008b8b, - darkgoldenrod: 0xb8860b, - darkgray: 0xa9a9a9, - darkgreen: 0x006400, - darkgrey: 0xa9a9a9, - darkkhaki: 0xbdb76b, - darkmagenta: 0x8b008b, - darkolivegreen: 0x556b2f, - darkorange: 0xff8c00, - darkorchid: 0x9932cc, - darkred: 0x8b0000, - darksalmon: 0xe9967a, - darkseagreen: 0x8fbc8f, - darkslateblue: 0x483d8b, - darkslategray: 0x2f4f4f, - darkslategrey: 0x2f4f4f, - darkturquoise: 0x00ced1, - darkviolet: 0x9400d3, - deeppink: 0xff1493, - deepskyblue: 0x00bfff, - dimgray: 0x696969, - dimgrey: 0x696969, - dodgerblue: 0x1e90ff, - firebrick: 0xb22222, - floralwhite: 0xfffaf0, - forestgreen: 0x228b22, - fuchsia: 0xff00ff, - gainsboro: 0xdcdcdc, - ghostwhite: 0xf8f8ff, - gold: 0xffd700, - goldenrod: 0xdaa520, - gray: 0x808080, - green: 0x008000, - greenyellow: 0xadff2f, - grey: 0x808080, - honeydew: 0xf0fff0, - hotpink: 0xff69b4, - indianred: 0xcd5c5c, - indigo: 0x4b0082, - ivory: 0xfffff0, - khaki: 0xf0e68c, - lavender: 0xe6e6fa, - lavenderblush: 0xfff0f5, - lawngreen: 0x7cfc00, - lemonchiffon: 0xfffacd, - lightblue: 0xadd8e6, - lightcoral: 0xf08080, - lightcyan: 0xe0ffff, - lightgoldenrodyellow: 0xfafad2, - lightgray: 0xd3d3d3, - lightgreen: 0x90ee90, - lightgrey: 0xd3d3d3, - lightpink: 0xffb6c1, - lightsalmon: 0xffa07a, - lightseagreen: 0x20b2aa, - lightskyblue: 0x87cefa, - lightslategray: 0x778899, - lightslategrey: 0x778899, - lightsteelblue: 0xb0c4de, - lightyellow: 0xffffe0, - lime: 0x00ff00, - limegreen: 0x32cd32, - linen: 0xfaf0e6, - magenta: 0xff00ff, - maroon: 0x800000, - mediumaquamarine: 0x66cdaa, - mediumblue: 0x0000cd, - mediumorchid: 0xba55d3, - mediumpurple: 0x9370db, - mediumseagreen: 0x3cb371, - mediumslateblue: 0x7b68ee, - mediumspringgreen: 0x00fa9a, - mediumturquoise: 0x48d1cc, - mediumvioletred: 0xc71585, - midnightblue: 0x191970, - mintcream: 0xf5fffa, - mistyrose: 0xffe4e1, - moccasin: 0xffe4b5, - navajowhite: 0xffdead, - navy: 0x000080, - oldlace: 0xfdf5e6, - olive: 0x808000, - olivedrab: 0x6b8e23, - orange: 0xffa500, - orangered: 0xff4500, - orchid: 0xda70d6, - palegoldenrod: 0xeee8aa, - palegreen: 0x98fb98, - paleturquoise: 0xafeeee, - palevioletred: 0xdb7093, - papayawhip: 0xffefd5, - peachpuff: 0xffdab9, - peru: 0xcd853f, - pink: 0xffc0cb, - plum: 0xdda0dd, - powderblue: 0xb0e0e6, - purple: 0x800080, - rebeccapurple: 0x663399, - red: 0xff0000, - rosybrown: 0xbc8f8f, - royalblue: 0x4169e1, - saddlebrown: 0x8b4513, - salmon: 0xfa8072, - sandybrown: 0xf4a460, - seagreen: 0x2e8b57, - seashell: 0xfff5ee, - sienna: 0xa0522d, - silver: 0xc0c0c0, - skyblue: 0x87ceeb, - slateblue: 0x6a5acd, - slategray: 0x708090, - slategrey: 0x708090, - snow: 0xfffafa, - springgreen: 0x00ff7f, - steelblue: 0x4682b4, - tan: 0xd2b48c, - teal: 0x008080, - thistle: 0xd8bfd8, - tomato: 0xff6347, - turquoise: 0x40e0d0, - violet: 0xee82ee, - wheat: 0xf5deb3, - white: 0xffffff, - whitesmoke: 0xf5f5f5, - yellow: 0xffff00, - yellowgreen: 0x9acd32, - }; - - src_define(Color, color, { - copy: function (channels) { - return Object.assign(new this.constructor(), this, channels); - }, - displayable: function () { - return this.rgb().displayable(); - }, - hex: color_formatHex, // Deprecated! Use color.formatHex. - formatHex: color_formatHex, - formatHsl: color_formatHsl, - formatRgb: color_formatRgb, - toString: color_formatRgb, - }); - - function color_formatHex() { - return this.rgb().formatHex(); - } - - function color_formatHsl() { - return hslConvert(this).formatHsl(); - } - - function color_formatRgb() { - return this.rgb().formatRgb(); - } - - function color(format) { - var m, l; - format = (format + '').trim().toLowerCase(); - return (m = reHex.exec(format)) - ? ((l = m[1].length), - (m = parseInt(m[1], 16)), - l === 6 - ? rgbn(m) // #ff0000 - : l === 3 - ? new Rgb( - ((m >> 8) & 0xf) | ((m >> 4) & 0xf0), - ((m >> 4) & 0xf) | (m & 0xf0), - ((m & 0xf) << 4) | (m & 0xf), - 1, - ) // #f00 - : l === 8 - ? rgba( - (m >> 24) & 0xff, - (m >> 16) & 0xff, - (m >> 8) & 0xff, - (m & 0xff) / 0xff, - ) // #ff000000 - : l === 4 - ? rgba( - ((m >> 12) & 0xf) | ((m >> 8) & 0xf0), - ((m >> 8) & 0xf) | ((m >> 4) & 0xf0), - ((m >> 4) & 0xf) | (m & 0xf0), - (((m & 0xf) << 4) | (m & 0xf)) / 0xff, - ) // #f000 - : null) // invalid hex - : (m = reRgbInteger.exec(format)) - ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0) - : (m = reRgbPercent.exec(format)) - ? new Rgb((m[1] * 255) / 100, (m[2] * 255) / 100, (m[3] * 255) / 100, 1) // rgb(100%, 0%, 0%) - : (m = reRgbaInteger.exec(format)) - ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1) - : (m = reRgbaPercent.exec(format)) - ? rgba((m[1] * 255) / 100, (m[2] * 255) / 100, (m[3] * 255) / 100, m[4]) // rgb(100%, 0%, 0%, 1) - : (m = reHslPercent.exec(format)) - ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%) - : (m = reHslaPercent.exec(format)) - ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1) - : named.hasOwnProperty(format) - ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins - : format === 'transparent' - ? new Rgb(NaN, NaN, NaN, 0) - : null; - } - - function rgbn(n) { - return new Rgb((n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff, 1); - } - - function rgba(r, g, b, a) { - if (a <= 0) r = g = b = NaN; - return new Rgb(r, g, b, a); - } - - function rgbConvert(o) { - if (!(o instanceof Color)) o = color(o); - if (!o) return new Rgb(); - o = o.rgb(); - return new Rgb(o.r, o.g, o.b, o.opacity); - } - - function color_rgb(r, g, b, opacity) { - return arguments.length === 1 - ? rgbConvert(r) - : new Rgb(r, g, b, opacity == null ? 1 : opacity); - } - - function Rgb(r, g, b, opacity) { - this.r = +r; - this.g = +g; - this.b = +b; - this.opacity = +opacity; - } - - src_define( - Rgb, - color_rgb, - extend(Color, { - brighter: function (k) { - k = k == null ? brighter : Math.pow(brighter, k); - return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); - }, - darker: function (k) { - k = k == null ? darker : Math.pow(darker, k); - return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); - }, - rgb: function () { - return this; - }, - displayable: function () { - return ( - -0.5 <= this.r && - this.r < 255.5 && - -0.5 <= this.g && - this.g < 255.5 && - -0.5 <= this.b && - this.b < 255.5 && - 0 <= this.opacity && - this.opacity <= 1 - ); - }, - hex: rgb_formatHex, // Deprecated! Use color.formatHex. - formatHex: rgb_formatHex, - formatRgb: rgb_formatRgb, - toString: rgb_formatRgb, - }), - ); - - function rgb_formatHex() { - return '#' + hex(this.r) + hex(this.g) + hex(this.b); - } - - function rgb_formatRgb() { - var a = this.opacity; - a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); - return ( - (a === 1 ? 'rgb(' : 'rgba(') + - Math.max(0, Math.min(255, Math.round(this.r) || 0)) + - ', ' + - Math.max(0, Math.min(255, Math.round(this.g) || 0)) + - ', ' + - Math.max(0, Math.min(255, Math.round(this.b) || 0)) + - (a === 1 ? ')' : ', ' + a + ')') - ); - } - - function hex(value) { - value = Math.max(0, Math.min(255, Math.round(value) || 0)); - return (value < 16 ? '0' : '') + value.toString(16); - } - - function hsla(h, s, l, a) { - if (a <= 0) h = s = l = NaN; - else if (l <= 0 || l >= 1) h = s = NaN; - else if (s <= 0) h = NaN; - return new Hsl(h, s, l, a); - } - - function hslConvert(o) { - if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity); - if (!(o instanceof Color)) o = color(o); - if (!o) return new Hsl(); - if (o instanceof Hsl) return o; - o = o.rgb(); - var r = o.r / 255, - g = o.g / 255, - b = o.b / 255, - min = Math.min(r, g, b), - max = Math.max(r, g, b), - h = NaN, - s = max - min, - l = (max + min) / 2; - if (s) { - if (r === max) h = (g - b) / s + (g < b) * 6; - else if (g === max) h = (b - r) / s + 2; - else h = (r - g) / s + 4; - s /= l < 0.5 ? max + min : 2 - max - min; - h *= 60; - } else { - s = l > 0 && l < 1 ? 0 : h; - } - return new Hsl(h, s, l, o.opacity); - } - - function hsl(h, s, l, opacity) { - return arguments.length === 1 - ? hslConvert(h) - : new Hsl(h, s, l, opacity == null ? 1 : opacity); - } - - function Hsl(h, s, l, opacity) { - this.h = +h; - this.s = +s; - this.l = +l; - this.opacity = +opacity; - } - - src_define( - Hsl, - hsl, - extend(Color, { - brighter: function (k) { - k = k == null ? brighter : Math.pow(brighter, k); - return new Hsl(this.h, this.s, this.l * k, this.opacity); - }, - darker: function (k) { - k = k == null ? darker : Math.pow(darker, k); - return new Hsl(this.h, this.s, this.l * k, this.opacity); - }, - rgb: function () { - var h = (this.h % 360) + (this.h < 0) * 360, - s = isNaN(h) || isNaN(this.s) ? 0 : this.s, - l = this.l, - m2 = l + (l < 0.5 ? l : 1 - l) * s, - m1 = 2 * l - m2; - return new Rgb( - hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2), - hsl2rgb(h, m1, m2), - hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2), - this.opacity, - ); - }, - displayable: function () { - return ( - ((0 <= this.s && this.s <= 1) || isNaN(this.s)) && - 0 <= this.l && - this.l <= 1 && - 0 <= this.opacity && - this.opacity <= 1 - ); - }, - formatHsl: function () { - var a = this.opacity; - a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); - return ( - (a === 1 ? 'hsl(' : 'hsla(') + - (this.h || 0) + - ', ' + - (this.s || 0) * 100 + - '%, ' + - (this.l || 0) * 100 + - '%' + - (a === 1 ? ')' : ', ' + a + ')') - ); - }, - }), - ); - - /* From FvD 13.37, CSS Color Module Level 3 */ - function hsl2rgb(h, m1, m2) { - return ( - (h < 60 - ? m1 + ((m2 - m1) * h) / 60 - : h < 180 - ? m2 - : h < 240 - ? m1 + ((m2 - m1) * (240 - h)) / 60 - : m1) * 255 - ); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/basis.js - - function basis(t1, v0, v1, v2, v3) { - var t2 = t1 * t1, - t3 = t2 * t1; - return ( - ((1 - 3 * t1 + 3 * t2 - t3) * v0 + - (4 - 6 * t2 + 3 * t3) * v1 + - (1 + 3 * t1 + 3 * t2 - 3 * t3) * v2 + - t3 * v3) / - 6 - ); - } - - /* harmony default export */ function src_basis(values) { - var n = values.length - 1; - return function (t) { - var i = - t <= 0 ? (t = 0) : t >= 1 ? ((t = 1), n - 1) : Math.floor(t * n), - v1 = values[i], - v2 = values[i + 1], - v0 = i > 0 ? values[i - 1] : 2 * v1 - v2, - v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1; - return basis((t - i / n) * n, v0, v1, v2, v3); - }; - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/basisClosed.js - - /* harmony default export */ function basisClosed(values) { - var n = values.length; - return function (t) { - var i = Math.floor(((t %= 1) < 0 ? ++t : t) * n), - v0 = values[(i + n - 1) % n], - v1 = values[i % n], - v2 = values[(i + 1) % n], - v3 = values[(i + 2) % n]; - return basis((t - i / n) * n, v0, v1, v2, v3); - }; - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/constant.js - - /* harmony default export */ const d3_interpolate_src_constant = ( - x, - ) => () => x; // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/color.js - - function linear(a, d) { - return function (t) { - return a + t * d; - }; - } - - function exponential(a, b, y) { - return ( - (a = Math.pow(a, y)), - (b = Math.pow(b, y) - a), - (y = 1 / y), - function (t) { - return Math.pow(a + t * b, y); - } - ); - } - - function hue(a, b) { - var d = b - a; - return d - ? linear(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) - : constant(isNaN(a) ? b : a); - } - - function gamma(y) { - return (y = +y) === 1 - ? nogamma - : function (a, b) { - return b - a - ? exponential(a, b, y) - : d3_interpolate_src_constant(isNaN(a) ? b : a); - }; - } - - function nogamma(a, b) { - var d = b - a; - return d ? linear(a, d) : d3_interpolate_src_constant(isNaN(a) ? b : a); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/rgb.js - - /* harmony default export */ const rgb = (function rgbGamma(y) { - var color = gamma(y); - - function rgb(start, end) { - var r = color((start = color_rgb(start)).r, (end = color_rgb(end)).r), - g = color(start.g, end.g), - b = color(start.b, end.b), - opacity = nogamma(start.opacity, end.opacity); - return function (t) { - start.r = r(t); - start.g = g(t); - start.b = b(t); - start.opacity = opacity(t); - return start + ''; - }; - } - - rgb.gamma = rgbGamma; - - return rgb; - })(1); - - function rgbSpline(spline) { - return function (colors) { - var n = colors.length, - r = new Array(n), - g = new Array(n), - b = new Array(n), - i, - color; - for (i = 0; i < n; ++i) { - color = color_rgb(colors[i]); - r[i] = color.r || 0; - g[i] = color.g || 0; - b[i] = color.b || 0; - } - r = spline(r); - g = spline(g); - b = spline(b); - color.opacity = 1; - return function (t) { - color.r = r(t); - color.g = g(t); - color.b = b(t); - return color + ''; - }; - }; - } - - var rgbBasis = rgbSpline(src_basis); - var rgbBasisClosed = rgbSpline(basisClosed); // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/array.js - - /* harmony default export */ function src_array(a, b) { - return (isNumberArray(b) ? numberArray : genericArray)(a, b); - } - - function genericArray(a, b) { - var nb = b ? b.length : 0, - na = a ? Math.min(nb, a.length) : 0, - x = new Array(na), - c = new Array(nb), - i; - - for (i = 0; i < na; ++i) x[i] = value(a[i], b[i]); - for (; i < nb; ++i) c[i] = b[i]; - - return function (t) { - for (i = 0; i < na; ++i) c[i] = x[i](t); - return c; - }; - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/date.js - - /* harmony default export */ function date(a, b) { - var d = new Date(); - return ( - (a = +a), - (b = +b), - function (t) { - return d.setTime(a * (1 - t) + b * t), d; - } - ); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/number.js - - /* harmony default export */ function src_number(a, b) { - return ( - (a = +a), - (b = +b), - function (t) { - return a * (1 - t) + b * t; - } - ); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/object.js - - /* harmony default export */ function object(a, b) { - var i = {}, - c = {}, - k; - - if (a === null || typeof a !== 'object') a = {}; - if (b === null || typeof b !== 'object') b = {}; - - for (k in b) { - if (k in a) { - i[k] = value(a[k], b[k]); - } else { - c[k] = b[k]; - } - } - - return function (t) { - for (k in i) c[k] = i[k](t); - return c; - }; - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/string.js - - var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, - reB = new RegExp(reA.source, 'g'); - - function zero(b) { - return function () { - return b; - }; - } - - function one(b) { - return function (t) { - return b(t) + ''; - }; - } - - /* harmony default export */ function string(a, b) { - var bi = (reA.lastIndex = reB.lastIndex = 0), // scan index for next number in b - am, // current match in a - bm, // current match in b - bs, // string preceding current number in b, if any - i = -1, // index in s - s = [], // string constants and placeholders - q = []; // number interpolators - - // Coerce inputs to strings. - (a = a + ''), (b = b + ''); - - // Interpolate pairs of numbers in a & b. - while ((am = reA.exec(a)) && (bm = reB.exec(b))) { - if ((bs = bm.index) > bi) { - // a string precedes the next number in b - bs = b.slice(bi, bs); - if (s[i]) s[i] += bs; - // coalesce with previous string - else s[++i] = bs; - } - if ((am = am[0]) === (bm = bm[0])) { - // numbers in a & b match - if (s[i]) s[i] += bm; - // coalesce with previous string - else s[++i] = bm; - } else { - // interpolate non-matching numbers - s[++i] = null; - q.push({ i: i, x: src_number(am, bm) }); - } - bi = reB.lastIndex; - } - - // Add remains of b. - if (bi < b.length) { - bs = b.slice(bi); - if (s[i]) s[i] += bs; - // coalesce with previous string - else s[++i] = bs; - } - - // Special optimization for only a single match. - // Otherwise, interpolate each of the numbers and rejoin the string. - return s.length < 2 - ? q[0] - ? one(q[0].x) - : zero(b) - : ((b = q.length), - function (t) { - for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); - return s.join(''); - }); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/numberArray.js - - /* harmony default export */ function src_numberArray(a, b) { - if (!b) b = []; - var n = a ? Math.min(b.length, a.length) : 0, - c = b.slice(), - i; - return function (t) { - for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t; - return c; - }; - } - - function numberArray_isNumberArray(x) { - return ArrayBuffer.isView(x) && !(x instanceof DataView); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/value.js - - /* harmony default export */ function value(a, b) { - var t = typeof b, - c; - return b == null || t === 'boolean' - ? d3_interpolate_src_constant(b) - : (t === 'number' - ? src_number - : t === 'string' - ? (c = color(b)) - ? ((b = c), rgb) - : string - : b instanceof color - ? rgb - : b instanceof Date - ? date - : numberArray_isNumberArray(b) - ? src_numberArray - : Array.isArray(b) - ? genericArray - : (typeof b.valueOf !== 'function' && - typeof b.toString !== 'function') || - isNaN(b) - ? object - : src_number)(a, b); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/round.js - - /* harmony default export */ function round(a, b) { - return ( - (a = +a), - (b = +b), - function (t) { - return Math.round(a * (1 - t) + b * t); - } - ); - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/constant.js - - function constants(x) { - return function () { - return x; - }; - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/number.js - - function number_number(x) { - return +x; - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/continuous.js - - var unit = [0, 1]; - - function continuous_identity(x) { - return x; - } - - function normalize(a, b) { - return (b -= a = +a) - ? function (x) { - return (x - a) / b; - } - : constants(isNaN(b) ? NaN : 0.5); - } - - function clamper(a, b) { - var t; - if (a > b) (t = a), (a = b), (b = t); - return function (x) { - return Math.max(a, Math.min(b, x)); - }; - } - - // normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1]. - // interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b]. - function bimap(domain, range, interpolate) { - var d0 = domain[0], - d1 = domain[1], - r0 = range[0], - r1 = range[1]; - if (d1 < d0) (d0 = normalize(d1, d0)), (r0 = interpolate(r1, r0)); - else (d0 = normalize(d0, d1)), (r0 = interpolate(r0, r1)); - return function (x) { - return r0(d0(x)); - }; - } - - function polymap(domain, range, interpolate) { - var j = Math.min(domain.length, range.length) - 1, - d = new Array(j), - r = new Array(j), - i = -1; - - // Reverse descending domains. - if (domain[j] < domain[0]) { - domain = domain.slice().reverse(); - range = range.slice().reverse(); - } - - while (++i < j) { - d[i] = normalize(domain[i], domain[i + 1]); - r[i] = interpolate(range[i], range[i + 1]); - } - - return function (x) { - var i = bisect(domain, x, 1, j) - 1; - return r[i](d[i](x)); - }; - } - - function copy(source, target) { - return target - .domain(source.domain()) - .range(source.range()) - .interpolate(source.interpolate()) - .clamp(source.clamp()) - .unknown(source.unknown()); - } - - function transformer() { - var domain = unit, - range = unit, - interpolate = value, - transform, - untransform, - unknown, - clamp = continuous_identity, - piecewise, - output, - input; - - function rescale() { - var n = Math.min(domain.length, range.length); - if (clamp !== continuous_identity) - clamp = clamper(domain[0], domain[n - 1]); - piecewise = n > 2 ? polymap : bimap; - output = input = null; - return scale; - } - - function scale(x) { - return x == null || isNaN((x = +x)) - ? unknown - : ( - output || - (output = piecewise(domain.map(transform), range, interpolate)) - )(transform(clamp(x))); - } - - scale.invert = function (y) { - return clamp( - untransform( - ( - input || - (input = piecewise(range, domain.map(transform), src_number)) - )(y), - ), - ); - }; - - scale.domain = function (_) { - return arguments.length - ? ((domain = Array.from(_, number_number)), rescale()) - : domain.slice(); - }; - - scale.range = function (_) { - return arguments.length - ? ((range = Array.from(_)), rescale()) - : range.slice(); - }; - - scale.rangeRound = function (_) { - return (range = Array.from(_)), (interpolate = round), rescale(); - }; - - scale.clamp = function (_) { - return arguments.length - ? ((clamp = _ ? true : continuous_identity), rescale()) - : clamp !== continuous_identity; - }; - - scale.interpolate = function (_) { - return arguments.length ? ((interpolate = _), rescale()) : interpolate; - }; - - scale.unknown = function (_) { - return arguments.length ? ((unknown = _), scale) : unknown; - }; - - return function (t, u) { - (transform = t), (untransform = u); - return rescale(); - }; - } - - function continuous() { - return transformer()(continuous_identity, continuous_identity); - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/init.js - - function initRange(domain, range) { - switch (arguments.length) { - case 0: - break; - case 1: - this.range(domain); - break; - default: - this.range(range).domain(domain); - break; - } - return this; - } - - function initInterpolator(domain, interpolator) { - switch (arguments.length) { - case 0: - break; - case 1: { - if (typeof domain === 'function') this.interpolator(domain); - else this.range(domain); - break; - } - default: { - this.domain(domain); - if (typeof interpolator === 'function') - this.interpolator(interpolator); - else this.range(interpolator); - break; - } - } - return this; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/precisionPrefix.js - - /* harmony default export */ function precisionPrefix(step, value) { - return Math.max( - 0, - Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - - exponent(Math.abs(step)), - ); - } // CONCATENATED MODULE: ../node_modules/d3-format/src/precisionRound.js - - /* harmony default export */ function precisionRound(step, max) { - (step = Math.abs(step)), (max = Math.abs(max) - step); - return Math.max(0, exponent(max) - exponent(step)) + 1; - } // CONCATENATED MODULE: ../node_modules/d3-format/src/precisionFixed.js - - /* harmony default export */ function precisionFixed(step) { - return Math.max(0, -exponent(Math.abs(step))); - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/tickFormat.js - - function tickFormat(start, stop, count, specifier) { - var step = tickStep(start, stop, count), - precision; - specifier = formatSpecifier(specifier == null ? ',f' : specifier); - switch (specifier.type) { - case 's': { - var value = Math.max(Math.abs(start), Math.abs(stop)); - if ( - specifier.precision == null && - !isNaN((precision = precisionPrefix(step, value))) - ) - specifier.precision = precision; - return formatPrefix(specifier, value); - } - case '': - case 'e': - case 'g': - case 'p': - case 'r': { - if ( - specifier.precision == null && - !isNaN( - (precision = precisionRound( - step, - Math.max(Math.abs(start), Math.abs(stop)), - )), - ) - ) - specifier.precision = precision - (specifier.type === 'e'); - break; - } - case 'f': - case '%': { - if ( - specifier.precision == null && - !isNaN((precision = precisionFixed(step))) - ) - specifier.precision = precision - (specifier.type === '%') * 2; - break; - } - } - return format(specifier); - } // CONCATENATED MODULE: ../node_modules/d3-scale/src/linear.js - - function linearish(scale) { - var domain = scale.domain; - - scale.ticks = function (count) { - var d = domain(); - return ticks(d[0], d[d.length - 1], count == null ? 10 : count); - }; - - scale.tickFormat = function (count, specifier) { - var d = domain(); - return tickFormat( - d[0], - d[d.length - 1], - count == null ? 10 : count, - specifier, - ); - }; - - scale.nice = function (count) { - if (count == null) count = 10; - - var d = domain(); - var i0 = 0; - var i1 = d.length - 1; - var start = d[i0]; - var stop = d[i1]; - var prestep; - var step; - var maxIter = 10; - - if (stop < start) { - (step = start), (start = stop), (stop = step); - (step = i0), (i0 = i1), (i1 = step); - } - - while (maxIter-- > 0) { - step = tickIncrement(start, stop, count); - if (step === prestep) { - d[i0] = start; - d[i1] = stop; - return domain(d); - } else if (step > 0) { - start = Math.floor(start / step) * step; - stop = Math.ceil(stop / step) * step; - } else if (step < 0) { - start = Math.ceil(start * step) / step; - stop = Math.floor(stop * step) / step; - } else { - break; - } - prestep = step; - } - - return scale; - }; - - return scale; - } - - function linear_linear() { - var scale = continuous(); - - scale.copy = function () { - return copy(scale, linear_linear()); - }; - - initRange.apply(scale, arguments); - - return linearish(scale); - } // CONCATENATED MODULE: ../node_modules/d3-ease/src/cubic.js - - function cubicIn(t) { - return t * t * t; - } - - function cubicOut(t) { - return --t * t * t + 1; - } - - function cubicInOut(t) { - return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2; - } // CONCATENATED MODULE: ../node_modules/d3-dispatch/src/dispatch.js - - var noop = { value: () => {} }; - - function dispatch_dispatch() { - for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) { - if (!(t = arguments[i] + '') || t in _ || /[\s.]/.test(t)) - throw new Error('illegal type: ' + t); - _[t] = []; - } - return new Dispatch(_); - } - - function Dispatch(_) { - this._ = _; - } - - function dispatch_parseTypenames(typenames, types) { - return typenames - .trim() - .split(/^|\s+/) - .map(function (t) { - var name = '', - i = t.indexOf('.'); - if (i >= 0) (name = t.slice(i + 1)), (t = t.slice(0, i)); - if (t && !types.hasOwnProperty(t)) - throw new Error('unknown type: ' + t); - return { type: t, name: name }; - }); - } - - Dispatch.prototype = dispatch_dispatch.prototype = { - constructor: Dispatch, - on: function (typename, callback) { - var _ = this._, - T = dispatch_parseTypenames(typename + '', _), - t, - i = -1, - n = T.length; - - // If no callback was specified, return the callback of the given type and name. - if (arguments.length < 2) { - while (++i < n) - if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) - return t; - return; - } - - // If a type was specified, set the callback for the given type and name. - // Otherwise, if a null callback was specified, remove callbacks of the given name. - if (callback != null && typeof callback !== 'function') - throw new Error('invalid callback: ' + callback); - while (++i < n) { - if ((t = (typename = T[i]).type)) - _[t] = set(_[t], typename.name, callback); - else if (callback == null) - for (t in _) _[t] = set(_[t], typename.name, null); - } - - return this; - }, - copy: function () { - var copy = {}, - _ = this._; - for (var t in _) copy[t] = _[t].slice(); - return new Dispatch(copy); - }, - call: function (type, that) { - if ((n = arguments.length - 2) > 0) - for (var args = new Array(n), i = 0, n, t; i < n; ++i) - args[i] = arguments[i + 2]; - if (!this._.hasOwnProperty(type)) - throw new Error('unknown type: ' + type); - for (t = this._[type], i = 0, n = t.length; i < n; ++i) - t[i].value.apply(that, args); - }, - apply: function (type, that, args) { - if (!this._.hasOwnProperty(type)) - throw new Error('unknown type: ' + type); - for (var t = this._[type], i = 0, n = t.length; i < n; ++i) - t[i].value.apply(that, args); - }, - }; - - function get(type, name) { - for (var i = 0, n = type.length, c; i < n; ++i) { - if ((c = type[i]).name === name) { - return c.value; - } - } - } - - function set(type, name, callback) { - for (var i = 0, n = type.length; i < n; ++i) { - if (type[i].name === name) { - (type[i] = noop), (type = type.slice(0, i).concat(type.slice(i + 1))); - break; - } - } - if (callback != null) type.push({ name: name, value: callback }); - return type; - } - - /* harmony default export */ const src_dispatch = dispatch_dispatch; // CONCATENATED MODULE: ../node_modules/d3-timer/src/timer.js - - var timer_frame = 0, // is an animation frame pending? - timeout = 0, // is a timeout pending? - interval = 0, // are any timers active? - pokeDelay = 1000, // how frequently we check for clock skew - taskHead, - taskTail, - clockLast = 0, - clockNow = 0, - clockSkew = 0, - clock = - typeof performance === 'object' && performance.now ? performance : Date, - setFrame = - typeof window === 'object' && window.requestAnimationFrame - ? window.requestAnimationFrame.bind(window) - : function (f) { - setTimeout(f, 17); - }; - - function now() { - return ( - clockNow || (setFrame(clearNow), (clockNow = clock.now() + clockSkew)) - ); - } - - function clearNow() { - clockNow = 0; - } - - function Timer() { - this._call = this._time = this._next = null; - } - - Timer.prototype = timer.prototype = { - constructor: Timer, - restart: function (callback, delay, time) { - if (typeof callback !== 'function') - throw new TypeError('callback is not a function'); - time = (time == null ? now() : +time) + (delay == null ? 0 : +delay); - if (!this._next && taskTail !== this) { - if (taskTail) taskTail._next = this; - else taskHead = this; - taskTail = this; - } - this._call = callback; - this._time = time; - sleep(); - }, - stop: function () { - if (this._call) { - this._call = null; - this._time = Infinity; - sleep(); - } - }, - }; - - function timer(callback, delay, time) { - var t = new Timer(); - t.restart(callback, delay, time); - return t; - } - - function timerFlush() { - now(); // Get the current time, if not already set. - ++timer_frame; // Pretend we’ve set an alarm, if we haven’t already. - var t = taskHead, - e; - while (t) { - if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e); - t = t._next; - } - --timer_frame; - } - - function wake() { - clockNow = (clockLast = clock.now()) + clockSkew; - timer_frame = timeout = 0; - try { - timerFlush(); - } finally { - timer_frame = 0; - nap(); - clockNow = 0; - } - } - - function poke() { - var now = clock.now(), - delay = now - clockLast; - if (delay > pokeDelay) (clockSkew -= delay), (clockLast = now); - } - - function nap() { - var t0, - t1 = taskHead, - t2, - time = Infinity; - while (t1) { - if (t1._call) { - if (time > t1._time) time = t1._time; - (t0 = t1), (t1 = t1._next); - } else { - (t2 = t1._next), (t1._next = null); - t1 = t0 ? (t0._next = t2) : (taskHead = t2); - } - } - taskTail = t0; - sleep(time); - } - - function sleep(time) { - if (timer_frame) return; // Soonest alarm already set, or will be. - if (timeout) timeout = clearTimeout(timeout); - var delay = time - clockNow; // Strictly less than if we recomputed clockNow. - if (delay > 24) { - if (time < Infinity) - timeout = setTimeout(wake, time - clock.now() - clockSkew); - if (interval) interval = clearInterval(interval); - } else { - if (!interval) - (clockLast = clock.now()), (interval = setInterval(poke, pokeDelay)); - (timer_frame = 1), setFrame(wake); - } - } // CONCATENATED MODULE: ../node_modules/d3-timer/src/timeout.js - - /* harmony default export */ function src_timeout(callback, delay, time) { - var t = new Timer(); - delay = delay == null ? 0 : +delay; - t.restart( - (elapsed) => { - t.stop(); - callback(elapsed + delay); - }, - delay, - time, - ); - return t; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/schedule.js - - var emptyOn = src_dispatch('start', 'end', 'cancel', 'interrupt'); - var emptyTween = []; - - var CREATED = 0; - var SCHEDULED = 1; - var STARTING = 2; - var STARTED = 3; - var RUNNING = 4; - var ENDING = 5; - var ENDED = 6; - - /* harmony default export */ function schedule( - node, - name, - id, - index, - group, - timing, - ) { - var schedules = node.__transition; - if (!schedules) node.__transition = {}; - else if (id in schedules) return; - create(node, id, { - name: name, - index: index, // For context during callback. - group: group, // For context during callback. - on: emptyOn, - tween: emptyTween, - time: timing.time, - delay: timing.delay, - duration: timing.duration, - ease: timing.ease, - timer: null, - state: CREATED, - }); - } - - function init(node, id) { - var schedule = schedule_get(node, id); - if (schedule.state > CREATED) - throw new Error('too late; already scheduled'); - return schedule; - } - - function schedule_set(node, id) { - var schedule = schedule_get(node, id); - if (schedule.state > STARTED) - throw new Error('too late; already running'); - return schedule; - } - - function schedule_get(node, id) { - var schedule = node.__transition; - if (!schedule || !(schedule = schedule[id])) - throw new Error('transition not found'); - return schedule; - } - - function create(node, id, self) { - var schedules = node.__transition, - tween; - - // Initialize the self timer when the transition is created. - // Note the actual delay is not known until the first callback! - schedules[id] = self; - self.timer = timer(schedule, 0, self.time); - - function schedule(elapsed) { - self.state = SCHEDULED; - self.timer.restart(start, self.delay, self.time); - - // If the elapsed delay is less than our first sleep, start immediately. - if (self.delay <= elapsed) start(elapsed - self.delay); - } - - function start(elapsed) { - var i, j, n, o; - - // If the state is not SCHEDULED, then we previously errored on start. - if (self.state !== SCHEDULED) return stop(); - - for (i in schedules) { - o = schedules[i]; - if (o.name !== self.name) continue; - - // While this element already has a starting transition during this frame, - // defer starting an interrupting transition until that transition has a - // chance to tick (and possibly end); see d3/d3-transition#54! - if (o.state === STARTED) return src_timeout(start); - - // Interrupt the active transition, if any. - if (o.state === RUNNING) { - o.state = ENDED; - o.timer.stop(); - o.on.call('interrupt', node, node.__data__, o.index, o.group); - delete schedules[i]; - } - - // Cancel any pre-empted transitions. - else if (+i < id) { - o.state = ENDED; - o.timer.stop(); - o.on.call('cancel', node, node.__data__, o.index, o.group); - delete schedules[i]; - } - } - - // Defer the first tick to end of the current frame; see d3/d3#1576. - // Note the transition may be canceled after start and before the first tick! - // Note this must be scheduled before the start event; see d3/d3-transition#16! - // Assuming this is successful, subsequent callbacks go straight to tick. - src_timeout(function () { - if (self.state === STARTED) { - self.state = RUNNING; - self.timer.restart(tick, self.delay, self.time); - tick(elapsed); - } - }); - - // Dispatch the start event. - // Note this must be done before the tween are initialized. - self.state = STARTING; - self.on.call('start', node, node.__data__, self.index, self.group); - if (self.state !== STARTING) return; // interrupted - self.state = STARTED; - - // Initialize the tween, deleting null tween. - tween = new Array((n = self.tween.length)); - for (i = 0, j = -1; i < n; ++i) { - if ( - (o = self.tween[i].value.call( - node, - node.__data__, - self.index, - self.group, - )) - ) { - tween[++j] = o; - } - } - tween.length = j + 1; - } - - function tick(elapsed) { - var t = - elapsed < self.duration - ? self.ease.call(null, elapsed / self.duration) - : (self.timer.restart(stop), (self.state = ENDING), 1), - i = -1, - n = tween.length; - - while (++i < n) { - tween[i].call(node, t); - } - - // Dispatch the end event. - if (self.state === ENDING) { - self.on.call('end', node, node.__data__, self.index, self.group); - stop(); - } - } - - function stop() { - self.state = ENDED; - self.timer.stop(); - delete schedules[id]; - for (var i in schedules) return; // eslint-disable-line no-unused-vars - delete node.__transition; - } - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/interrupt.js - - /* harmony default export */ function interrupt(node, name) { - var schedules = node.__transition, - schedule, - active, - empty = true, - i; - - if (!schedules) return; - - name = name == null ? null : name + ''; - - for (i in schedules) { - if ((schedule = schedules[i]).name !== name) { - empty = false; - continue; - } - active = schedule.state > STARTING && schedule.state < ENDING; - schedule.state = ENDED; - schedule.timer.stop(); - schedule.on.call( - active ? 'interrupt' : 'cancel', - node, - node.__data__, - schedule.index, - schedule.group, - ); - delete schedules[i]; - } - - if (empty) delete node.__transition; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/interrupt.js - - /* harmony default export */ function selection_interrupt(name) { - return this.each(function () { - interrupt(this, name); - }); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/decompose.js - - var degrees = 180 / Math.PI; - - var decompose_identity = { - translateX: 0, - translateY: 0, - rotate: 0, - skewX: 0, - scaleX: 1, - scaleY: 1, - }; - - /* harmony default export */ function decompose(a, b, c, d, e, f) { - var scaleX, scaleY, skewX; - if ((scaleX = Math.sqrt(a * a + b * b))) (a /= scaleX), (b /= scaleX); - if ((skewX = a * c + b * d)) (c -= a * skewX), (d -= b * skewX); - if ((scaleY = Math.sqrt(c * c + d * d))) - (c /= scaleY), (d /= scaleY), (skewX /= scaleY); - if (a * d < b * c) - (a = -a), (b = -b), (skewX = -skewX), (scaleX = -scaleX); - return { - translateX: e, - translateY: f, - rotate: Math.atan2(b, a) * degrees, - skewX: Math.atan(skewX) * degrees, - scaleX: scaleX, - scaleY: scaleY, - }; - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/parse.js - - var svgNode; - - /* eslint-disable no-undef */ - function parseCss(value) { - const m = new (typeof DOMMatrix === 'function' - ? DOMMatrix - : WebKitCSSMatrix)(value + ''); - return m.isIdentity - ? decompose_identity - : decompose(m.a, m.b, m.c, m.d, m.e, m.f); - } - - function parseSvg(value) { - if (value == null) return decompose_identity; - if (!svgNode) - svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - svgNode.setAttribute('transform', value); - if (!(value = svgNode.transform.baseVal.consolidate())) - return decompose_identity; - value = value.matrix; - return decompose(value.a, value.b, value.c, value.d, value.e, value.f); - } // CONCATENATED MODULE: ../node_modules/d3-interpolate/src/transform/index.js - - function interpolateTransform(parse, pxComma, pxParen, degParen) { - function pop(s) { - return s.length ? s.pop() + ' ' : ''; - } - - function translate(xa, ya, xb, yb, s, q) { - if (xa !== xb || ya !== yb) { - var i = s.push('translate(', null, pxComma, null, pxParen); - q.push( - { i: i - 4, x: src_number(xa, xb) }, - { i: i - 2, x: src_number(ya, yb) }, - ); - } else if (xb || yb) { - s.push('translate(' + xb + pxComma + yb + pxParen); - } - } - - function rotate(a, b, s, q) { - if (a !== b) { - if (a - b > 180) b += 360; - else if (b - a > 180) a += 360; // shortest path - q.push({ - i: s.push(pop(s) + 'rotate(', null, degParen) - 2, - x: src_number(a, b), - }); - } else if (b) { - s.push(pop(s) + 'rotate(' + b + degParen); - } - } - - function skewX(a, b, s, q) { - if (a !== b) { - q.push({ - i: s.push(pop(s) + 'skewX(', null, degParen) - 2, - x: src_number(a, b), - }); - } else if (b) { - s.push(pop(s) + 'skewX(' + b + degParen); - } - } - - function scale(xa, ya, xb, yb, s, q) { - if (xa !== xb || ya !== yb) { - var i = s.push(pop(s) + 'scale(', null, ',', null, ')'); - q.push( - { i: i - 4, x: src_number(xa, xb) }, - { i: i - 2, x: src_number(ya, yb) }, - ); - } else if (xb !== 1 || yb !== 1) { - s.push(pop(s) + 'scale(' + xb + ',' + yb + ')'); - } - } - - return function (a, b) { - var s = [], // string constants and placeholders - q = []; // number interpolators - (a = parse(a)), (b = parse(b)); - translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q); - rotate(a.rotate, b.rotate, s, q); - skewX(a.skewX, b.skewX, s, q); - scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q); - a = b = null; // gc - return function (t) { - var i = -1, - n = q.length, - o; - while (++i < n) s[(o = q[i]).i] = o.x(t); - return s.join(''); - }; - }; - } - - var interpolateTransformCss = interpolateTransform( - parseCss, - 'px, ', - 'px)', - 'deg)', - ); - var interpolateTransformSvg = interpolateTransform( - parseSvg, - ', ', - ')', - ')', - ); // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/tween.js - - function tweenRemove(id, name) { - var tween0, tween1; - return function () { - var schedule = schedule_set(this, id), - tween = schedule.tween; - - // If this node shared tween with the previous node, - // just assign the updated shared tween and we’re done! - // Otherwise, copy-on-write. - if (tween !== tween0) { - tween1 = tween0 = tween; - for (var i = 0, n = tween1.length; i < n; ++i) { - if (tween1[i].name === name) { - tween1 = tween1.slice(); - tween1.splice(i, 1); - break; - } - } - } - - schedule.tween = tween1; - }; - } - - function tweenFunction(id, name, value) { - var tween0, tween1; - if (typeof value !== 'function') throw new Error(); - return function () { - var schedule = schedule_set(this, id), - tween = schedule.tween; - - // If this node shared tween with the previous node, - // just assign the updated shared tween and we’re done! - // Otherwise, copy-on-write. - if (tween !== tween0) { - tween1 = (tween0 = tween).slice(); - for ( - var t = { name: name, value: value }, i = 0, n = tween1.length; - i < n; - ++i - ) { - if (tween1[i].name === name) { - tween1[i] = t; - break; - } - } - if (i === n) tween1.push(t); - } - - schedule.tween = tween1; - }; - } - - /* harmony default export */ function tween(name, value) { - var id = this._id; - - name += ''; - - if (arguments.length < 2) { - var tween = schedule_get(this.node(), id).tween; - for (var i = 0, n = tween.length, t; i < n; ++i) { - if ((t = tween[i]).name === name) { - return t.value; - } - } - return null; - } - - return this.each( - (value == null ? tweenRemove : tweenFunction)(id, name, value), - ); - } - - function tweenValue(transition, name, value) { - var id = transition._id; - - transition.each(function () { - var schedule = schedule_set(this, id); - (schedule.value || (schedule.value = {}))[name] = value.apply( - this, - arguments, - ); - }); - - return function (node) { - return schedule_get(node, id).value[name]; - }; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/interpolate.js - - /* harmony default export */ function interpolate(a, b) { - var c; - return (typeof b === 'number' - ? src_number - : b instanceof color - ? rgb - : (c = color(b)) - ? ((b = c), rgb) - : string)(a, b); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/attr.js - - function attr_attrRemove(name) { - return function () { - this.removeAttribute(name); - }; - } - - function attr_attrRemoveNS(fullname) { - return function () { - this.removeAttributeNS(fullname.space, fullname.local); - }; - } - - function attr_attrConstant(name, interpolate, value1) { - var string00, - string1 = value1 + '', - interpolate0; - return function () { - var string0 = this.getAttribute(name); - return string0 === string1 - ? null - : string0 === string00 - ? interpolate0 - : (interpolate0 = interpolate((string00 = string0), value1)); - }; - } - - function attr_attrConstantNS(fullname, interpolate, value1) { - var string00, - string1 = value1 + '', - interpolate0; - return function () { - var string0 = this.getAttributeNS(fullname.space, fullname.local); - return string0 === string1 - ? null - : string0 === string00 - ? interpolate0 - : (interpolate0 = interpolate((string00 = string0), value1)); - }; - } - - function attr_attrFunction(name, interpolate, value) { - var string00, string10, interpolate0; - return function () { - var string0, - value1 = value(this), - string1; - if (value1 == null) return void this.removeAttribute(name); - string0 = this.getAttribute(name); - string1 = value1 + ''; - return string0 === string1 - ? null - : string0 === string00 && string1 === string10 - ? interpolate0 - : ((string10 = string1), - (interpolate0 = interpolate((string00 = string0), value1))); - }; - } - - function attr_attrFunctionNS(fullname, interpolate, value) { - var string00, string10, interpolate0; - return function () { - var string0, - value1 = value(this), - string1; - if (value1 == null) - return void this.removeAttributeNS(fullname.space, fullname.local); - string0 = this.getAttributeNS(fullname.space, fullname.local); - string1 = value1 + ''; - return string0 === string1 - ? null - : string0 === string00 && string1 === string10 - ? interpolate0 - : ((string10 = string1), - (interpolate0 = interpolate((string00 = string0), value1))); - }; - } - - /* harmony default export */ function transition_attr(name, value) { - var fullname = namespace(name), - i = fullname === 'transform' ? interpolateTransformSvg : interpolate; - return this.attrTween( - name, - typeof value === 'function' - ? (fullname.local ? attr_attrFunctionNS : attr_attrFunction)( - fullname, - i, - tweenValue(this, 'attr.' + name, value), - ) - : value == null - ? (fullname.local ? attr_attrRemoveNS : attr_attrRemove)(fullname) - : (fullname.local ? attr_attrConstantNS : attr_attrConstant)( - fullname, - i, - value, - ), - ); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/attrTween.js - - function attrInterpolate(name, i) { - return function (t) { - this.setAttribute(name, i.call(this, t)); - }; - } - - function attrInterpolateNS(fullname, i) { - return function (t) { - this.setAttributeNS(fullname.space, fullname.local, i.call(this, t)); - }; - } - - function attrTweenNS(fullname, value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i); - return t0; - } - tween._value = value; - return tween; - } - - function attrTween(name, value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i); - return t0; - } - tween._value = value; - return tween; - } - - /* harmony default export */ function transition_attrTween(name, value) { - var key = 'attr.' + name; - if (arguments.length < 2) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== 'function') throw new Error(); - var fullname = namespace(name); - return this.tween( - key, - (fullname.local ? attrTweenNS : attrTween)(fullname, value), - ); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/delay.js - - function delayFunction(id, value) { - return function () { - init(this, id).delay = +value.apply(this, arguments); - }; - } - - function delayConstant(id, value) { - return ( - (value = +value), - function () { - init(this, id).delay = value; - } - ); - } - - /* harmony default export */ function delay(value) { - var id = this._id; - - return arguments.length - ? this.each( - (typeof value === 'function' ? delayFunction : delayConstant)( - id, - value, - ), - ) - : schedule_get(this.node(), id).delay; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/duration.js - - function durationFunction(id, value) { - return function () { - schedule_set(this, id).duration = +value.apply(this, arguments); - }; - } - - function durationConstant(id, value) { - return ( - (value = +value), - function () { - schedule_set(this, id).duration = value; - } - ); - } - - /* harmony default export */ function duration(value) { - var id = this._id; - - return arguments.length - ? this.each( - (typeof value === 'function' ? durationFunction : durationConstant)( - id, - value, - ), - ) - : schedule_get(this.node(), id).duration; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/ease.js - - function easeConstant(id, value) { - if (typeof value !== 'function') throw new Error(); - return function () { - schedule_set(this, id).ease = value; - }; - } - - /* harmony default export */ function ease(value) { - var id = this._id; - - return arguments.length - ? this.each(easeConstant(id, value)) - : schedule_get(this.node(), id).ease; - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/easeVarying.js - - function easeVarying(id, value) { - return function () { - var v = value.apply(this, arguments); - if (typeof v !== 'function') throw new Error(); - schedule_set(this, id).ease = v; - }; - } - - /* harmony default export */ function transition_easeVarying(value) { - if (typeof value !== 'function') throw new Error(); - return this.each(easeVarying(this._id, value)); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/filter.js - - /* harmony default export */ function transition_filter(match) { - if (typeof match !== 'function') match = matcher(match); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = new Array(m), - j = 0; - j < m; - ++j - ) { - for ( - var group = groups[j], - n = group.length, - subgroup = (subgroups[j] = []), - node, - i = 0; - i < n; - ++i - ) { - if ((node = group[i]) && match.call(node, node.__data__, i, group)) { - subgroup.push(node); - } - } - } - - return new Transition(subgroups, this._parents, this._name, this._id); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/merge.js - - /* harmony default export */ function transition_merge(transition) { - if (transition._id !== this._id) throw new Error(); - - for ( - var groups0 = this._groups, - groups1 = transition._groups, - m0 = groups0.length, - m1 = groups1.length, - m = Math.min(m0, m1), - merges = new Array(m0), - j = 0; - j < m; - ++j - ) { - for ( - var group0 = groups0[j], - group1 = groups1[j], - n = group0.length, - merge = (merges[j] = new Array(n)), - node, - i = 0; - i < n; - ++i - ) { - if ((node = group0[i] || group1[i])) { - merge[i] = node; - } - } - } - - for (; j < m0; ++j) { - merges[j] = groups0[j]; - } - - return new Transition(merges, this._parents, this._name, this._id); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/on.js - - function start(name) { - return (name + '') - .trim() - .split(/^|\s+/) - .every(function (t) { - var i = t.indexOf('.'); - if (i >= 0) t = t.slice(0, i); - return !t || t === 'start'; - }); - } - - function onFunction(id, name, listener) { - var on0, - on1, - sit = start(name) ? init : schedule_set; - return function () { - var schedule = sit(this, id), - on = schedule.on; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener); - - schedule.on = on1; - }; - } - - /* harmony default export */ function transition_on(name, listener) { - var id = this._id; - - return arguments.length < 2 - ? schedule_get(this.node(), id).on.on(name) - : this.each(onFunction(id, name, listener)); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/remove.js - - function removeFunction(id) { - return function () { - var parent = this.parentNode; - for (var i in this.__transition) if (+i !== id) return; - if (parent) parent.removeChild(this); - }; - } - - /* harmony default export */ function transition_remove() { - return this.on('end.remove', removeFunction(this._id)); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/select.js - - /* harmony default export */ function transition_select(select) { - var name = this._name, - id = this._id; - - if (typeof select !== 'function') select = selector(select); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = new Array(m), - j = 0; - j < m; - ++j - ) { - for ( - var group = groups[j], - n = group.length, - subgroup = (subgroups[j] = new Array(n)), - node, - subnode, - i = 0; - i < n; - ++i - ) { - if ( - (node = group[i]) && - (subnode = select.call(node, node.__data__, i, group)) - ) { - if ('__data__' in node) subnode.__data__ = node.__data__; - subgroup[i] = subnode; - schedule( - subgroup[i], - name, - id, - i, - subgroup, - schedule_get(node, id), - ); - } - } - } - - return new Transition(subgroups, this._parents, name, id); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/selectAll.js - - /* harmony default export */ function transition_selectAll(select) { - var name = this._name, - id = this._id; - - if (typeof select !== 'function') select = selectorAll(select); - - for ( - var groups = this._groups, - m = groups.length, - subgroups = [], - parents = [], - j = 0; - j < m; - ++j - ) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if ((node = group[i])) { - for ( - var children = select.call(node, node.__data__, i, group), - child, - inherit = schedule_get(node, id), - k = 0, - l = children.length; - k < l; - ++k - ) { - if ((child = children[k])) { - schedule(child, name, id, k, children, inherit); - } - } - subgroups.push(children); - parents.push(node); - } - } - } - - return new Transition(subgroups, parents, name, id); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/selection.js - - var selection_Selection = src_selection.prototype.constructor; - - /* harmony default export */ function transition_selection() { - return new selection_Selection(this._groups, this._parents); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/style.js - - function styleNull(name, interpolate) { - var string00, string10, interpolate0; - return function () { - var string0 = styleValue(this, name), - string1 = (this.style.removeProperty(name), styleValue(this, name)); - return string0 === string1 - ? null - : string0 === string00 && string1 === string10 - ? interpolate0 - : (interpolate0 = interpolate( - (string00 = string0), - (string10 = string1), - )); - }; - } - - function style_styleRemove(name) { - return function () { - this.style.removeProperty(name); - }; - } - - function style_styleConstant(name, interpolate, value1) { - var string00, - string1 = value1 + '', - interpolate0; - return function () { - var string0 = styleValue(this, name); - return string0 === string1 - ? null - : string0 === string00 - ? interpolate0 - : (interpolate0 = interpolate((string00 = string0), value1)); - }; - } - - function style_styleFunction(name, interpolate, value) { - var string00, string10, interpolate0; - return function () { - var string0 = styleValue(this, name), - value1 = value(this), - string1 = value1 + ''; - if (value1 == null) - string1 = value1 = - (this.style.removeProperty(name), styleValue(this, name)); - return string0 === string1 - ? null - : string0 === string00 && string1 === string10 - ? interpolate0 - : ((string10 = string1), - (interpolate0 = interpolate((string00 = string0), value1))); - }; - } - - function styleMaybeRemove(id, name) { - var on0, - on1, - listener0, - key = 'style.' + name, - event = 'end.' + key, - remove; - return function () { - var schedule = schedule_set(this, id), - on = schedule.on, - listener = - schedule.value[key] == null - ? remove || (remove = style_styleRemove(name)) - : undefined; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0 || listener0 !== listener) - (on1 = (on0 = on).copy()).on(event, (listener0 = listener)); - - schedule.on = on1; - }; - } - - /* harmony default export */ function transition_style( - name, - value, - priority, - ) { - var i = - (name += '') === 'transform' ? interpolateTransformCss : interpolate; - return value == null - ? this.styleTween(name, styleNull(name, i)).on( - 'end.style.' + name, - style_styleRemove(name), - ) - : typeof value === 'function' - ? this.styleTween( - name, - style_styleFunction( - name, - i, - tweenValue(this, 'style.' + name, value), - ), - ).each(styleMaybeRemove(this._id, name)) - : this.styleTween( - name, - style_styleConstant(name, i, value), - priority, - ).on('end.style.' + name, null); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/styleTween.js - - function styleInterpolate(name, i, priority) { - return function (t) { - this.style.setProperty(name, i.call(this, t), priority); - }; - } - - function styleTween(name, value, priority) { - var t, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority); - return t; - } - tween._value = value; - return tween; - } - - /* harmony default export */ function transition_styleTween( - name, - value, - priority, - ) { - var key = 'style.' + (name += ''); - if (arguments.length < 2) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== 'function') throw new Error(); - return this.tween( - key, - styleTween(name, value, priority == null ? '' : priority), - ); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/text.js - - function text_textConstant(value) { - return function () { - this.textContent = value; - }; - } - - function text_textFunction(value) { - return function () { - var value1 = value(this); - this.textContent = value1 == null ? '' : value1; - }; - } - - /* harmony default export */ function transition_text(value) { - return this.tween( - 'text', - typeof value === 'function' - ? text_textFunction(tweenValue(this, 'text', value)) - : text_textConstant(value == null ? '' : value + ''), - ); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/textTween.js - - function textInterpolate(i) { - return function (t) { - this.textContent = i.call(this, t); - }; - } - - function textTween(value) { - var t0, i0; - function tween() { - var i = value.apply(this, arguments); - if (i !== i0) t0 = (i0 = i) && textInterpolate(i); - return t0; - } - tween._value = value; - return tween; - } - - /* harmony default export */ function transition_textTween(value) { - var key = 'text'; - if (arguments.length < 1) return (key = this.tween(key)) && key._value; - if (value == null) return this.tween(key, null); - if (typeof value !== 'function') throw new Error(); - return this.tween(key, textTween(value)); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/transition.js - - /* harmony default export */ function transition() { - var name = this._name, - id0 = this._id, - id1 = newId(); - - for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if ((node = group[i])) { - var inherit = schedule_get(node, id0); - schedule(node, name, id1, i, group, { - time: inherit.time + inherit.delay + inherit.duration, - delay: 0, - duration: inherit.duration, - ease: inherit.ease, - }); - } - } - } - - return new Transition(groups, this._parents, name, id1); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/end.js - - /* harmony default export */ function end() { - var on0, - on1, - that = this, - id = that._id, - size = that.size(); - return new Promise(function (resolve, reject) { - var cancel = { value: reject }, - end = { - value: function () { - if (--size === 0) resolve(); - }, - }; - - that.each(function () { - var schedule = schedule_set(this, id), - on = schedule.on; - - // If this node shared a dispatch with the previous node, - // just assign the updated shared dispatch and we’re done! - // Otherwise, copy-on-write. - if (on !== on0) { - on1 = (on0 = on).copy(); - on1._.cancel.push(cancel); - on1._.interrupt.push(cancel); - on1._.end.push(end); - } - - schedule.on = on1; - }); - - // The selection was empty, resolve end immediately - if (size === 0) resolve(); - }); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/transition/index.js - - var id = 0; - - function Transition(groups, parents, name, id) { - this._groups = groups; - this._parents = parents; - this._name = name; - this._id = id; - } - - function transition_transition(name) { - return src_selection().transition(name); - } - - function newId() { - return ++id; - } - - var selection_prototype = src_selection.prototype; - - Transition.prototype = transition_transition.prototype = { - constructor: Transition, - select: transition_select, - selectAll: transition_selectAll, - selectChild: selection_prototype.selectChild, - selectChildren: selection_prototype.selectChildren, - filter: transition_filter, - merge: transition_merge, - selection: transition_selection, - transition: transition, - call: selection_prototype.call, - nodes: selection_prototype.nodes, - node: selection_prototype.node, - size: selection_prototype.size, - empty: selection_prototype.empty, - each: selection_prototype.each, - on: transition_on, - attr: transition_attr, - attrTween: transition_attrTween, - style: transition_style, - styleTween: transition_styleTween, - text: transition_text, - textTween: transition_textTween, - remove: transition_remove, - tween: tween, - delay: delay, - duration: duration, - ease: ease, - easeVarying: transition_easeVarying, - end: end, - [Symbol.iterator]: selection_prototype[Symbol.iterator], - }; // CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/transition.js - - var defaultTiming = { - time: null, // Set on use. - delay: 0, - duration: 250, - ease: cubicInOut, - }; - - function inherit(node, id) { - var timing; - while (!(timing = node.__transition) || !(timing = timing[id])) { - if (!(node = node.parentNode)) { - throw new Error(`transition ${id} not found`); - } - } - return timing; - } - - /* harmony default export */ function selection_transition(name) { - var id, timing; - - if (name instanceof Transition) { - (id = name._id), (name = name._name); - } else { - (id = newId()), - ((timing = defaultTiming).time = now()), - (name = name == null ? null : name + ''); - } - - for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) { - for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { - if ((node = group[i])) { - schedule(node, name, id, i, group, timing || inherit(node, id)); - } - } - } - - return new Transition(groups, this._parents, name, id); - } // CONCATENATED MODULE: ../node_modules/d3-transition/src/selection/index.js - - src_selection.prototype.interrupt = selection_interrupt; - src_selection.prototype.transition = selection_transition; // CONCATENATED MODULE: ../node_modules/d3-transition/src/index.js // CONCATENATED MODULE: ./colorUtils.js - - function generateHash(name) { - // Return a vector (0.0->1.0) that is a hash of the input string. - // The hash is computed to favor early characters over later ones, so - // that strings with similar starts have similar vectors. Only the first - // 6 characters are considered. - const MAX_CHAR = 6; - - let hash = 0; - let maxHash = 0; - let weight = 1; - const mod = 10; - - if (name) { - for (let i = 0; i < name.length; i++) { - if (i > MAX_CHAR) { - break; - } - hash += weight * (name.charCodeAt(i) % mod); - maxHash += weight * (mod - 1); - weight *= 0.7; - } - if (maxHash > 0) { - hash = hash / maxHash; - } - } - return hash; - } - - function generateColorVector(name) { - let vector = 0; - if (name) { - const nameArr = name.split('`'); - if (nameArr.length > 1) { - name = nameArr[nameArr.length - 1]; // drop module name if present - } - name = name.split('(')[0]; // drop extra info - vector = generateHash(name); - } - return vector; - } // CONCATENATED MODULE: ./colorScheme.js - - function calculateColor(hue, vector) { - let r; - let g; - let b; - - if (hue === 'red') { - r = 200 + Math.round(55 * vector); - g = 50 + Math.round(80 * vector); - b = g; - } else if (hue === 'orange') { - r = 190 + Math.round(65 * vector); - g = 90 + Math.round(65 * vector); - b = 0; - } else if (hue === 'yellow') { - r = 175 + Math.round(55 * vector); - g = r; - b = 50 + Math.round(20 * vector); - } else if (hue === 'green') { - r = 50 + Math.round(60 * vector); - g = 200 + Math.round(55 * vector); - b = r; - } else if (hue === 'pastelgreen') { - // rgb(163,195,72) - rgb(238,244,221) - r = 163 + Math.round(75 * vector); - g = 195 + Math.round(49 * vector); - b = 72 + Math.round(149 * vector); - } else if (hue === 'blue') { - // rgb(91,156,221) - rgb(217,232,247) - r = 91 + Math.round(126 * vector); - g = 156 + Math.round(76 * vector); - b = 221 + Math.round(26 * vector); - } else if (hue === 'aqua') { - r = 50 + Math.round(60 * vector); - g = 165 + Math.round(55 * vector); - b = g; - } else if (hue === 'cold') { - r = 0 + Math.round(55 * (1 - vector)); - g = 0 + Math.round(230 * (1 - vector)); - b = 200 + Math.round(55 * vector); - } else { - // original warm palette - r = 200 + Math.round(55 * vector); - g = 0 + Math.round(230 * (1 - vector)); - b = 0 + Math.round(55 * (1 - vector)); - } - - return 'rgb(' + r + ',' + g + ',' + b + ')'; - } // CONCATENATED MODULE: ./flamegraph.js - - /* harmony default export */ function flamegraph() { - let w = 960; // graph width - let h = null; // graph height - let c = 18; // cell height - let selection = null; // selection - let tooltip = null; // tooltip - let title = ''; // graph title - let transitionDuration = 750; - let transitionEase = cubicInOut; // tooltip offset - let sort = false; - let inverted = false; // invert the graph direction - let clickHandler = null; - let hoverHandler = null; - let minFrameSize = 0; - let detailsElement = null; - let searchDetails = null; - let selfValue = false; - let resetHeightOnZoom = false; - let scrollOnZoom = false; - let minHeight = null; - let computeDelta = false; - let colorHue = null; - - let getName = function (d) { - return d.data.n || d.data.name; - }; - - let getValue = function (d) { - if ('v' in d) { - return d.v; - } else { - return d.value; - } - }; - - let getChildren = function (d) { - return d.c || d.children; - }; - - let getLibtype = function (d) { - return d.data.l || d.data.libtype; - }; - - let getDelta = function (d) { - if ('d' in d.data) { - return d.data.d; - } else { - return d.data.delta; - } - }; - - let searchHandler = function (searchResults, searchSum, totalValue) { - searchDetails = () => { - if (detailsElement) { - detailsElement.textContent = - 'search: ' + - searchSum + - ' of ' + - totalValue + - ' total time ( ' + - format('.3f')(100 * (searchSum / totalValue), 3) + - '%)'; - } - }; - searchDetails(); - }; - const originalSearchHandler = searchHandler; - - let searchMatch = (d, term, ignoreCase = false) => { - if (!term) { - return false; - } - let label = getName(d); - if (ignoreCase) { - term = term.toLowerCase(); - label = label.toLowerCase(); - } - const re = new RegExp(term); - return typeof label !== 'undefined' && label && label.match(re); - }; - const originalSearchMatch = searchMatch; - - let detailsHandler = function (d) { - if (detailsElement) { - if (d) { - detailsElement.textContent = d; - } else { - if (typeof searchDetails === 'function') { - searchDetails(); - } else { - detailsElement.textContent = ''; - } - } - } - }; - const originalDetailsHandler = detailsHandler; - - let labelHandler = function (d) { - return ( - getName(d) + - ' (' + - format('.3f')(100 * (d.x1 - d.x0), 3) + - '%, ' + - getValue(d) + - ' ms)' - ); - }; - - let colorMapper = function (d) { - return d.highlight ? '#E600E6' : colorHash(getName(d), getLibtype(d)); - }; - const originalColorMapper = colorMapper; - - function colorHash(name, libtype) { - // Return a color for the given name and library type. The library type - // selects the hue, and the name is hashed to a color in that hue. - - // default when libtype is not in use - let hue = colorHue || 'warm'; - - if (!colorHue && !(typeof libtype === 'undefined' || libtype === '')) { - // Select hue. Order is important. - hue = 'red'; - if (typeof name !== 'undefined' && name && name.match(/::/)) { - hue = 'yellow'; - } - if (libtype === 'kernel') { - hue = 'orange'; - } else if (libtype === 'jit') { - hue = 'green'; - } else if (libtype === 'inlined') { - hue = 'aqua'; - } - } - - const vector = generateColorVector(name); - return calculateColor(hue, vector); - } - - function show(d) { - d.data.fade = false; - d.data.hide = false; - if (d.children) { - d.children.forEach(show); - } - } - - function hideSiblings(node) { - let child = node; - let parent = child.parent; - let children, i, sibling; - while (parent) { - children = parent.children; - i = children.length; - while (i--) { - sibling = children[i]; - if (sibling !== child) { - sibling.data.hide = true; - } - } - child = parent; - parent = child.parent; - } - } - - function fadeAncestors(d) { - if (d.parent) { - d.parent.data.fade = true; - fadeAncestors(d.parent); - } - } - - function zoom(d) { - if (tooltip) tooltip.hide(); - hideSiblings(d); - show(d); - fadeAncestors(d); - update(); - if (scrollOnZoom) { - const chartOffset = src_select(this).select('svg')._groups[0][0] - .parentNode.offsetTop; - const maxFrames = (window.innerHeight - chartOffset) / c; - const frameOffset = (d.height - maxFrames + 10) * c; - window.scrollTo({ - top: chartOffset + frameOffset, - left: 0, - behavior: 'smooth', - }); - } - if (typeof clickHandler === 'function') { - clickHandler(d); - } - } - - function searchTree(d, term) { - const results = []; - let sum = 0; - - function searchInner(d, foundParent) { - let found = false; - - if (searchMatch(d, term)) { - d.highlight = true; - found = true; - if (!foundParent) { - sum += getValue(d); - } - results.push(d); - } else { - d.highlight = false; - } - - if (getChildren(d)) { - getChildren(d).forEach(function (child) { - searchInner(child, foundParent || found); - }); - } - } - searchInner(d, false); - - return [results, sum]; - } - - function findTree(d, id) { - if (d.id === id) { - return d; - } else { - const children = getChildren(d); - if (children) { - for (let i = 0; i < children.length; i++) { - const found = findTree(children[i], id); - if (found) { - return found; - } - } - } - } - } - - function clear(d) { - d.highlight = false; - if (getChildren(d)) { - getChildren(d).forEach(function (child) { - clear(child); - }); - } - } - - function doSort(a, b) { - if (typeof sort === 'function') { - return sort(a, b); - } else if (sort) { - return ascending_ascending(getName(a), getName(b)); - } - } - - const p = partition(); - - function filterNodes(root) { - let nodeList = root.descendants(); - if (minFrameSize > 0) { - const kx = w / (root.x1 - root.x0); - nodeList = nodeList.filter(function (el) { - return (el.x1 - el.x0) * kx > minFrameSize; - }); - } - return nodeList; - } - - function update() { - selection.each(function (root) { - const x = linear_linear().range([0, w]); - const y = linear_linear().range([0, c]); - - reappraiseNode(root); - - if (sort) root.sort(doSort); - - p(root); - - const kx = w / (root.x1 - root.x0); - function width(d) { - return (d.x1 - d.x0) * kx; - } - - const descendants = filterNodes(root); - const svg = src_select(this).select('svg'); - svg.attr('width', w); - - let g = svg.selectAll('g').data(descendants, function (d) { - return d.id; - }); - - // if height is not set: set height on first update, after nodes were filtered by minFrameSize - if (!h || resetHeightOnZoom) { - const maxDepth = Math.max.apply( - null, - descendants.map(function (n) { - return n.depth; - }), - ); - - h = (maxDepth + 3) * c; - if (h < minHeight) h = minHeight; - - svg.attr('height', h); - } - - g.transition() - .duration(transitionDuration) - .ease(transitionEase) - .attr('transform', function (d) { - return ( - 'translate(' + - x(d.x0) + - ',' + - (inverted ? y(d.depth) : h - y(d.depth) - c) + - ')' - ); - }); - - g.select('rect') - .transition() - .duration(transitionDuration) - .ease(transitionEase) - .attr('width', width); - - const node = g - .enter() - .append('svg:g') - .attr('transform', function (d) { - return ( - 'translate(' + - x(d.x0) + - ',' + - (inverted ? y(d.depth) : h - y(d.depth) - c) + - ')' - ); - }); - - node - .append('svg:rect') - .transition() - .delay(transitionDuration / 2) - .attr('width', width); - - if (!tooltip) { - node.append('svg:title'); - } - - node.append('foreignObject').append('xhtml:div'); - - // Now we have to re-select to see the new elements (why?). - g = svg.selectAll('g').data(descendants, function (d) { - return d.id; - }); - - g.attr('width', width) - .attr('height', function (d) { - return c; - }) - .attr('name', function (d) { - return getName(d); - }) - .attr('class', function (d) { - return d.data.fade ? 'frame fade' : 'frame'; - }); - - g.select('rect') - .attr('height', function (d) { - return c; - }) - .attr('fill', function (d) { - return colorMapper(d); - }); - - if (!tooltip) { - g.select('title').text(labelHandler); - } - - g.select('foreignObject') - .attr('width', width) - .attr('height', function (d) { - return c; - }) - .select('div') - .attr('class', 'd3-flame-graph-label') - .style('display', function (d) { - return width(d) < 35 ? 'none' : 'block'; - }) - .transition() - .delay(transitionDuration) - .text(getName); - - g.on('click', (_, d) => { - zoom(d); - }); - - g.exit().remove(); - - g.on('mouseover', function (_, d) { - if (tooltip) tooltip.show(d, this); - detailsHandler(labelHandler(d)); - if (typeof hoverHandler === 'function') { - hoverHandler(d); - } - }).on('mouseout', function () { - if (tooltip) tooltip.hide(); - detailsHandler(null); - }); - }); - } - - function merge(data, samples) { - samples.forEach(function (sample) { - const node = data.find(function (element) { - return element.name === sample.name; - }); - - if (node) { - node.value += sample.value; - if (sample.children) { - if (!node.children) { - node.children = []; - } - merge(node.children, sample.children); - } - } else { - data.push(sample); - } - }); - } - - function forEachNode(node, f) { - f(node); - let children = node.children; - if (children) { - const stack = [children]; - let count, child, grandChildren; - while (stack.length) { - children = stack.pop(); - count = children.length; - while (count--) { - child = children[count]; - f(child); - grandChildren = child.children; - if (grandChildren) { - stack.push(grandChildren); - } - } - } - } - } - - function adoptNode(node) { - let id = 0; - forEachNode(node, function (n) { - n.id = id++; - }); - } - - function reappraiseNode(root) { - let node, - children, - grandChildren, - childrenValue, - i, - j, - child, - childValue; - const stack = []; - const included = []; - const excluded = []; - const compoundValue = !selfValue; - let item = root.data; - if (item.hide) { - root.value = 0; - children = root.children; - if (children) { - excluded.push(children); - } - } else { - root.value = item.fade ? 0 : getValue(item); - stack.push(root); - } - // First DFS pass: - // 1. Update node.value with node's self value - // 2. Populate excluded list with children under hidden nodes - // 3. Populate included list with children under visible nodes - while ((node = stack.pop())) { - children = node.children; - if (children && (i = children.length)) { - childrenValue = 0; - while (i--) { - child = children[i]; - item = child.data; - if (item.hide) { - child.value = 0; - grandChildren = child.children; - if (grandChildren) { - excluded.push(grandChildren); - } - continue; - } - if (item.fade) { - child.value = 0; - } else { - childValue = getValue(item); - child.value = childValue; - childrenValue += childValue; - } - stack.push(child); - } - // Here second part of `&&` is actually checking for `node.data.fade`. However, - // checking for node.value is faster and presents more oportunities for JS optimizer. - if (compoundValue && node.value) { - node.value -= childrenValue; - } - included.push(children); - } - } - // Postorder traversal to compute compound value of each visible node. - i = included.length; - while (i--) { - children = included[i]; - childrenValue = 0; - j = children.length; - while (j--) { - childrenValue += children[j].value; - } - children[0].parent.value += childrenValue; - } - // Continue DFS to set value of all hidden nodes to 0. - while (excluded.length) { - children = excluded.pop(); - j = children.length; - while (j--) { - child = children[j]; - child.value = 0; - grandChildren = child.children; - if (grandChildren) { - excluded.push(grandChildren); - } - } - } - } - - function processData() { - selection.datum((data) => { - if (data.constructor.name !== 'Node') { - // creating a root hierarchical structure - const root = hierarchy(data, getChildren); - - // augumenting nodes with ids - adoptNode(root); - - // calculate actual value - reappraiseNode(root); - - // store value for later use - root.originalValue = root.value; - - // computing deltas for differentials - if (computeDelta) { - root.eachAfter((node) => { - let sum = getDelta(node); - const children = node.children; - let i = children && children.length; - while (--i >= 0) sum += children[i].delta; - node.delta = sum; - }); - } - - // setting the bound data for the selection - return root; - } - }); - } - - function chart(s) { - if (!arguments.length) { - return chart; - } - - // saving the selection on `.call` - selection = s; - - // processing raw data to be used in the chart - processData(); - - // create chart svg - selection.each(function (data) { - if (src_select(this).select('svg').size() === 0) { - const svg = src_select(this) - .append('svg:svg') - .attr('width', w) - .attr('class', 'partition d3-flame-graph'); - - if (h) { - if (h < minHeight) h = minHeight; - svg.attr('height', h); - } - - svg - .append('svg:text') - .attr('class', 'title') - .attr('text-anchor', 'middle') - .attr('y', '25') - .attr('x', w / 2) - .attr('fill', '#808080') - .text(title); - - if (tooltip) svg.call(tooltip); - } - }); - - // first draw - update(); - } - - chart.height = function (_) { - if (!arguments.length) { - return h; - } - h = _; - return chart; - }; - - chart.minHeight = function (_) { - if (!arguments.length) { - return minHeight; - } - minHeight = _; - return chart; - }; - - chart.width = function (_) { - if (!arguments.length) { - return w; - } - w = _; - return chart; - }; - - chart.cellHeight = function (_) { - if (!arguments.length) { - return c; - } - c = _; - return chart; - }; - - chart.tooltip = function (_) { - if (!arguments.length) { - return tooltip; - } - if (typeof _ === 'function') { - tooltip = _; - } - return chart; - }; - - chart.title = function (_) { - if (!arguments.length) { - return title; - } - title = _; - return chart; - }; - - chart.transitionDuration = function (_) { - if (!arguments.length) { - return transitionDuration; - } - transitionDuration = _; - return chart; - }; - - chart.transitionEase = function (_) { - if (!arguments.length) { - return transitionEase; - } - transitionEase = _; - return chart; - }; - - chart.sort = function (_) { - if (!arguments.length) { - return sort; - } - sort = _; - return chart; - }; - - chart.inverted = function (_) { - if (!arguments.length) { - return inverted; - } - inverted = _; - return chart; - }; - - chart.computeDelta = function (_) { - if (!arguments.length) { - return computeDelta; - } - computeDelta = _; - return chart; - }; - - chart.setLabelHandler = function (_) { - if (!arguments.length) { - return labelHandler; - } - labelHandler = _; - return chart; - }; - // Kept for backwards compatibility. - chart.label = chart.setLabelHandler; - - chart.search = function (term) { - const searchResults = []; - let searchSum = 0; - let totalValue = 0; - selection.each(function (data) { - const res = searchTree(data, term); - searchResults.push(...res[0]); - searchSum += res[1]; - totalValue += data.originalValue; - }); - searchHandler(searchResults, searchSum, totalValue); - update(); - }; - - chart.findById = function (id) { - if (typeof id === 'undefined' || id === null) { - return null; - } - let found = null; - selection.each(function (data) { - if (found === null) { - found = findTree(data, id); - } - }); - return found; - }; - - chart.clear = function () { - detailsHandler(null); - selection.each(function (root) { - clear(root); - update(); - }); - }; - - chart.zoomTo = function (d) { - zoom(d); - }; - - chart.resetZoom = function () { - selection.each(function (root) { - zoom(root); // zoom to root - }); - }; - - chart.onClick = function (_) { - if (!arguments.length) { - return clickHandler; - } - clickHandler = _; - return chart; - }; - - chart.onHover = function (_) { - if (!arguments.length) { - return hoverHandler; - } - hoverHandler = _; - return chart; - }; - - chart.merge = function (data) { - if (!selection) { - return chart; - } - - // TODO: Fix merge with zoom - // Merging a zoomed chart doesn't work properly, so - // clearing zoom before merge. - // To apply zoom on merge, we would need to set hide - // and fade on new data according to current data. - // New ids are generated for the whole data structure, - // so previous ids might not be the same. For merge to - // work with zoom, previous ids should be maintained. - this.resetZoom(); - - // Clear search details - // Merge requires a new search, updating data and - // the details handler with search results. - // Since we don't store the search term, can't - // perform search again. - searchDetails = null; - detailsHandler(null); - - selection.datum((root) => { - merge([root.data], [data]); - return root.data; - }); - processData(); - update(); - return chart; - }; - - chart.update = function (data) { - if (!selection) { - return chart; - } - if (data) { - selection.datum(data); - processData(); - } - update(); - return chart; - }; - - chart.destroy = function () { - if (!selection) { - return chart; - } - if (tooltip) { - tooltip.hide(); - if (typeof tooltip.destroy === 'function') { - tooltip.destroy(); - } - } - selection.selectAll('svg').remove(); - return chart; - }; - - chart.setColorMapper = function (_) { - if (!arguments.length) { - colorMapper = originalColorMapper; - return chart; - } - colorMapper = (d) => { - const originalColor = originalColorMapper(d); - return _(d, originalColor); - }; - return chart; - }; - // Kept for backwards compatibility. - chart.color = chart.setColorMapper; - - chart.setColorHue = function (_) { - if (!arguments.length) { - colorHue = null; - return chart; - } - colorHue = _; - return chart; - }; - - chart.minFrameSize = function (_) { - if (!arguments.length) { - return minFrameSize; - } - minFrameSize = _; - return chart; - }; - - chart.setDetailsElement = function (_) { - if (!arguments.length) { - return detailsElement; - } - detailsElement = _; - return chart; - }; - // Kept for backwards compatibility. - chart.details = chart.setDetailsElement; - - chart.selfValue = function (_) { - if (!arguments.length) { - return selfValue; - } - selfValue = _; - return chart; - }; - - chart.resetHeightOnZoom = function (_) { - if (!arguments.length) { - return resetHeightOnZoom; - } - resetHeightOnZoom = _; - return chart; - }; - - chart.scrollOnZoom = function (_) { - if (!arguments.length) { - return scrollOnZoom; - } - scrollOnZoom = _; - return chart; - }; - - chart.getName = function (_) { - if (!arguments.length) { - return getName; - } - getName = _; - return chart; - }; - - chart.getValue = function (_) { - if (!arguments.length) { - return getValue; - } - getValue = _; - return chart; - }; - - chart.getChildren = function (_) { - if (!arguments.length) { - return getChildren; - } - getChildren = _; - return chart; - }; - - chart.getLibtype = function (_) { - if (!arguments.length) { - return getLibtype; - } - getLibtype = _; - return chart; - }; - - chart.getDelta = function (_) { - if (!arguments.length) { - return getDelta; - } - getDelta = _; - return chart; - }; - - chart.setSearchHandler = function (_) { - if (!arguments.length) { - searchHandler = originalSearchHandler; - return chart; - } - searchHandler = _; - return chart; - }; - - chart.setDetailsHandler = function (_) { - if (!arguments.length) { - detailsHandler = originalDetailsHandler; - return chart; - } - detailsHandler = _; - return chart; - }; - - chart.setSearchMatch = function (_) { - if (!arguments.length) { - searchMatch = originalSearchMatch; - return chart; - } - searchMatch = _; - return chart; - }; - - return chart; - } - - __webpack_exports__ = __webpack_exports__['default']; - /******/ return __webpack_exports__; - /******/ - })(); -}); diff --git a/development/charts/table/index.html b/development/charts/table/index.html deleted file mode 100644 index 8fa7c604f91b..000000000000 --- a/development/charts/table/index.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - -
-
- - - - - - - -
S.NoNameTotalTime
-
-
- - diff --git a/development/charts/table/jquery.min.js b/development/charts/table/jquery.min.js deleted file mode 100644 index 8cdc80eb85d8..000000000000 --- a/development/charts/table/jquery.min.js +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * jQuery JavaScript Library v1.6.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Jun 30 14:16:56 2011 -0400 - */ -(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+""),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i. -shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j -)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index f85d64faa887..eaf8b7b544f0 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -156,12 +156,6 @@ async function start() { // links to bundle browser builds const depVizUrl = `${BUILD_LINK_BASE}/build-artifacts/build-viz/index.html`; const depVizLink = `Build System`; - const moduleInitStatsBackgroundUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/initialisation/background/index.html`; - const moduleInitStatsBackgroundLink = `Background Module Init Stats`; - const moduleInitStatsUIUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/initialisation/ui/index.html`; - const moduleInitStatsUILink = `UI Init Stats`; - const moduleLoadStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/load_time/index.html`; - const moduleLoadStatsLink = `Module Load Stats`; const bundleSizeStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/bundle_size.json`; const bundleSizeStatsLink = `Bundle Size Stats`; const userActionsStatsUrl = `${BUILD_LINK_BASE}/test-artifacts/chrome/benchmark/user_actions.json`; @@ -178,9 +172,6 @@ async function start() { `builds (test): ${testBuildLinks}`, `builds (test-flask): ${testFlaskBuildLinks}`, `build viz: ${depVizLink}`, - `mv3: ${moduleInitStatsBackgroundLink}`, - `mv3: ${moduleInitStatsUILink}`, - `mv3: ${moduleLoadStatsLink}`, `mv3: ${bundleSizeStatsLink}`, `mv2: ${userActionsStatsLink}`, `code coverage: ${coverageLink}`, diff --git a/package.json b/package.json index da93c6c75761..8bcb1985e69a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "start:test:mv2:flask": "ENABLE_MV3=false yarn start:test:flask --apply-lavamoat=false --snow=false", "start:test:mv2": "ENABLE_MV3=false BLOCKAID_FILE_CDN=static.cx.metamask.io/api/v1/confirmations/ppom yarn start:test --apply-lavamoat=false --snow=false", "benchmark:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/benchmark.js", - "mv3:stats:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/mv3-perf-stats/index.js", "user-actions-benchmark:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/user-actions-benchmark.js", "benchmark:firefox": "SELENIUM_BROWSER=firefox ts-node test/e2e/benchmark.js", "build:test": "yarn env:e2e build test", diff --git a/test/e2e/mv3-perf-stats/bundle-size.js b/test/e2e/mv3-perf-stats/bundle-size.js deleted file mode 100755 index d37ec561bde5..000000000000 --- a/test/e2e/mv3-perf-stats/bundle-size.js +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable node/shebang */ -const path = require('path'); -const { promises: fs } = require('fs'); -const yargs = require('yargs/yargs'); -const { hideBin } = require('yargs/helpers'); -const { - isWritable, - getFirstParentDirectoryThatExists, -} = require('../../helpers/file'); - -const { exitWithError } = require('../../../development/lib/exit-with-error'); - -/** - * The e2e test case is used to capture bundle time statistics for extension. - */ - -const backgroundFiles = [ - 'scripts/runtime-lavamoat.js', - 'scripts/lockdown-more.js', - 'scripts/sentry-install.js', - 'scripts/policy-load.js', -]; - -const uiFiles = [ - 'scripts/sentry-install.js', - 'scripts/runtime-lavamoat.js', - 'scripts/lockdown-more.js', - 'scripts/policy-load.js', -]; - -const BackgroundFileRegex = /background-[0-9]*.js/u; -const CommonFileRegex = /common-[0-9]*.js/u; -const UIFileRegex = /ui-[0-9]*.js/u; - -async function main() { - const { argv } = yargs(hideBin(process.argv)).usage( - '$0 [options]', - 'Run a page load benchmark', - (_yargs) => - _yargs.option('out', { - description: - 'Output filename. Output printed to STDOUT of this is omitted.', - type: 'string', - normalize: true, - }), - ); - const { out } = argv; - - const distFolder = 'dist/chrome'; - const backgroundFileList = []; - const uiFileList = []; - const commonFileList = []; - - const files = await fs.readdir(distFolder); - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (CommonFileRegex.test(file)) { - const stats = await fs.stat(`${distFolder}/${file}`); - commonFileList.push({ name: file, size: stats.size }); - } else if ( - backgroundFiles.includes(file) || - BackgroundFileRegex.test(file) - ) { - const stats = await fs.stat(`${distFolder}/${file}`); - backgroundFileList.push({ name: file, size: stats.size }); - } else if (uiFiles.includes(file) || UIFileRegex.test(file)) { - const stats = await fs.stat(`${distFolder}/${file}`); - uiFileList.push({ name: file, size: stats.size }); - } - } - - const backgroundBundleSize = backgroundFileList.reduce( - (result, file) => result + file.size, - 0, - ); - - const uiBundleSize = uiFileList.reduce( - (result, file) => result + file.size, - 0, - ); - - const commonBundleSize = commonFileList.reduce( - (result, file) => result + file.size, - 0, - ); - - const result = { - background: { - name: 'background', - size: backgroundBundleSize, - fileList: backgroundFileList, - }, - ui: { - name: 'ui', - size: uiBundleSize, - fileList: uiFileList, - }, - common: { - name: 'common', - size: commonBundleSize, - fileList: commonFileList, - }, - }; - - if (out) { - const outPath = `${out}/bundle_size.json`; - const outputDirectory = path.dirname(outPath); - const existingParentDirectory = await getFirstParentDirectoryThatExists( - outputDirectory, - ); - if (!(await isWritable(existingParentDirectory))) { - throw new Error('Specified output file directory is not writable'); - } - if (outputDirectory !== existingParentDirectory) { - await fs.mkdir(outputDirectory, { recursive: true }); - } - await fs.writeFile(outPath, JSON.stringify(result, null, 2)); - await fs.writeFile( - `${out}/bundle_size_stats.json`, - JSON.stringify( - { - background: backgroundBundleSize, - ui: uiBundleSize, - common: commonBundleSize, - timestamp: new Date().getTime(), - }, - null, - 2, - ), - ); - } else { - console.log(JSON.stringify(result, null, 2)); - } -} - -main().catch((error) => { - exitWithError(error); -}); diff --git a/test/e2e/mv3-perf-stats/index.js b/test/e2e/mv3-perf-stats/index.js deleted file mode 100644 index 4e56a2385a67..000000000000 --- a/test/e2e/mv3-perf-stats/index.js +++ /dev/null @@ -1,2 +0,0 @@ -require('./init-load-stats'); -require('./bundle-size'); diff --git a/test/e2e/mv3-perf-stats/init-load-stats.js b/test/e2e/mv3-perf-stats/init-load-stats.js deleted file mode 100755 index 40584343d990..000000000000 --- a/test/e2e/mv3-perf-stats/init-load-stats.js +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable node/shebang */ -const path = require('path'); -const { promises: fs } = require('fs'); -const yargs = require('yargs/yargs'); -const { hideBin } = require('yargs/helpers'); - -const { exitWithError } = require('../../../development/lib/exit-with-error'); -const { - isWritable, - getFirstParentDirectoryThatExists, -} = require('../../helpers/file'); -const { withFixtures, tinyDelayMs } = require('../helpers'); -const FixtureBuilder = require('../fixture-builder'); - -/** - * The e2e test case is used to capture load and initialisation time statistics for extension in MV3 environment. - */ - -async function profilePageLoad() { - const parsedLogs = {}; - try { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - disableServerMochaToBackground: true, - }, - async ({ driver }) => { - await driver.delay(tinyDelayMs); - await driver.navigate(); - await driver.delay(1000); - const logs = await driver.checkBrowserForLavamoatLogs(); - - let logString = ''; - let logType = ''; - - logs.forEach((log) => { - if (log.indexOf('"version": 1') >= 0) { - // log end here - logString += log; - parsedLogs[logType] = JSON.parse(`{${logString}}`); - logString = ''; - logType = ''; - } else if (logType) { - // log string continues - logString += log; - } else if ( - log.search(/"name": ".*app\/scripts\/background.js",/u) >= 0 - ) { - // background log starts - logString += log; - logType = 'background'; - } else if (log.search(/"name": ".*app\/scripts\/ui.js",/u) >= 0) { - // ui log starts - logString += log; - logType = 'ui'; - } else if (log.search(/"name": "Total"/u) >= 0) { - // load time log starts - logString += log; - logType = 'loadTime'; - } - }); - }, - ); - } catch (error) { - console.log('Error in trying to parse logs.'); - } - return parsedLogs; -} - -async function main() { - const { argv } = yargs(hideBin(process.argv)).usage( - '$0 [options]', - 'Run a page load benchmark', - (_yargs) => - _yargs.option('out', { - description: - 'Output filename. Output printed to STDOUT of this is omitted.', - type: 'string', - normalize: true, - }), - ); - - const results = await profilePageLoad(); - const { out } = argv; - - const logCategories = [ - { key: 'background', dirPath: 'initialisation/background/stacks.json' }, - { key: 'ui', dirPath: 'initialisation/ui/stacks.json' }, - { key: 'loadTime', dirPath: 'load_time/stats.json' }, - ]; - - if (out) { - logCategories.forEach(async ({ key, dirPath }) => { - if (results[key]) { - const outPath = `${out}/${dirPath}`; - const outputDirectory = path.dirname(outPath); - const existingParentDirectory = await getFirstParentDirectoryThatExists( - outputDirectory, - ); - if (!(await isWritable(existingParentDirectory))) { - throw new Error('Specified output file directory is not writable'); - } - if (outputDirectory !== existingParentDirectory) { - await fs.mkdir(outputDirectory, { recursive: true }); - } - await fs.writeFile(outPath, JSON.stringify(results[key], null, 2)); - } - }); - } else { - console.log(JSON.stringify(results, null, 2)); - } -} - -main().catch((error) => { - exitWithError(error); -}); diff --git a/test/e2e/mv3-stats.js b/test/e2e/mv3-stats.js deleted file mode 100755 index 2dd24791e9a5..000000000000 --- a/test/e2e/mv3-stats.js +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable node/shebang */ -const path = require('path'); -const { promises: fs } = require('fs'); -const yargs = require('yargs/yargs'); -const { hideBin } = require('yargs/helpers'); - -const { exitWithError } = require('../../development/lib/exit-with-error'); -const { - isWritable, - getFirstParentDirectoryThatExists, -} = require('../helpers/file'); -const { withFixtures, tinyDelayMs } = require('./helpers'); -const FixtureBuilder = require('./fixture-builder'); - -/** - * The e2e test case is used to capture load and initialisation time statistics for extension in MV3 environment. - */ - -async function profilePageLoad() { - const parsedLogs = {}; - try { - await withFixtures( - { fixtures: new FixtureBuilder().build() }, - async ({ driver }) => { - await driver.delay(tinyDelayMs); - await driver.navigate(); - await driver.delay(1000); - const logs = await driver.checkBrowserForLavamoatLogs(); - - let logString = ''; - let logType = ''; - - logs.forEach((log) => { - if (log.indexOf('"version": 1') >= 0) { - // log end here - logString += log; - parsedLogs[logType] = JSON.parse(`{${logString}}`); - logString = ''; - logType = ''; - } else if (logType) { - // log string continues - logString += log; - } else if ( - log.search(/"name": ".*app\/scripts\/background.js",/u) >= 0 - ) { - // background log starts - logString += log; - logType = 'background'; - } else if (log.search(/"name": ".*app\/scripts\/ui.js",/u) >= 0) { - // ui log starts - logString += log; - logType = 'ui'; - } else if (log.search(/"name": "Total"/u) >= 0) { - // load time log starts - logString += log; - logType = 'loadTime'; - } - }); - }, - ); - } catch (error) { - console.log('Error in trying to parse logs.'); - } - return parsedLogs; -} - -async function main() { - const { argv } = yargs(hideBin(process.argv)).usage( - '$0 [options]', - 'Run a page load benchmark', - (_yargs) => - _yargs.option('out', { - description: - 'Output filename. Output printed to STDOUT of this is omitted.', - type: 'string', - normalize: true, - }), - ); - - const results = await profilePageLoad(); - const { out } = argv; - - const logCategories = [ - { key: 'background', dirPath: 'initialisation/background/stacks.json' }, - { key: 'ui', dirPath: 'initialisation/ui/stacks.json' }, - { key: 'loadTime', dirPath: 'load_time/stats.json' }, - ]; - - if (out) { - logCategories.forEach(async ({ key, dirPath }) => { - if (results[key]) { - const outPath = `${out}/${dirPath}`; - const outputDirectory = path.dirname(outPath); - const existingParentDirectory = await getFirstParentDirectoryThatExists( - outputDirectory, - ); - if (!(await isWritable(existingParentDirectory))) { - throw new Error('Specified output file directory is not writable'); - } - if (outputDirectory !== existingParentDirectory) { - await fs.mkdir(outputDirectory, { recursive: true }); - } - await fs.writeFile(outPath, JSON.stringify(results[key], null, 2)); - } - }); - } else { - console.log(JSON.stringify(results, null, 2)); - } -} - -main().catch((error) => { - exitWithError(error); -}); From c91b4ee95c5c95a693c181f95608d93682d778a9 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Sat, 21 Dec 2024 10:05:18 +0100 Subject: [PATCH 27/71] fix: Use `break-word` for Snaps UI text wrapping (#29387) ## **Description** Use `break-word` for Snaps UI text wrapping to prevent squishing of text in `Section` among other components. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29387?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/snaps/issues/2816 ## **Screenshots/Recordings** ![image](https://github.com/user-attachments/assets/9466464c-862d-4cc2-84a5-97896cd521f4) --- ui/components/app/snaps/snap-ui-renderer/components/text.ts | 2 +- .../snaps-section/__snapshots__/snaps-section.test.tsx.snap | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-renderer/components/text.ts b/ui/components/app/snaps/snap-ui-renderer/components/text.ts index 96bb6c520e48..6b68227f6bd5 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/text.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/text.ts @@ -55,7 +55,7 @@ export const text: UIComponentFactory = ({ variant: element.props.size === 'sm' ? TextVariant.bodySm : TextVariant.bodyMd, fontWeight: getFontWeight(element.props.fontWeight), - overflowWrap: OverflowWrap.Anywhere, + overflowWrap: OverflowWrap.BreakWord, color: getTextColor(element.props.color), className: 'snap-ui-renderer__text', textAlign: element.props.alignment, diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap index 656b1fc49740..27b21ab7ab3c 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap @@ -46,7 +46,7 @@ exports[`SnapsSection renders section for typed sign request 1`] = ` style="overflow-y: auto;" >

Hello world again!

@@ -104,7 +104,7 @@ exports[`SnapsSection renders section personal sign request 1`] = ` style="overflow-y: auto;" >

Hello world!

From 768716d250f945b9573e0a49ab22397dde8fa102 Mon Sep 17 00:00:00 2001 From: Priya Date: Mon, 23 Dec 2024 16:03:31 +0100 Subject: [PATCH 28/71] test: fix flaky native send and transaction decoding test (#29362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The test dapp page loads and the element is visible but not enabled yet and this causes sometimes for the test to click on the button too soon and to fail. Added a new wait condition in the waitForSelector method to wait until the element is enabled and then click on it. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29362?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28485 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/page-objects/pages/test-dapp.ts | 6 ++++++ test/e2e/webdriver/driver.js | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/e2e/page-objects/pages/test-dapp.ts b/test/e2e/page-objects/pages/test-dapp.ts index 5471afb3556a..5e86a7189dce 100644 --- a/test/e2e/page-objects/pages/test-dapp.ts +++ b/test/e2e/page-objects/pages/test-dapp.ts @@ -238,10 +238,16 @@ class TestDapp { } async clickSimpleSendButton() { + await this.driver.waitForSelector(this.simpleSendButton, { + state: 'enabled', + }); await this.driver.clickElement(this.simpleSendButton); } async clickERC721MintButton() { + await this.driver.waitForSelector(this.erc721MintButton, { + state: 'enabled', + }); await this.driver.clickElement(this.erc721MintButton); } diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index c16ef3999490..5bdf95d8322d 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -355,7 +355,7 @@ class Driver { // bucket that can include the state attribute to wait for elements that // match the selector to be removed from the DOM. let element; - if (!['visible', 'detached'].includes(state)) { + if (!['visible', 'detached', 'enabled'].includes(state)) { throw new Error(`Provided state selector ${state} is not supported`); } if (state === 'visible') { @@ -368,7 +368,13 @@ class Driver { until.stalenessOf(await this.findElement(rawLocator)), timeout, ); + } else if (state === 'enabled') { + element = await this.driver.wait( + until.elementIsEnabled(await this.findElement(rawLocator)), + timeout, + ); } + return wrapElementWithAPI(element, this); } From 1fbb63cc4e2c15ffc344155f8fa6e6f240d8dd74 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Mon, 6 Jan 2025 12:20:27 +0100 Subject: [PATCH 29/71] fix: Correct theme value for Snap UI footer buttons (#29434) ## **Description** The Snap UI footer buttons use the DS button which forces light theme, this does not work for the colors used in the Snap buttons, therefore we revert back to using the actually selected theme. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29434?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/29388 ## **Screenshots/Recordings** ### **Before** ![Image](https://github.com/user-attachments/assets/fb14d5c1-44f9-473a-81bf-6f0f01c46686) ### **After** ![image](https://github.com/user-attachments/assets/c9f08368-58f5-43b3-a129-fe7689572242) --- .../app/snaps/snap-ui-footer-button/snap-ui-footer-button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/components/app/snaps/snap-ui-footer-button/snap-ui-footer-button.tsx b/ui/components/app/snaps/snap-ui-footer-button/snap-ui-footer-button.tsx index ff3f530ae1e7..6b4dc63e9a3d 100644 --- a/ui/components/app/snaps/snap-ui-footer-button/snap-ui-footer-button.tsx +++ b/ui/components/app/snaps/snap-ui-footer-button/snap-ui-footer-button.tsx @@ -87,6 +87,7 @@ export const SnapUIFooterButton: FunctionComponent< alignItems: AlignItems.center, flexDirection: FlexDirection.Row, }} + data-theme={null} > {isSnapAction && !hideSnapBranding && !loading && ( From c7d14a001f534fabd09866824eef7ce6a34a885e Mon Sep 17 00:00:00 2001 From: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Date: Mon, 6 Jan 2025 08:07:23 -0500 Subject: [PATCH 30/71] chore: replace local `isSnapId` definition with `isSnapId` from `@metamask/snaps-utils` (#29422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **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? Deduplication of code 2. What is the improvement/solution? Using the `isSnapId` function from the `@metamask/snaps-utils` package. ## **Related issues** Fixes: #29280 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 7 +++---- ui/components/app/confirm/info/row/url.tsx | 2 +- .../connected-sites-list.component.js | 2 +- .../snap-metadata-modal/snap-metadata-modal.js | 7 +++++-- .../pages/permissions-page/permissions-page.js | 2 +- ui/helpers/utils/snaps.ts | 16 ---------------- .../info/personal-sign/personal-sign.test.tsx | 11 ++++------- .../confirm/info/personal-sign/personal-sign.tsx | 2 +- .../info/typed-sign-v1/typed-sign-v1.test.tsx | 11 ++++------- .../confirm/info/typed-sign-v1/typed-sign-v1.tsx | 2 +- .../confirm/info/typed-sign/typed-sign.test.tsx | 11 ++++------- .../confirm/info/typed-sign/typed-sign.tsx | 2 +- .../permissions-connect.component.js | 2 +- .../snaps/snap-install/snap-install.js | 2 +- .../snaps/snaps-connect/snaps-connect.js | 2 +- ui/pages/snaps/snap-view/snap-settings.js | 2 +- 16 files changed, 30 insertions(+), 53 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 62fe3c942589..0e568424a69c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -146,12 +146,13 @@ import { TransactionType, } from '@metamask/transaction-controller'; -///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) getLocalizedSnapManifest, stripSnapPrefix, + ///: END:ONLY_INCLUDE_IF + isSnapId, } from '@metamask/snaps-utils'; -///: END:ONLY_INCLUDE_IF import { Interface } from '@ethersproject/abi'; import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis'; @@ -245,8 +246,6 @@ import { } from '../../shared/lib/transactions-controller-utils'; import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { endTrace, trace } from '../../shared/lib/trace'; -// eslint-disable-next-line import/no-restricted-paths -import { isSnapId } from '../../ui/helpers/utils/snaps'; import { BridgeStatusAction } from '../../shared/types/bridge-status'; import { ENVIRONMENT } from '../../development/build/constants'; import fetchWithCache from '../../shared/lib/fetch-with-cache'; diff --git a/ui/components/app/confirm/info/row/url.tsx b/ui/components/app/confirm/info/row/url.tsx index 807fc7298042..cc517880bb42 100644 --- a/ui/components/app/confirm/info/row/url.tsx +++ b/ui/components/app/confirm/info/row/url.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState } from 'react'; +import { isSnapId } from '@metamask/snaps-utils'; import { Box, Icon, @@ -18,7 +19,6 @@ import { } from '../../../../../helpers/constants/design-system'; import SnapAuthorshipPill from '../../../snaps/snap-authorship-pill'; import { SnapMetadataModal } from '../../../snaps/snap-metadata-modal'; -import { isSnapId } from '../../../../../helpers/utils/snaps'; export type ConfirmInfoRowUrlProps = { url: string; diff --git a/ui/components/app/connected-sites-list/connected-sites-list.component.js b/ui/components/app/connected-sites-list/connected-sites-list.component.js index d3b6cffa03fb..edb9ef4aaf50 100644 --- a/ui/components/app/connected-sites-list/connected-sites-list.component.js +++ b/ui/components/app/connected-sites-list/connected-sites-list.component.js @@ -1,11 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { isSnapId } from '@metamask/snaps-utils'; import Button from '../../ui/button'; import { AvatarFavicon, IconSize } from '../../component-library'; import { stripHttpsSchemeWithoutPort } from '../../../helpers/utils/util'; import SiteOrigin from '../../ui/site-origin'; import { Size } from '../../../helpers/constants/design-system'; -import { isSnapId } from '../../../helpers/utils/snaps'; import { SnapIcon } from '../snaps/snap-icon'; export default class ConnectedSitesList extends Component { diff --git a/ui/components/app/snaps/snap-metadata-modal/snap-metadata-modal.js b/ui/components/app/snaps/snap-metadata-modal/snap-metadata-modal.js index 93a57309e89a..ca5049d16598 100644 --- a/ui/components/app/snaps/snap-metadata-modal/snap-metadata-modal.js +++ b/ui/components/app/snaps/snap-metadata-modal/snap-metadata-modal.js @@ -1,7 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; -import { getSnapPrefix, stripSnapPrefix } from '@metamask/snaps-utils'; +import { + getSnapPrefix, + isSnapId, + stripSnapPrefix, +} from '@metamask/snaps-utils'; import { getSnap, getSnapRegistryData, @@ -39,7 +43,6 @@ import { ShowMore } from '../show-more'; import SnapExternalPill from '../snap-version/snap-external-pill'; import { useSafeWebsite } from '../../../../hooks/snaps/useSafeWebsite'; import Tooltip from '../../../ui/tooltip'; -import { isSnapId } from '../../../../helpers/utils/snaps'; import { SnapIcon } from '../snap-icon'; export const SnapMetadataModal = ({ snapId, isOpen, onClose }) => { diff --git a/ui/components/multichain/pages/permissions-page/permissions-page.js b/ui/components/multichain/pages/permissions-page/permissions-page.js index 3f4680bf0350..fa9082b54af8 100644 --- a/ui/components/multichain/pages/permissions-page/permissions-page.js +++ b/ui/components/multichain/pages/permissions-page/permissions-page.js @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { isSnapId } from '@metamask/snaps-utils'; import { Content, Header, Page } from '../page'; import { Box, @@ -26,7 +27,6 @@ import { REVIEW_PERMISSIONS, } from '../../../../helpers/constants/routes'; import { getConnectedSitesListWithNetworkInfo } from '../../../../selectors'; -import { isSnapId } from '../../../../helpers/utils/snaps'; import { ConnectionListItem } from './connection-list-item'; export const PermissionsPage = () => { diff --git a/ui/helpers/utils/snaps.ts b/ui/helpers/utils/snaps.ts index c788393e83ab..d7527f89b482 100644 --- a/ui/helpers/utils/snaps.ts +++ b/ui/helpers/utils/snaps.ts @@ -1,21 +1,5 @@ -import { SnapId } from '@metamask/snaps-sdk'; import { isProduction } from '../../../shared/modules/environment'; -/** - * Check if the given value is a valid snap ID. - * - * NOTE: This function is a duplicate oF a yet to be released version in @metamask/snaps-utils. - * - * @param value - The value to check. - * @returns `true` if the value is a valid snap ID, and `false` otherwise. - */ -export function isSnapId(value: unknown): value is SnapId { - return ( - (typeof value === 'string' || value instanceof String) && - (value.startsWith('local:') || value.startsWith('npm:')) - ); -} - /** * Decode a snap ID fron a pathname. * diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx index 336ef3f9f92f..471309bf5e31 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { TransactionType } from '@metamask/transaction-controller'; +import { isSnapId } from '@metamask/snaps-utils'; import { getMockConfirmState, getMockPersonalSignConfirmState, @@ -13,7 +14,6 @@ import { signatureRequestSIWE, unapprovedPersonalSignMsg, } from '../../../../../../../test/data/confirmations/personal_sign'; -import * as snapUtils from '../../../../../../helpers/utils/snaps'; import { SignatureRequestType } from '../../../../types/confirm'; import * as utils from '../../../../utils'; import PersonalSignInfo from './personal-sign'; @@ -43,13 +43,10 @@ jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { ...originalUtils, stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), getSnapPrefix: jest.fn().mockReturnValue('npm:'), + isSnapId: jest.fn(), }; }); -jest.mock('../../../../../../helpers/utils/snaps', () => ({ - isSnapId: jest.fn(), -})); - describe('PersonalSignInfo', () => { it('renders correctly for personal sign request', () => { const state = getMockPersonalSignConfirmState(); @@ -149,7 +146,7 @@ describe('PersonalSignInfo', () => { getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); + (isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { queryByText, getByText } = renderWithConfirmContextProvider( @@ -171,7 +168,7 @@ describe('PersonalSignInfo', () => { const state = getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); + (isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(state); const { getByText, queryByText } = renderWithConfirmContextProvider( diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx index 38238a2fa49e..7a83402f2ec6 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; +import { isSnapId } from '@metamask/snaps-utils'; import { ConfirmInfoRowText, ConfirmInfoRowUrl, @@ -27,7 +28,6 @@ import { TextColor, TextVariant, } from '../../../../../../helpers/constants/design-system'; -import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { hexToText, sanitizeString, diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx index 83696b69ac64..34ee667ea797 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx @@ -2,10 +2,10 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { TransactionType } from '@metamask/transaction-controller'; +import { isSnapId } from '@metamask/snaps-utils'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../test/data/confirmations/helper'; import { unapprovedTypedSignMsgV1 } from '../../../../../../../test/data/confirmations/typed_sign'; -import * as snapUtils from '../../../../../../helpers/utils/snaps'; import TypedSignInfoV1 from './typed-sign-v1'; jest.mock( @@ -25,13 +25,10 @@ jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { ...originalUtils, stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), getSnapPrefix: jest.fn().mockReturnValue('npm:'), + isSnapId: jest.fn(), }; }); -jest.mock('../../../../../../helpers/utils/snaps', () => ({ - isSnapId: jest.fn(), -})); - describe('TypedSignInfo', () => { it('correctly renders typed sign data request', () => { const mockState = getMockTypedSignConfirmStateForRequest( @@ -65,7 +62,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); + (isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -88,7 +85,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); + (isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx index 5ebc5a9e0f56..6560c33b4491 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { isSnapId } from '@metamask/snaps-utils'; import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; import { ConfirmInfoRow, @@ -14,7 +15,6 @@ import { import { useConfirmContext } from '../../../../context/confirm'; import { ConfirmInfoRowTypedSignDataV1 } from '../../row/typed-sign-data-v1/typedSignDataV1'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; -import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { SigningInWithRow } from '../shared/sign-in-with-row/sign-in-with-row'; const TypedSignV1Info: React.FC = () => { diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx index 640b663e5cc1..4c0ce81a076b 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.test.tsx @@ -5,6 +5,7 @@ import { TransactionType, } from '@metamask/transaction-controller'; +import { isSnapId } from '@metamask/snaps-utils'; import { getMockConfirmStateForTransaction, getMockTypedSignConfirmState, @@ -17,7 +18,6 @@ import { unapprovedTypedSignMsgV4, } from '../../../../../../../test/data/confirmations/typed_sign'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; -import * as snapUtils from '../../../../../../helpers/utils/snaps'; import TypedSignInfo from './typed-sign'; jest.mock( @@ -44,13 +44,10 @@ jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { ...originalUtils, stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), getSnapPrefix: jest.fn().mockReturnValue('npm:'), + isSnapId: jest.fn(), }; }); -jest.mock('../../../../../../helpers/utils/snaps', () => ({ - isSnapId: jest.fn(), -})); - describe('TypedSignInfo', () => { it('renders origin for typed sign data request', () => { const state = getMockTypedSignConfirmState(); @@ -153,7 +150,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(true); + (isSnapId as unknown as jest.Mock).mockReturnValue(true); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , @@ -177,7 +174,7 @@ describe('TypedSignInfo', () => { type: TransactionType.signTypedData, chainId: '0x5', }); - (snapUtils.isSnapId as unknown as jest.Mock).mockReturnValue(false); + (isSnapId as unknown as jest.Mock).mockReturnValue(false); const mockStore = configureMockStore([])(mockState); const { queryByText } = renderWithConfirmContextProvider( , diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx index d21ed2b1fca9..d1fb12bf675b 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { isValidAddress } from 'ethereumjs-util'; +import { isSnapId } from '@metamask/snaps-utils'; import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; import { parseTypedDataMessage } from '../../../../../../../shared/modules/transaction.utils'; import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants'; @@ -21,7 +22,6 @@ import { import { useConfirmContext } from '../../../../context/confirm'; import { useTypesSignSimulationEnabledInfo } from '../../../../hooks/useTypesSignSimulationEnabledInfo'; import { ConfirmInfoRowTypedSignData } from '../../row/typed-sign-data/typedSignData'; -import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { SigningInWithRow } from '../shared/sign-in-with-row/sign-in-with-row'; import { TypedSignV4Simulation } from './typed-sign-v4-simulation'; diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 17f73d633953..f7dc90363363 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; import { providerErrors, serializeError } from '@metamask/rpc-errors'; import { SubjectType } from '@metamask/permission-controller'; +import { isSnapId } from '@metamask/snaps-utils'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { isEthAddress } from '../../../app/scripts/lib/multichain/address'; @@ -19,7 +20,6 @@ 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'; diff --git a/ui/pages/permissions-connect/snaps/snap-install/snap-install.js b/ui/pages/permissions-connect/snaps/snap-install/snap-install.js index 93bdc3c5bd8f..0adeac87dfeb 100644 --- a/ui/pages/permissions-connect/snaps/snap-install/snap-install.js +++ b/ui/pages/permissions-connect/snaps/snap-install/snap-install.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; +import { isSnapId } from '@metamask/snaps-utils'; import { PageContainerFooter } from '../../../../components/ui/page-container'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import SnapInstallWarning from '../../../../components/app/snaps/snap-install-warning'; @@ -34,7 +35,6 @@ import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; import { getSnapMetadata, getSnapsMetadata } from '../../../../selectors'; import { getSnapName } from '../../../../helpers/utils/util'; import PermissionConnectHeader from '../../../../components/app/permission-connect-header'; -import { isSnapId } from '../../../../helpers/utils/snaps'; export default function SnapInstall({ request, diff --git a/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js index b46eb070c3ed..645019848e25 100644 --- a/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js +++ b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { isSnapId } from '@metamask/snaps-utils'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { Box, IconSize, Text } from '../../../../components/component-library'; import { @@ -26,7 +27,6 @@ import { getSnapMetadata, } from '../../../../selectors'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; -import { isSnapId } from '../../../../helpers/utils/snaps'; import { SnapIcon } from '../../../../components/app/snaps/snap-icon'; export default function SnapsConnect({ diff --git a/ui/pages/snaps/snap-view/snap-settings.js b/ui/pages/snaps/snap-view/snap-settings.js index 9db18ea7350e..2e8e02897abf 100644 --- a/ui/pages/snaps/snap-view/snap-settings.js +++ b/ui/pages/snaps/snap-view/snap-settings.js @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import semver from 'semver'; +import { isSnapId } from '@metamask/snaps-utils'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { BackgroundColor, @@ -51,7 +52,6 @@ import { DelineatorType } from '../../../helpers/constants/snaps'; import SnapUpdateAlert from '../../../components/app/snaps/snap-update-alert'; import { CONNECT_ROUTE } from '../../../helpers/constants/routes'; import { ShowMore } from '../../../components/app/snaps/show-more'; -import { isSnapId } from '../../../helpers/utils/snaps'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { KeyringSnapRemovalResultStatus } from './constants'; ///: END:ONLY_INCLUDE_IF From 4ee7e3f33faf439e90a6486c308eb6a6bda03b4a Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Mon, 6 Jan 2025 13:23:30 +0000 Subject: [PATCH 31/71] feat: add some authentication state to sentry logs (#29432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This adds some authentication controller state to sentry logs. We do not log sensitive and important info such as the `accessToken`. This should help with logging some of the service failures that rely on authentication. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29432?quickstart=1) ## **Related issues** Fixes: N/A ## **Manual testing steps** 1. Follow the steps in the [development readme](https://github.com/MetaMask/metamask-extension/blob/main/development/README.md#debugging-sentry) to invoke a sentry error - `DEBUG=metamask:sentry:*` and `METAMASK_DEBUG=1` can help to show the sentry state we are sending. ## **Screenshots/Recordings** ### **Before** ![Screenshot 2025-01-06 at 09 04 48](https://github.com/user-attachments/assets/0469fb5d-9eb3-4616-96f2-e863f2ddcb7c) ### **After** ![Screenshot 2025-01-06 at 11 28 23](https://github.com/user-attachments/assets/b81e6830-bb0f-4276-a345-0a0f7f097fdd) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/constants/sentry-state.ts | 5 +++++ test/e2e/tests/metrics/errors.spec.js | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index ab53ffb3f22d..705b33f450ba 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -42,6 +42,11 @@ export const SENTRY_BACKGROUND_STATE = { }, AuthenticationController: { isSignedIn: false, + sessionData: { + profile: true, + accessToken: false, + expiresIn: true, + }, }, NetworkOrderController: { orderedNetworkList: [], diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 12a44e26fbf1..aaa5c0536485 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -904,6 +904,13 @@ describe('Sentry errors', function () { // the "resetState" method swapsFeatureFlags: true, }, + // Part of the AuthenticationController store, but initialized as undefined + // Only populated once the client is authenticated + sessionData: { + accessToken: false, + expiresIn: true, + profile: true, + }, // This can get erased due to a bug in the app state controller's // preferences state change handler timeoutMinutes: true, From 634b672205f355f0643262a3d37c41aa081abbec Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 6 Jan 2025 17:46:19 -0330 Subject: [PATCH 32/71] chore: Remove broken coverage report link (#29410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The unit test coverage report was previously available in the `metamaskbot` comment, but this link has been broken since we migrated unit tests from CircleCI to GitHub Actions in #25570 The broken link has been removed for now. It can be restored in the future, after migrating the `metamaskbot` comment to GitHub actions as well. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29410?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** Check that the broken link is no longer shown in the `metamaskbot` PR comment. ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- development/metamaskbot-build-announce.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index eaf8b7b544f0..daf9289a0201 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -144,9 +144,6 @@ async function start() { const bundleSizeDataUrl = 'https://raw.githubusercontent.com/MetaMask/extension_bundlesize_stats/main/stats/bundle_size_data.json'; - const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`; - const coverageLink = `Report`; - const storybookUrl = `${BUILD_LINK_BASE}/storybook/index.html`; const storybookLink = `Storybook`; @@ -174,7 +171,6 @@ async function start() { `build viz: ${depVizLink}`, `mv3: ${bundleSizeStatsLink}`, `mv2: ${userActionsStatsLink}`, - `code coverage: ${coverageLink}`, `storybook: ${storybookLink}`, `typescript migration: ${tsMigrationDashboardLink}`, `all artifacts`, From 47fdbe4e5ba86a2d2ff408369a4d5025ff0def04 Mon Sep 17 00:00:00 2001 From: Priya Date: Tue, 7 Jan 2025 09:12:54 +0100 Subject: [PATCH 33/71] chore: Fix flaky snap signature insights tests (#29437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The sign button is sometimes not enabled after the delay, just added a better waiting condition to wait for the button to be enabled [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29437?quickstart=1) ## **Related issues** Fixes: [#29227](https://github.com/MetaMask/metamask-extension/issues/29227) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/helpers.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 3b69d55fc122..24240d6f609a 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -724,7 +724,10 @@ async function clickSignOnSignatureConfirmation({ // so we have to add a small delay as the last alternative to avoid flakiness. await driver.delay(regularDelayMs); } - + await driver.waitForSelector( + { text: 'Sign', tag: 'button' }, + { state: 'enabled' }, + ); await driver.clickElement({ text: 'Sign', tag: 'button' }); } From 3c220da34df09a6ca1413229923f2f1020e4c2dd Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Tue, 7 Jan 2025 11:21:40 +0100 Subject: [PATCH 34/71] fix: Use correct prop for Snap UI Avatar size (#29466) ## **Description** Fixes an issue where the `size` prop would not work for the Snap UI `Avatar` component. This prop was simply missed in the mapping function, this PR corrects that. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29466?quickstart=1) ## **Manual testing steps** ```ts export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request, }) => { switch (request.method) { case 'hello': return snap.request({ method: 'snap_dialog', params: { type: 'confirmation', content: ( ), }, }); default: throw new Error('Method not found.'); } }; ``` --- ui/components/app/snaps/snap-ui-renderer/components/avatar.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts index 9572516383b6..fd7953e54936 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/avatar.ts @@ -5,5 +5,6 @@ export const avatar: UIComponentFactory = ({ element }) => ({ element: 'SnapUIAvatar', props: { address: element.props.address, + size: element.props.size, }, }); From 41930af0408c73d2f4bc4ed3bfa5c3cfcf792fe5 Mon Sep 17 00:00:00 2001 From: Eric Bishard Date: Tue, 7 Jan 2025 07:37:44 -0500 Subject: [PATCH 35/71] feat: enable STX by default with migration and notification (#28854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR enables Smart Transactions (STX) by default through migration number 135 for users who have either opted out or haven't interacted with the STX toggle, provided they have no recorded STX activity. How it works: - Upon Migration 135, alert displays on transaction confirmations: - Legacy transaction flow - New transaction flow (experimental) - Swaps confirmation flow - Contract deployment - Contract interactions (minting, etc.) In the case a user migrates from a previous version of the extension and the migration runs and sets STX toggle "ON" in `Settings > Advanced > Smart Transactions`, they will receive an STX Banner Alert on transaction confirmation screens until dismissed through a close button, or by clicking on the "Higher success rates" link within the alert that goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) for more information. Edge Cases: If a user is new and setting up a wallet for the first time, they will not receive the Banner Alert. If a user imports a new wallet during a fresh install of the extension on a new browser or recovers a wallet, it's possible they may not see the alert if STX was on in a previous install. The STX Banner Alert is dismissed and will not show again if a user is in the state to get shown the banner and toggles STX off independently even if they do not physically dismiss the STX Banner Alert. Migration Logic: 1. If `smartTransactionsOptInStatus` is `null` (new/never interacted) - Sets status to true - Enables notification flag 2. If status is false (previously opted out): - With no Ethereum Mainnet STX activity: Sets to true with notification - With existing Mainnet STX activity: Preserves user preference 3. If status is true: No changes needed UI Components: - Implements SmartTransactionsBannerAlert component for user notification The notification system bridges the migration changes with the UI, ensuring users are informed of the STX enablement while maintaining their ability to opt out through settings. **Target release:** TBD **Affected user base:** ~5.7M users who previously opted out of STX but have no STX activity. --- ## **Related issues** Fixes: N/A --- ## **Running Unit Tests** Migration Test: ```bash yarn jest app/scripts/migrations/135.test.ts --verbose ``` Smart Transaction Banner Component Test: ```bash yarn jest ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.ts --coverage=false ``` Confirm Transaction base Test: ```bash yarn jest ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js --coverage=false ``` Preferences Controller Test: ```bash yarn jest app/scripts/controllers/preferences-controller.test.ts --coverage=false ``` Transaction Alerts Component Test: ```bash yarn jest ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js --coverage=false ``` ## **Manual testing steps** **Test Migration (using a wallet/account with no STX Transactions)** 1. Switch branch: `git checkout tags/v12.5.0` and run: ```bash yarn yarn webpack ``` - Generate a `dist/chrome` directory 6. In Chrome Extension Manager, "Load Unpacked" from this directory 7. Import or setup a wallet without STX transactions, launch the wallet and choose "No Thanks" on the "Enhanced Transaction Protection" popup. 8. Check that toggle is OFF in: `Settings > Advanced > Smart Transactions` 9. Close MetaMask Extension and toggle the Extension "OFF" in the Extension Manager 10. Switch to branch from this PR `git checkout feat/enable-stx-migration` ```bash yarn yarn webpack ``` 11. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` **Test STX Banner Alert that it shows on Transaction Confirmations and not Sign Confirmations)** **_(using new confirmations flow)_** 12. Check that `Improved transaction requests` is ON in `Settings > Experimental` 13. Open the E2E TestDapp, try several Signs (ETH Sign, Personal Sign, Sign Typed Data, etc..) and ensure the STX Banner Alert does not show on those confirmations screens. 14. Create a Send transaction to your own wallet for `0.0001` ETH 15. Ensure that Smart Transactions Banner Alert IS showing 16. Start a Swaps transaction on Ethereum Mainnet 17. Ensure that Smart Transactions Banner Alert IS showing **Test STX Banner Alert that it shows on Transaction Confirmations and not Sign Confirmations)** **_(using old confirmations flow)_** 18. Check that `Improved transaction requests` is OFF in `Settings > Experimental` 19. Open the E2E TestDapp, try several Signs (ETH Sign, Personal Sign, Sign Typed Data, etc..) and ensure the STX Banner Alert does not show on those confirmations screens. 20. Create a Send transaction to your own wallet for `0.0001` ETH 21. Ensure that Smart Transactions Banner Alert IS showing 22. Start a Swaps transaction on Ethereum Mainnet 23. Ensure that Smart Transactions Banner Alert IS showing 24. Without clicking on Check that "Higher success rates" link (inspect) goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) 25. Dismiss the Smart Transactions Banner Alert and set `Improved transaction requests` back to ON in `Settings > Experimental` 26. Create a Send transaction to your own wallet for `0.0001` ETH 27. Ensure that Smart Transactions Banner Alert IS NOT showing **Congrats, you have manually tested the happy path, now we just need to test the edge cases:** 1. Remove the extensions from Extension Manager and Repeat steps 1 to 10 above to run migration again 2. Create a Send transaction to your own wallet for `0.0001` ETH 3. Ensure that Smart Transactions Banner Alert IS showing - DO NOT DISMISS THE ALERT. Instead 4. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` 5. Turn it off 6. Create a Send transaction to your own wallet for `0.0001` ETH 7. Ensure that Smart Transactions Banner Alert IS NOT showing 8. Perform any other Signing and/or Transaction Confirmations and ensure there are no modals that show errors and that the Banner Alert does not show anymore. **Test edge case that after STX migration runs and Banner is being shown that clicking on "Higher success rates" link:** 1. Remove the extensions from Extension Manager and Repeat steps 1 to 10 above to run migration again 2. Create a Send transaction to your own wallet for `0.0001` ETH 3. Ensure that Smart Transactions Banner Alert IS showing - DO NOT DISMISS THE ALERT WITH CLOSE BUTTON... INSTEAD 4. Click on "Higher success rates" link and ensure that it goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) 5. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` 6. Turn it off 7. Create a Send transaction to your own wallet for `0.0001` ETH 8. Ensure that Smart Transactions Banner Alert IS NOT showing 9. Perform any other Signing and/or Transaction Confirmations and ensure there are no modals that show errors and that the Banner Alert does not show anymore. Because the NEW confirmation flow does not support alerts using hooks that are dismissible, we have used the old style Banner Alert, and it is normal for their to be some variation on where the alert shows up and it's surroundings. But overall they should look similar. --- ## **Screenshots/Recordings** ### **Before** 01-stx_before 02-legacySend_before 03-legacySwap_before 04-signTypedDataV4_before 05-contractDeployment_before 06-contractInteraction_before 07-wideSwap_before ### **After** 01-stx_after 02-legacySend_after 03-legacySwap_after 04-signTypedDataV4_after 05-contractDeployment_after 06-contractInteraction_after 07-wideSwap_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 for the new behavior, covering: - [x] Version update handling - [x] All logic branches: - [x] `null` opt-in status - [x] `false` opt-in status with no STX activity - [x] `false` opt-in status with existing STX activity - [x] `true` opt-in status - [x] Notification flag setting - [x] Error handling - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format where 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. pulled and built the branch, ran the app, and tested the changes described above). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket and includes the necessary testing evidence (e.g., recordings, screenshots, or detailed descriptions). --------- Co-authored-by: Dan J Miller Co-authored-by: georgeweiler Co-authored-by: dan437 <80175477+dan437@users.noreply.github.com> --- app/_locales/en/messages.json | 9 + .../preferences-controller.test.ts | 2 + .../controllers/preferences-controller.ts | 13 + app/scripts/migrations/135.test.ts | 187 ++++ app/scripts/migrations/135.ts | 84 ++ app/scripts/migrations/index.js | 1 + shared/constants/alerts.ts | 2 + .../modules/selectors/smart-transactions.ts | 20 + ...rs-after-init-opt-in-background-state.json | 19 +- .../errors-after-init-opt-in-ui-state.json | 19 +- test/e2e/tests/metrics/swaps.spec.js | 1 + test/e2e/tests/swaps/shared.ts | 20 +- test/e2e/tests/swaps/swap-eth.spec.ts | 41 +- .../tests/swaps/swaps-notifications.spec.ts | 2 + .../smart-transactions-banner-alert/index.ts | 1 + .../smart-transactions-banner-alert.test.tsx | 258 ++++++ .../smart-transactions-banner-alert.tsx | 124 +++ .../transaction-alerts/transaction-alerts.js | 2 + .../transaction-alerts.test.js | 73 +- .../confirm-transaction-base.test.js | 1 + .../__snapshots__/confirm.test.tsx.snap | 855 ++++++++++++++++++ .../confirmations/confirm/confirm.test.tsx | 40 + ui/pages/confirmations/confirm/confirm.tsx | 5 + .../prepare-swap-page/prepare-swap-page.js | 4 + .../prepare-swap-page.test.js | 56 ++ 25 files changed, 1791 insertions(+), 48 deletions(-) create mode 100644 app/scripts/migrations/135.test.ts create mode 100644 app/scripts/migrations/135.ts create mode 100644 ui/pages/confirmations/components/smart-transactions-banner-alert/index.ts create mode 100644 ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.tsx create mode 100644 ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 0ef2ec82406f..be2f7e4f6046 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5288,6 +5288,15 @@ "smartTransactions": { "message": "Smart Transactions" }, + "smartTransactionsEnabledDescription": { + "message": " and MEV protection. Now on by default." + }, + "smartTransactionsEnabledLink": { + "message": "Higher success rates" + }, + "smartTransactionsEnabledTitle": { + "message": "Transactions just got smarter" + }, "snapAccountCreated": { "message": "Account created" }, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 39a2d49648b2..f30e938e635e 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -733,6 +733,7 @@ describe('preferences controller', () => { privacyMode: false, showFiatInTestnets: false, showTestNetworks: false, + smartTransactionsMigrationApplied: false, smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, @@ -762,6 +763,7 @@ describe('preferences controller', () => { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, + smartTransactionsMigrationApplied: false, smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index d705be4c3180..217fe43b022d 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -104,6 +104,7 @@ export type Preferences = { showFiatInTestnets: boolean; showTestNetworks: boolean; smartTransactionsOptInStatus: boolean; + smartTransactionsMigrationApplied: boolean; showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; @@ -129,6 +130,7 @@ export type PreferencesControllerState = Omit< PreferencesState, | 'showTestNetworks' | 'smartTransactionsOptInStatus' + | 'smartTransactionsMigrationApplied' | 'privacyMode' | 'tokenSortConfig' | 'useMultiRpcMigration' @@ -217,6 +219,7 @@ export const getDefaultPreferencesControllerState = showFiatInTestnets: false, showTestNetworks: false, smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: false, showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, @@ -406,6 +409,16 @@ const controllerMetadata = { preferences: { persist: true, anonymous: true, + properties: { + smartTransactionsOptInStatus: { + persist: true, + anonymous: true, + }, + smartTransactionsMigrationApplied: { + persist: true, + anonymous: true, + }, + }, }, ipfsGateway: { persist: true, diff --git a/app/scripts/migrations/135.test.ts b/app/scripts/migrations/135.test.ts new file mode 100644 index 000000000000..b2cca43b7733 --- /dev/null +++ b/app/scripts/migrations/135.test.ts @@ -0,0 +1,187 @@ +import { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types'; +import { migrate, VersionedData } from './135'; + +const prevVersion = 134; + +describe('migration #135', () => { + const mockSmartTransaction: SmartTransaction = { + uuid: 'test-uuid', + }; + + it('should update the version metadata', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version: 135 }); + }); + + it('should set stx opt-in to true and migration flag when stx opt-in status is null', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: { + preferences: { + smartTransactionsOptInStatus: null, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsOptInStatus, + ).toBe(true); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should set stx opt-in to true and migration flag when stx opt-in status is undefined', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: {}, + }, + }; + + const newStorage = await migrate(oldStorage); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsOptInStatus, + ).toBe(true); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should set stx opt-in to true and migration flag when stx opt-in is false and no existing mainnet smart transactions', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: { + preferences: { + smartTransactionsOptInStatus: false, + }, + }, + SmartTransactionsController: { + smartTransactionsState: { + smartTransactions: { + '0x1': [], // Empty mainnet transactions + '0xAA36A7': [mockSmartTransaction], // Sepolia has transactions + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsOptInStatus, + ).toBe(true); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should not change stx opt-in when stx opt-in is false but has existing smart transactions, but should set migration flag', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: { + preferences: { + smartTransactionsOptInStatus: false, + }, + }, + SmartTransactionsController: { + smartTransactionsState: { + smartTransactions: { + '0x1': [mockSmartTransaction], + }, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsOptInStatus, + ).toBe(false); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should not change stx opt-in when stx opt-in is already true, but should set migration flag', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: { + preferences: { + smartTransactionsOptInStatus: true, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsOptInStatus, + ).toBe(true); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should initialize preferences object if it does not exist', async () => { + const oldStorage: VersionedData = { + meta: { version: prevVersion }, + data: { + PreferencesController: { + preferences: { + smartTransactionsOptInStatus: true, + }, + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data.PreferencesController?.preferences).toBeDefined(); + expect( + newStorage.data.PreferencesController?.preferences + ?.smartTransactionsMigrationApplied, + ).toBe(true); + }); + + it('should capture exception if PreferencesController state is invalid', async () => { + const sentryCaptureExceptionMock = jest.fn(); + global.sentry = { + captureException: sentryCaptureExceptionMock, + }; + + const oldStorage = { + meta: { version: prevVersion }, + data: { + PreferencesController: 'invalid', + }, + } as unknown as VersionedData; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error('Invalid PreferencesController state: string'), + ); + }); +}); diff --git a/app/scripts/migrations/135.ts b/app/scripts/migrations/135.ts new file mode 100644 index 000000000000..277aafa66227 --- /dev/null +++ b/app/scripts/migrations/135.ts @@ -0,0 +1,84 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import type { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; + +export type VersionedData = { + meta: { + version: number; + }; + data: { + PreferencesController?: { + preferences?: { + smartTransactionsOptInStatus?: boolean | null; + smartTransactionsMigrationApplied?: boolean; + }; + }; + SmartTransactionsController?: { + smartTransactionsState: { + smartTransactions: Record; + }; + }; + }; +}; + +export const version = 135; + +function transformState(state: VersionedData['data']) { + if ( + !hasProperty(state, 'PreferencesController') || + !isObject(state.PreferencesController) + ) { + global.sentry?.captureException?.( + new Error( + `Invalid PreferencesController state: ${typeof state.PreferencesController}`, + ), + ); + return state; + } + + const { PreferencesController } = state; + + const currentOptInStatus = + PreferencesController.preferences?.smartTransactionsOptInStatus; + + if ( + currentOptInStatus === undefined || + currentOptInStatus === null || + (currentOptInStatus === false && !hasExistingSmartTransactions(state)) + ) { + state.PreferencesController.preferences = { + ...state.PreferencesController.preferences, + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }; + } else { + state.PreferencesController.preferences = { + ...state.PreferencesController.preferences, + smartTransactionsMigrationApplied: true, + }; + } + + return state; +} + +function hasExistingSmartTransactions(state: VersionedData['data']): boolean { + const smartTransactions = + state?.SmartTransactionsController?.smartTransactionsState + ?.smartTransactions; + + if (!isObject(smartTransactions)) { + return false; + } + + return (smartTransactions[CHAIN_IDS.MAINNET] || []).length > 0; +} + +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 8700b833d8de..35b72816b388 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -158,6 +158,7 @@ const migrations = [ require('./133.1'), require('./133.2'), require('./134'), + require('./135'), ]; export default migrations; diff --git a/shared/constants/alerts.ts b/shared/constants/alerts.ts index bbf6318f448e..71b246db2fb2 100644 --- a/shared/constants/alerts.ts +++ b/shared/constants/alerts.ts @@ -2,6 +2,7 @@ export enum AlertTypes { unconnectedAccount = 'unconnectedAccount', web3ShimUsage = 'web3ShimUsage', invalidCustomNetwork = 'invalidCustomNetwork', + smartTransactionsMigration = 'smartTransactionsMigration', } /** @@ -10,6 +11,7 @@ export enum AlertTypes { export const TOGGLEABLE_ALERT_TYPES = [ AlertTypes.unconnectedAccount, AlertTypes.web3ShimUsage, + AlertTypes.smartTransactionsMigration, ]; export enum Web3ShimUsageAlertStates { diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index 9367c24853c6..f3f7bb922711 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -17,6 +17,7 @@ type SmartTransactionsMetaMaskState = { metamask: { preferences: { smartTransactionsOptInStatus?: boolean; + smartTransactionsMigrationApplied?: boolean; }; internalAccounts: { selectedAccount: string; @@ -72,6 +73,25 @@ export const getSmartTransactionsOptInStatusInternal = createSelector( }, ); +/** + * Returns whether the smart transactions migration has been applied to the user's settings. + * This specifically tracks if Migration 135 has been run, which enables Smart Transactions + * by default for users who have never interacted with the feature or who previously opted out + * with no STX activity. + * + * This should only be used for internal checks of the migration status, and not + * for determining overall Smart Transactions availability. + * + * @param state - The state object. + * @returns true if the migration has been applied to the user's settings, false if not or if unset. + */ +export const getSmartTransactionsMigrationAppliedInternal = createSelector( + getPreferences, + (preferences: { smartTransactionsMigrationApplied?: boolean }): boolean => { + return preferences?.smartTransactionsMigrationApplied ?? false; + }, +); + /** * 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 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 cae1a6ae8951..c4d176348946 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 @@ -63,14 +63,10 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionConfig": { - "maxRefreshCount": "number", "refreshRate": "number", + "maxRefreshCount": "number", "support": "boolean", - "chains": { - "0x1": "object", - "0xa4b1": "object", - "0xe708": "object" - } + "chains": { "0x1": "object", "0xa4b1": "object", "0xe708": "object" } } }, "srcTokens": {}, @@ -242,11 +238,10 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "tokenSortConfig": "object", "shouldShowAggregatedBalancePopover": "boolean", - "tokenNetworkFilter": { - "0x539": "boolean" - }, + "tokenNetworkFilter": { "0x539": "boolean" }, "redesignedConfirmationsEnabled": true, - "redesignedTransactionsEnabled": "boolean" + "redesignedTransactionsEnabled": "boolean", + "smartTransactionsMigrationApplied": "boolean" }, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", @@ -330,9 +325,7 @@ "TokenBalancesController": { "tokenBalances": "object" }, "TokenListController": { "tokenList": "object", - "tokensChainsCache": { - "0x539": "object" - }, + "tokensChainsCache": { "0x539": "object" }, "preventPollingOnNetworkRestart": false }, "TokenRatesController": { "marketData": "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 a306c63c70b6..76cc18653595 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,11 +37,10 @@ "isRedesignedConfirmationsDeveloperEnabled": "boolean", "tokenSortConfig": "object", "shouldShowAggregatedBalancePopover": "boolean", - "tokenNetworkFilter": { - "0x539": "boolean" - }, + "tokenNetworkFilter": { "0x539": "boolean" }, "redesignedConfirmationsEnabled": true, - "redesignedTransactionsEnabled": "boolean" + "redesignedTransactionsEnabled": "boolean", + "smartTransactionsMigrationApplied": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, @@ -176,9 +175,7 @@ "gasEstimateType": "none", "nonRPCGasFeeApisDisabled": "boolean", "tokenList": "object", - "tokensChainsCache": { - "0x539": "object" - }, + "tokensChainsCache": { "0x539": "object" }, "preventPollingOnNetworkRestart": false, "tokens": "object", "ignoredTokens": "object", @@ -280,14 +277,10 @@ "bridgeState": { "bridgeFeatureFlags": { "extensionConfig": { - "maxRefreshCount": "number", "refreshRate": "number", + "maxRefreshCount": "number", "support": "boolean", - "chains": { - "0x1": "object", - "0xa4b1": "object", - "0xe708": "object" - } + "chains": { "0x1": "object", "0xa4b1": "object", "0xe708": "object" } } }, "srcTokens": {}, diff --git a/test/e2e/tests/metrics/swaps.spec.js b/test/e2e/tests/metrics/swaps.spec.js index 6b73e3f400b7..df2137b0157f 100644 --- a/test/e2e/tests/metrics/swaps.spec.js +++ b/test/e2e/tests/metrics/swaps.spec.js @@ -90,6 +90,7 @@ async function mockSegmentAndMetaswapRequests(mockServer) { ]; } +// TODO: (MM-PENDING) These tests are planned for deprecation as part of swaps testing revamp describe('Swap Eth for another Token @no-mmi', function () { it('Completes a Swap between ETH and DAI after changing initial rate', async function () { const { initialBalanceInHex } = genRandInitBal(); diff --git a/test/e2e/tests/swaps/shared.ts b/test/e2e/tests/swaps/shared.ts index 3f3aff4447e5..fa55b3a7f0a8 100644 --- a/test/e2e/tests/swaps/shared.ts +++ b/test/e2e/tests/swaps/shared.ts @@ -190,6 +190,12 @@ export const checkActivityTransaction = async ( await driver.clickElement('[data-testid="popover-close"]'); }; +export const closeSmartTransactionsMigrationNotification = async ( + driver: Driver, +) => { + await driver.clickElement('[aria-label="Close"]'); +}; + export const checkNotification = async ( driver: Driver, options: { title: string; text: string }, @@ -216,9 +222,21 @@ export const checkNotification = async ( }; export const changeExchangeRate = async (driver: Driver) => { + // Ensure quote view button is present + await driver.waitForSelector('[data-testid="review-quote-view-all-quotes"]'); + + // Scroll button into view before clicking + await driver.executeScript(` + const element = document.querySelector('[data-testid="review-quote-view-all-quotes"]'); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + `); + + // Add small delay allowing for smooth scroll + await driver.delay(500); + + // Try to click the element await driver.clickElement('[data-testid="review-quote-view-all-quotes"]'); await driver.waitForSelector({ text: 'Quote details', tag: 'h2' }); - const networkFees = await driver.findElements( '[data-testid*="select-quote-popover-row"]', ); diff --git a/test/e2e/tests/swaps/swap-eth.spec.ts b/test/e2e/tests/swaps/swap-eth.spec.ts index 18d049e5de16..376d86fd2852 100644 --- a/test/e2e/tests/swaps/swap-eth.spec.ts +++ b/test/e2e/tests/swaps/swap-eth.spec.ts @@ -7,53 +7,54 @@ import { checkActivityTransaction, changeExchangeRate, mockEthDaiTrade, + closeSmartTransactionsMigrationNotification, } from './shared'; +// TODO: (MM-PENDING) These tests are planned for deprecation as part of swaps testing revamp describe('Swap Eth for another Token @no-mmi', function () { - it('Completes second Swaps while first swap is processing', async function () { - withFixturesOptions.ganacheOptions.miner.blockTime = 10; - + it('Completes a Swap between ETH and DAI after changing initial rate', async function () { await withFixtures( { ...withFixturesOptions, + testSpecificMock: mockEthDaiTrade, title: this.test?.fullTitle(), }, async ({ driver }) => { await unlockWallet(driver); + await buildQuote(driver, { - amount: 0.001, - swapTo: 'USDC', + amount: 2, + swapTo: 'DAI', }); + + // Close the STX notification immediately after buildQuote + // This ensures the UI is clear before we proceed with quote review + await closeSmartTransactionsMigrationNotification(driver); + await reviewQuote(driver, { - amount: 0.001, + amount: 2, swapFrom: 'TESTETH', - swapTo: 'USDC', - }); - await driver.clickElement({ text: 'Swap', tag: 'button' }); - await driver.clickElement({ text: 'View in activity', tag: 'button' }); - await buildQuote(driver, { - amount: 0.003, swapTo: 'DAI', }); + + // The changeExchangeRate function now includes scrolling logic + await changeExchangeRate(driver); + await reviewQuote(driver, { - amount: 0.003, + amount: 2, swapFrom: 'TESTETH', swapTo: 'DAI', + skipCounter: true, }); + await driver.clickElement({ text: 'Swap', tag: 'button' }); await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); await checkActivityTransaction(driver, { index: 0, - amount: '0.003', + amount: '2', swapFrom: 'TESTETH', swapTo: 'DAI', }); - await checkActivityTransaction(driver, { - index: 1, - amount: '0.001', - swapFrom: 'TESTETH', - swapTo: 'USDC', - }); }, ); }); diff --git a/test/e2e/tests/swaps/swaps-notifications.spec.ts b/test/e2e/tests/swaps/swaps-notifications.spec.ts index 134741d3683c..835b86d277e6 100644 --- a/test/e2e/tests/swaps/swaps-notifications.spec.ts +++ b/test/e2e/tests/swaps/swaps-notifications.spec.ts @@ -6,6 +6,7 @@ import { buildQuote, reviewQuote, checkNotification, + closeSmartTransactionsMigrationNotification, } from './shared'; async function mockSwapsTransactionQuote(mockServer: Mockttp) { @@ -80,6 +81,7 @@ describe('Swaps - notifications @no-mmi', function () { amount: 2, swapTo: 'INUINU', }); + await closeSmartTransactionsMigrationNotification(driver); await checkNotification(driver, { title: 'Potentially inauthentic token', text: 'INUINU is only verified on 1 source. Consider verifying it on Etherscan before proceeding.', diff --git a/ui/pages/confirmations/components/smart-transactions-banner-alert/index.ts b/ui/pages/confirmations/components/smart-transactions-banner-alert/index.ts new file mode 100644 index 000000000000..2ffa38054d60 --- /dev/null +++ b/ui/pages/confirmations/components/smart-transactions-banner-alert/index.ts @@ -0,0 +1 @@ +export { SmartTransactionsBannerAlert } from './smart-transactions-banner-alert'; diff --git a/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.tsx b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.tsx new file mode 100644 index 000000000000..79be7acec262 --- /dev/null +++ b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import type { Store } from '@reduxjs/toolkit'; +import { screen } from '@testing-library/react'; +import { TransactionType } from '@metamask/transaction-controller'; +import { ConfirmContext } from '../../context/confirm'; +import type { Confirmation, SignatureRequestType } from '../../types/confirm'; +import { renderWithProvider } from '../../../../../test/jest/rendering'; +import configureStore from '../../../../store/store'; +import { AlertTypes } from '../../../../../shared/constants/alerts'; +import { setAlertEnabledness } from '../../../../store/actions'; +import { SmartTransactionsBannerAlert } from './smart-transactions-banner-alert'; + +type TestConfirmContextValue = { + currentConfirmation: Confirmation; + isScrollToBottomCompleted: boolean; + setIsScrollToBottomCompleted: (isScrollToBottomCompleted: boolean) => void; +}; + +jest.mock('../../../../hooks/useI18nContext', () => ({ + useI18nContext: () => (key: string) => key, + __esModule: true, + default: () => (key: string) => key, +})); + +jest.mock('../../../../store/actions', () => ({ + setAlertEnabledness: jest.fn(() => ({ type: 'mock-action' })), +})); + +const renderWithConfirmContext = ( + component: React.ReactElement, + store: Store, + confirmationValue: TestConfirmContextValue = { + currentConfirmation: { + type: TransactionType.simpleSend, + id: '1', + } as SignatureRequestType, + isScrollToBottomCompleted: true, + setIsScrollToBottomCompleted: () => undefined, + }, +) => { + return renderWithProvider( + + {component} + , + store, + ); +}; + +describe('SmartTransactionsBannerAlert', () => { + const mockState = { + metamask: { + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: true, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }; + + it('renders banner when alert is enabled, STX is opted in, and migration is applied', () => { + const store = configureStore(mockState); + renderWithProvider(, store); + + expect( + screen.getByTestId('smart-transactions-banner-alert'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledTitle'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledDescription'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledLink'), + ).toBeInTheDocument(); + }); + + it('does not render when alert is disabled', () => { + const disabledState = { + metamask: { + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: false, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }; + const store = configureStore(disabledState); + renderWithProvider(, store); + + expect( + screen.queryByTestId('smart-transactions-banner-alert'), + ).not.toBeInTheDocument(); + }); + + it('does not render when migration has not been applied', () => { + const noMigrationState = { + metamask: { + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: true, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: false, + }, + }, + }; + const store = configureStore(noMigrationState); + renderWithProvider(, store); + + expect( + screen.queryByTestId('smart-transactions-banner-alert'), + ).not.toBeInTheDocument(); + }); + + it('dismisses banner when close button or link is clicked', () => { + const store = configureStore(mockState); + + // Test close button + const { unmount } = renderWithProvider( + , + store, + ); + screen.getByRole('button', { name: /close/iu }).click(); + expect(setAlertEnabledness).toHaveBeenCalledWith( + AlertTypes.smartTransactionsMigration, + false, + ); + + // Cleanup + unmount(); + jest.clearAllMocks(); + + // Test link + renderWithProvider(, store); + screen.getByText('smartTransactionsEnabledLink').click(); + expect(setAlertEnabledness).toHaveBeenCalledWith( + AlertTypes.smartTransactionsMigration, + false, + ); + }); + + it('renders banner when inside ConfirmContext with supported transaction type', () => { + const store = configureStore(mockState); + renderWithConfirmContext(, store); + + expect( + screen.getByTestId('smart-transactions-banner-alert'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledTitle'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledDescription'), + ).toBeInTheDocument(); + expect( + screen.getByText('smartTransactionsEnabledLink'), + ).toBeInTheDocument(); + }); + + it('does not render banner for unsupported transaction types', () => { + const store = configureStore(mockState); + const unsupportedConfirmation: TestConfirmContextValue = { + currentConfirmation: { + type: TransactionType.signTypedData, + id: '2', + } as SignatureRequestType, + isScrollToBottomCompleted: true, + setIsScrollToBottomCompleted: () => undefined, + }; + + renderWithConfirmContext( + , + store, + unsupportedConfirmation, + ); + + expect( + screen.queryByTestId('smart-transactions-banner-alert'), + ).not.toBeInTheDocument(); + }); + + describe('margin style tests', () => { + const store = configureStore(mockState); + + it('applies no styles with default margin type', () => { + renderWithConfirmContext(, store); + const alert = screen.getByTestId('smart-transactions-banner-alert'); + expect(alert).not.toHaveStyle({ margin: 0 }); + expect(alert).not.toHaveStyle({ marginTop: 0 }); + }); + + it('applies zero margin when marginType is "none"', () => { + renderWithConfirmContext( + , + store, + ); + const alert = screen.getByTestId('smart-transactions-banner-alert'); + expect(alert).toHaveStyle({ margin: 0 }); + }); + + it('applies zero top margin when marginType is "noTop"', () => { + renderWithConfirmContext( + , + store, + ); + const alert = screen.getByTestId('smart-transactions-banner-alert'); + expect(alert).toHaveStyle({ marginTop: 0 }); + }); + + it('applies only top margin when marginType is "onlyTop"', () => { + renderWithConfirmContext( + , + store, + ); + const alert = screen.getByTestId('smart-transactions-banner-alert'); + expect(alert).toHaveStyle({ margin: '16px 0px 0px 0px' }); + }); + }); + + it('handles being outside of ConfirmContext correctly', () => { + const store = configureStore(mockState); + + renderWithProvider(, store); + + expect( + screen.getByTestId('smart-transactions-banner-alert'), + ).toBeInTheDocument(); + }); + + it('automatically dismisses banner when Smart Transactions is manually disabled', () => { + const store = configureStore({ + metamask: { + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: true, + }, + preferences: { + smartTransactionsOptInStatus: false, + smartTransactionsMigrationApplied: true, + }, + }, + }); + + jest.clearAllMocks(); + + renderWithConfirmContext(, store); + + expect(setAlertEnabledness).toHaveBeenCalledTimes(1); + expect(setAlertEnabledness).toHaveBeenCalledWith( + AlertTypes.smartTransactionsMigration, + false, + ); + }); +}); diff --git a/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx new file mode 100644 index 000000000000..e9f28a25c318 --- /dev/null +++ b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx @@ -0,0 +1,124 @@ +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { TransactionType } from '@metamask/transaction-controller'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + BannerAlert, + ButtonLink, + Text, + BannerAlertSeverity, +} from '../../../../components/component-library'; +import { setAlertEnabledness } from '../../../../store/actions'; +import { AlertTypes } from '../../../../../shared/constants/alerts'; +import { SMART_TRANSACTIONS_LEARN_MORE_URL } from '../../../../../shared/constants/smartTransactions'; +import { FontWeight } from '../../../../helpers/constants/design-system'; +import { useConfirmContext } from '../../context/confirm'; +import { isCorrectDeveloperTransactionType } from '../../../../../shared/lib/confirmation.utils'; +import { + getSmartTransactionsOptInStatusInternal, + getSmartTransactionsMigrationAppliedInternal, +} from '../../../../../shared/modules/selectors/smart-transactions'; + +type MarginType = 'default' | 'none' | 'noTop' | 'onlyTop'; + +type SmartTransactionsBannerAlertProps = { + marginType?: MarginType; +}; + +export const SmartTransactionsBannerAlert: React.FC = + React.memo(({ marginType = 'default' }) => { + const t = useI18nContext(); + + let currentConfirmation; + try { + const context = useConfirmContext(); + currentConfirmation = context?.currentConfirmation; + } catch { + currentConfirmation = null; + } + + const alertEnabled = useSelector( + (state: { + metamask: { alertEnabledness?: { [key: string]: boolean } }; + }) => + state.metamask.alertEnabledness?.[ + AlertTypes.smartTransactionsMigration + ] !== false, + ); + + const smartTransactionsOptIn = useSelector( + getSmartTransactionsOptInStatusInternal, + ); + + const smartTransactionsMigrationApplied = useSelector( + getSmartTransactionsMigrationAppliedInternal, + ); + + const dismissAlert = useCallback(() => { + setAlertEnabledness(AlertTypes.smartTransactionsMigration, false); + }, []); + + React.useEffect(() => { + if (alertEnabled && !smartTransactionsOptIn) { + dismissAlert(); + } + }, [alertEnabled, smartTransactionsOptIn, dismissAlert]); + + const alertConditions = + alertEnabled && + smartTransactionsOptIn && + smartTransactionsMigrationApplied; + + const shouldRender = + currentConfirmation === null + ? alertConditions + : alertConditions && + isCorrectDeveloperTransactionType( + currentConfirmation?.type as TransactionType, + ); + + if (!shouldRender) { + return null; + } + + const getMarginStyle = () => { + switch (marginType) { + case 'none': + return { margin: 0 }; + case 'noTop': + return { marginTop: 0 }; + case 'onlyTop': + return { margin: 0, marginTop: 16 }; + default: + return undefined; + } + }; + + return ( + + + {t('smartTransactionsEnabledTitle')} + + + + {t('smartTransactionsEnabledLink')} + + {t('smartTransactionsEnabledDescription')} + + + ); + }); + +SmartTransactionsBannerAlert.displayName = 'SmartTransactionsBannerAlert'; + +export default SmartTransactionsBannerAlert; diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js index 5127003a81e8..99acb94bb754 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js @@ -5,6 +5,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import { PriorityLevels } from '../../../../../shared/constants/gas'; import { useGasFeeContext } from '../../../../contexts/gasFee'; import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { SmartTransactionsBannerAlert } from '../smart-transactions-banner-alert'; import { BannerAlert, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -81,6 +82,7 @@ const TransactionAlerts = ({ return (
+ {isSuspiciousResponse(txData?.securityProviderResponse) && ( { @@ -33,6 +35,13 @@ const STATE_MOCK = { ...mockNetworkState({ chainId: CHAIN_ID_MOCK, }), + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + [AlertTypes.smartTransactionsMigration]: { + state: ALERT_STATE.OPEN, }, }; @@ -40,12 +49,13 @@ function render({ componentProps = {}, useGasFeeContextValue = {}, submittedPendingTransactionsSelectorValue = null, + state = STATE_MOCK, }) { useGasFeeContext.mockReturnValue(useGasFeeContextValue); submittedPendingTransactionsSelector.mockReturnValue( submittedPendingTransactionsSelectorValue, ); - const store = configureStore(STATE_MOCK); + const store = configureStore(state); return renderWithProvider(, store); } @@ -558,3 +568,64 @@ describe('TransactionAlerts', () => { }); }); }); + +describe('Smart Transactions Migration Alert', () => { + it('shows when alert is enabled, opted in, and migration applied', () => { + const { getByTestId } = render({ + componentProps: { + txData: { + chainId: CHAIN_ID_MOCK, + txParams: { value: '0x1' }, + }, + }, + state: { + ...STATE_MOCK, + metamask: { + ...STATE_MOCK.metamask, + networkConfigurationsByChainId: { + [CHAIN_ID_MOCK]: { + chainId: CHAIN_ID_MOCK, + }, + }, + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: true, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }, + }); + expect(getByTestId('smart-transactions-banner-alert')).toBeInTheDocument(); + }); + + it('does not show when alert is disabled', () => { + const closedState = { + ...STATE_MOCK, + metamask: { + ...STATE_MOCK.metamask, + alertEnabledness: { + [AlertTypes.smartTransactionsMigration]: false, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }; + const store = configureStore(closedState); + const { queryByTestId } = renderWithProvider( + , + store, + ); + expect( + queryByTestId('smart-transactions-banner-alert'), + ).not.toBeInTheDocument(); + }); +}); 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 7dbcc2c3b0ab..cdcaabb1ce48 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 @@ -42,6 +42,7 @@ setBackgroundConnection({ getNextNonce: jest.fn(), updateTransaction: jest.fn(), getLastInteractedConfirmationInfo: jest.fn(), + setAlertEnabledness: jest.fn(), }); const mockTxParamsFromAddress = '0x123456789'; diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index 23e0030109ee..85c6e24b3d78 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -116,6 +116,9 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = `
+
+
+
+
+
+
`; + +exports[`Confirm should render SmartTransactionsBannerAlert for transaction types but not signature types 1`] = ` +
+
+
+
+
+
+
+
+ network logo +
+
+
+

+

+

+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+`; + +exports[`Confirm should render SmartTransactionsBannerAlert for transaction types but not signature types 2`] = ` +
+
+
+
+
+
+
+
+
+
+ Goerli logo +
+
+
+

+ Test Account +

+

+ Goerli +

+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+

+ Request from +

+
+
+ +
+
+
+
+
+

+ metamask.github.io +

+
+
+
+
+
+

+ Interacting with +

+
+
+
+
+
+
+
+

+ 0xCcCCc...ccccC +

+
+
+
+
+
+
+ +
+
+

+ Message +

+
+
+
+
+
+
+

+ Primary type: +

+
+
+
+

+ Mail +

+
+
+
+
+
+
+
+

+ Contents: +

+
+
+
+

+ Hello, Bob! +

+
+
+
+
+
+

+ From: +

+
+
+
+
+
+
+

+ Name: +

+
+
+
+

+ Cow +

+
+
+
+
+
+

+ Wallets: +

+
+
+
+
+
+
+

+ 0: +

+
+
+
+
+
+
+
+

+ 0xCD2a3...DD826 +

+
+
+
+
+
+
+

+ 1: +

+
+
+
+
+
+
+
+

+ 0xDeaDb...DbeeF +

+
+
+
+
+
+
+

+ 2: +

+
+
+
+
+
+
+
+

+ 0x06195...43896 +

+
+
+
+
+
+
+
+
+
+
+

+ To: +

+
+
+
+
+
+
+

+ 0: +

+
+
+
+
+
+
+

+ Name: +

+
+
+
+

+ Bob +

+
+
+
+
+
+

+ Wallets: +

+
+
+
+
+
+
+

+ 0: +

+
+
+
+
+
+
+
+

+ 0xbBbBB...bBBbB +

+
+
+
+
+
+
+

+ 1: +

+
+
+
+
+
+
+
+

+ 0xB0Bda...bEa57 +

+
+
+
+
+
+
+

+ 2: +

+
+
+
+
+
+
+
+

+ 0xB0B0b...00000 +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+`; diff --git a/ui/pages/confirmations/confirm/confirm.test.tsx b/ui/pages/confirmations/confirm/confirm.test.tsx index 158884fc36a0..ded2c0a9bb3f 100644 --- a/ui/pages/confirmations/confirm/confirm.test.tsx +++ b/ui/pages/confirmations/confirm/confirm.test.tsx @@ -198,4 +198,44 @@ describe('Confirm', () => { expect(container).toMatchSnapshot(); }); }); + + it('should render SmartTransactionsBannerAlert for transaction types but not signature types', async () => { + // Test with a transaction type + const mockStateTransaction = { + ...mockState, + metamask: { + ...mockState.metamask, + alertEnabledness: { + smartTransactionsMigration: true, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }; + + const mockStoreTransaction = + configureMockStore(middleware)(mockStateTransaction); + + await act(async () => { + const { container } = renderWithConfirmContextProvider( + , + mockStoreTransaction, + ); + expect(container).toMatchSnapshot(); + }); + + // Test with a signature type (reuse existing mock) + const mockStateTypedSign = getMockTypedSignConfirmState(); + const mockStoreSign = configureMockStore(middleware)(mockStateTypedSign); + + await act(async () => { + const { container } = renderWithConfirmContextProvider( + , + mockStoreSign, + ); + expect(container).toMatchSnapshot(); + }); + }); }); diff --git a/ui/pages/confirmations/confirm/confirm.tsx b/ui/pages/confirmations/confirm/confirm.tsx index ed2cb9047271..094c8040a5aa 100644 --- a/ui/pages/confirmations/confirm/confirm.tsx +++ b/ui/pages/confirmations/confirm/confirm.tsx @@ -16,12 +16,14 @@ import { Header } from '../components/confirm/header'; import { Info } from '../components/confirm/info'; import { LedgerInfo } from '../components/confirm/ledger-info'; import { NetworkChangeToast } from '../components/confirm/network-change-toast'; +import { SmartTransactionsBannerAlert } from '../components/smart-transactions-banner-alert'; import { PluggableSection } from '../components/confirm/pluggable-section'; import ScrollToBottom from '../components/confirm/scroll-to-bottom'; import { Title } from '../components/confirm/title'; import EditGasFeePopover from '../components/edit-gas-fee-popover'; import { ConfirmContextProvider, useConfirmContext } from '../context/confirm'; import { ConfirmNav } from '../components/confirm/nav/nav'; +import { Box } from '../../../components/component-library'; const EIP1559TransactionGasModal = () => { return ( @@ -53,6 +55,9 @@ const Confirm = () => (
+ + + { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 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 e85cbfc6eb09..6857bb10d79b 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -144,6 +144,7 @@ import SelectedToken from '../selected-token/selected-token'; import ListWithSearch from '../list-with-search/list-with-search'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import useBridging from '../../../hooks/bridge/useBridging'; +import { SmartTransactionsBannerAlert } from '../../confirmations/components/smart-transactions-banner-alert'; import QuotesLoadingAnimation from './quotes-loading-animation'; import ReviewQuote from './review-quote'; @@ -822,6 +823,9 @@ export default function PrepareSwapPage({ {tokenForImport && isImportTokenModalOpen && ( )} + + + { expect(bridgeButton).toBeNull(); }); + + describe('Smart Transactions Migration Banner', () => { + it('shows banner when alert is enabled, opted in, and migration applied', () => { + const mockStore = createSwapsMockStore(); + const store = configureMockStore(middleware)({ + ...mockStore, + metamask: { + ...mockStore.metamask, + alertEnabledness: { + smartTransactionsMigration: true, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }); + + const props = createProps(); + const { getByTestId } = renderWithProvider( + , + store, + ); + + expect( + getByTestId('smart-transactions-banner-alert'), + ).toBeInTheDocument(); + }); + + it('does not show banner when alert is disabled', () => { + const mockStore = createSwapsMockStore(); + const store = configureMockStore(middleware)({ + ...mockStore, + metamask: { + ...mockStore.metamask, + alertEnabledness: { + smartTransactionsMigration: false, + }, + preferences: { + smartTransactionsOptInStatus: true, + smartTransactionsMigrationApplied: true, + }, + }, + }); + + const props = createProps(); + const { queryByTestId } = renderWithProvider( + , + store, + ); + + expect( + queryByTestId('smart-transactions-banner-alert'), + ).not.toBeInTheDocument(); + }); + }); }); From 80e6e147b52c478e893dc3a046492bb908f81426 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:42:43 +0100 Subject: [PATCH 36/71] fix: Add missing allowed action to the `SmartTransactionsController` messenger (#29473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add missing allowed action `NetworkController:getState` to the SmartTransactionsController messenger. This fixes and error in smart transaction publish hook, falling back to regular transaction submission `Error: Action missing from allow list: NetworkController:getState`. ![image](https://github.com/user-attachments/assets/952d8ae3-c13c-4293-b096-944b0c06c30a) ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29473?quickstart=1) ## **Related issues** Fixes: ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0e568424a69c..49517145bcd7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2270,7 +2270,10 @@ export default class MetamaskController extends EventEmitter { const smartTransactionsControllerMessenger = this.controllerMessenger.getRestricted({ name: 'SmartTransactionsController', - allowedActions: ['NetworkController:getNetworkClientById'], + allowedActions: [ + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + ], allowedEvents: ['NetworkController:stateChange'], }); this.smartTransactionsController = new SmartTransactionsController({ From 1ed30c576439bc369d29d0e32375cff180af2800 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 7 Jan 2025 11:41:23 -0330 Subject: [PATCH 37/71] ci: Fix `metamaskbot` comment test build links (#29403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Various build links in the `metamaskbot` PR comment were broken. All links have been fixed except beta (which is trickier to fix, requires more substantial changes to the beta workflows, tracked as #29404). The beta link has been removed until we can fix it. Additionally, the Firefox MMI build link has been removed (this build does not work) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29403?quickstart=1) ## **Related issues** Fixes: #29402 ## **Manual testing steps** * Test that all build links work ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 15 +++-- development/metamaskbot-build-announce.js | 80 +++++++++-------------- 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 607571e36eb1..38ccd80d7059 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -151,7 +151,6 @@ workflows: requires: - prep-deps - prep-build-test-flask-mv2: - <<: *main_master_rc_only requires: - prep-deps - prep-build-test-mmi: @@ -283,10 +282,14 @@ workflows: - prep-deps - prep-build - prep-build-mv2 - - trigger-beta-build - prep-build-mmi - prep-build-flask - prep-build-flask-mv2 + - prep-build-test + - prep-build-test-mv2 + - prep-build-test-flask + - prep-build-test-flask-mv2 + - trigger-beta-build - prep-build-storybook - prep-build-ts-migration-dashboard - benchmark @@ -701,10 +704,10 @@ jobs: name: Build extension for testing command: yarn build:test:flask:mv2 - run: - name: Move test build to 'dist-test-flask' to avoid conflict with production build + name: Move test build to 'dist-test-flask-mv2' to avoid conflict with production build command: mv ./dist ./dist-test-flask-mv2 - run: - name: Move test zips to 'builds-test-flask' to avoid conflict with production build + name: Move test zips to 'builds-test-flask-mv2' to avoid conflict with production build command: mv ./builds ./builds-test-flask-mv2 - persist_to_workspace: root: . @@ -1306,8 +1309,12 @@ jobs: destination: builds-mv2 - store_artifacts: path: builds-test + - store_artifacts: + path: builds-test-mv2 - store_artifacts: path: builds-test-flask + - store_artifacts: + path: builds-test-flask-mv2 - store_artifacts: path: test-artifacts destination: test-artifacts diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index daf9289a0201..dc8c2ece4be6 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -65,50 +65,34 @@ async function start() { // build the github comment content // links to extension builds - const platforms = ['chrome', 'firefox']; - const buildLinks = platforms - .map((platform) => { - const url = - platform === 'firefox' - ? `${BUILD_LINK_BASE}/builds-mv2/metamask-${platform}-${VERSION}.zip` - : `${BUILD_LINK_BASE}/builds/metamask-${platform}-${VERSION}.zip`; - return `${platform}`; - }) - .join(', '); - const betaBuildLinks = `chrome`; - const flaskBuildLinks = platforms - .map((platform) => { - const url = - platform === 'firefox' - ? `${BUILD_LINK_BASE}/builds-flask-mv2/metamask-flask-${platform}-${VERSION}-flask.0.zip` - : `${BUILD_LINK_BASE}/builds-flask/metamask-flask-${platform}-${VERSION}-flask.0.zip`; - return `${platform}`; - }) - .join(', '); - const mmiBuildLinks = platforms - .map((platform) => { - const url = `${BUILD_LINK_BASE}/builds-mmi/metamask-mmi-${platform}-${VERSION}-mmi.0.zip`; - return `${platform}`; - }) - .join(', '); - const testBuildLinks = platforms - .map((platform) => { - const url = - platform === 'firefox' - ? `${BUILD_LINK_BASE}/builds-test-mv2/metamask-${platform}-${VERSION}.zip` - : `${BUILD_LINK_BASE}/builds-test/metamask-${platform}-${VERSION}.zip`; - return `${platform}`; - }) - .join(', '); - const testFlaskBuildLinks = platforms - .map((platform) => { - const url = - platform === 'firefox' - ? `${BUILD_LINK_BASE}/builds-test-flask-mv2/metamask-flask-${platform}-${VERSION}-flask.0.zip` - : `${BUILD_LINK_BASE}/builds-test-flask/metamask-flask-${platform}-${VERSION}-flask.0.zip`; + const buildMap = { + builds: { + chrome: `${BUILD_LINK_BASE}/builds/metamask-chrome-${VERSION}.zip`, + firefox: `${BUILD_LINK_BASE}/builds-mv2/metamask-firefox-${VERSION}.zip`, + }, + 'builds (flask)': { + chrome: `${BUILD_LINK_BASE}/builds-flask/metamask-flask-chrome-${VERSION}-flask.0.zip`, + firefox: `${BUILD_LINK_BASE}/builds-flask-mv2/metamask-flask-firefox-${VERSION}-flask.0.zip`, + }, + 'builds (MMI)': { + chrome: `${BUILD_LINK_BASE}/builds-mmi/metamask-mmi-chrome-${VERSION}-mmi.0.zip`, + }, + 'builds (test)': { + chrome: `${BUILD_LINK_BASE}/builds-test/metamask-chrome-${VERSION}.zip`, + firefox: `${BUILD_LINK_BASE}/builds-test-mv2/metamask-firefox-${VERSION}.zip`, + }, + 'builds (test-flask)': { + chrome: `${BUILD_LINK_BASE}/builds-test-flask/metamask-flask-chrome-${VERSION}-flask.0.zip`, + firefox: `${BUILD_LINK_BASE}/builds-test-flask-mv2/metamask-flask-firefox-${VERSION}-flask.0.zip`, + }, + }; + + const buildContentRows = Object.entries(buildMap).map(([label, builds]) => { + const buildLinks = Object.entries(builds).map(([platform, url]) => { return `${platform}`; - }) - .join(', '); + }); + return `${label}: ${buildLinks.join(', ')}`; + }); // links to bundle browser builds const bundles = {}; @@ -162,12 +146,7 @@ async function start() { const allArtifactsUrl = `https://circleci.com/gh/MetaMask/metamask-extension/${CIRCLE_BUILD_NUM}#artifacts/containers/0`; const contentRows = [ - `builds: ${buildLinks}`, - `builds (beta): ${betaBuildLinks}`, - `builds (flask): ${flaskBuildLinks}`, - `builds (MMI): ${mmiBuildLinks}`, - `builds (test): ${testBuildLinks}`, - `builds (test-flask): ${testFlaskBuildLinks}`, + ...buildContentRows, `build viz: ${depVizLink}`, `mv3: ${bundleSizeStatsLink}`, `mv2: ${userActionsStatsLink}`, @@ -185,8 +164,9 @@ async function start() { const exposedContent = `Builds ready [${SHORT_SHA1}]`; const artifactsBody = `
${exposedContent}${hiddenContent}
\n\n`; + const benchmarkPlatforms = ['chrome']; const benchmarkResults = {}; - for (const platform of platforms) { + for (const platform of benchmarkPlatforms) { const benchmarkPath = path.resolve( __dirname, '..', From 481505f692bccd7bb2f4f78d0f40d4b7c1799b13 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 8 Jan 2025 00:46:48 +0900 Subject: [PATCH 38/71] fix: xchain linea bugs (#29409) 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/29409?quickstart=1) This PR fixes a couple of issues and does the following: 1. Shows a loading spinner on the Bridge button after submitting 2. Does not wait 5 sec to submit a bridge tx on Linea if there is no approval tx ## **Related issues** Fixes: ## **Manual testing steps** Gas token 1. Start a bridge from Linea to any chain for ETH 3. Observe that you are redirected to Activity page almost instantly ERC20 1. Start a bridge from Linea to any chain for an ERC20 2. Observe that a loading spinner shows on the button 4. Observe you are redirected to Activity page after 5 sec ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/24445f52-ddaf-4d08-b3ce-755c10b3f976 https://github.com/user-attachments/assets/ae93204d-80f6-4beb-aff5-b8274e4541e7 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts | 3 ++- ui/pages/bridge/prepare/bridge-cta-button.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index e34c4e9400c6..dee7139a623f 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -93,7 +93,8 @@ export default function useSubmitBridgeTransaction() { CHAIN_IDS.LINEA_GOERLI, CHAIN_IDS.LINEA_SEPOLIA, ] as Hex[] - ).includes(srcChainId) + ).includes(srcChainId) && + quoteResponse?.approval ) { debugLog( 'Delaying submitting bridge tx to make Linea confirmation more likely', diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 8e45f90cd1db..7b8fdf397e5a 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -161,7 +161,7 @@ export const BridgeCTAButton = ({ variant={TextVariant.bodyMd} data-testid="bridge-cta-button" style={{ boxShadow: 'none' }} - onClick={() => { + onClick={async () => { if (activeQuote && isTxSubmittable && !isSubmitting) { try { // We don't need to worry about setting to false if the tx submission succeeds @@ -179,7 +179,7 @@ export const BridgeCTAButton = ({ ...tradeProperties, }, }); - submitBridgeTransaction(activeQuote); + await submitBridgeTransaction(activeQuote); } finally { setIsSubmitting(false); } From d0775ad14d990e3c4fc53698582dce5c37df20ad Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:51:35 -0800 Subject: [PATCH 39/71] fix: 'Incomplete Asset Displayed' called excessively via useTrackERC20WithoutDecimalInformation + clean: token details logic (#29320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes and cleanup around confirmation: - useTrackERC20WithoutDecimalInformation should only call trackEvent once per use - useGetTokenStandardAndDetails should consistently call useAsyncResult by removing early return - add missing tokenId dep prop to PermitSimulationValueDisplay [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29320?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3362 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/modules/transaction.utils.ts | 4 +- .../value-display/value-display.test.tsx | 26 ++++------ .../value-display/value-display.tsx | 4 +- .../hooks/useGetTokenStandardAndDetails.ts | 17 +++---- ...rackERC20WithoutDecimalInformation.test.ts | 4 +- .../useTrackERC20WithoutDecimalInformation.ts | 51 +++++++++++-------- 6 files changed, 55 insertions(+), 51 deletions(-) diff --git a/shared/modules/transaction.utils.ts b/shared/modules/transaction.utils.ts index 910fe9b6d4be..eb7a987f360f 100644 --- a/shared/modules/transaction.utils.ts +++ b/shared/modules/transaction.utils.ts @@ -294,10 +294,10 @@ function extractLargeMessageValue(dataToParse: string): string | undefined { } /** - * JSON.parse has a limitation which coerces values to scientific notation if numbers are greator than + * JSON.parse has a limitation which coerces values to scientific notation if numbers are greater than * Number.MAX_SAFE_INTEGER. This can cause a loss in precision. * - * Aside from precision concerns, if the value returned was a large number greator than 15 digits, + * Aside from precision concerns, if the value returned was a large number greater than 15 digits, * e.g. 3.000123123123121e+26, passing the value to BigNumber will throw the error: * Error: new BigNumber() number type has more than 15 significant digits * diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.test.tsx index 453f4a1b6d79..8c912eb1afd1 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.test.tsx @@ -4,7 +4,7 @@ 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 { MetaMetricsContext } from '../../../../../../../../contexts/metametrics'; import PermitSimulationValueDisplay from './value-display'; jest.mock('../../../../../../../../store/actions', () => { @@ -15,13 +15,6 @@ jest.mock('../../../../../../../../store/actions', () => { }; }); -jest.mock( - '../../../../../../hooks/useTrackERC20WithoutDecimalInformation', - () => { - return jest.fn(); - }, -); - describe('PermitSimulationValueDisplay', () => { it('renders component correctly', async () => { const mockStore = configureMockStore([])(mockState); @@ -43,18 +36,21 @@ describe('PermitSimulationValueDisplay', () => { it('should invoke method to track missing decimal information for ERC20 tokens', async () => { const mockStore = configureMockStore([])(mockState); + const mockTrackEvent = jest.fn(); await act(async () => { renderWithProvider( - , + + + , mockStore, ); - - expect(useTrackERC20WithoutDecimalInformation).toHaveBeenCalled(); }); + + expect(mockTrackEvent).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.tsx index 1d59418d80c2..720b19a29086 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/value-display/value-display.tsx @@ -92,7 +92,7 @@ const PermitSimulationValueDisplay: React.FC< return exchangeRate.times(tokenAmount).toNumber(); } return undefined; - }, [exchangeRate, tokenDecimals, value]); + }, [exchangeRate, tokenDecimals, tokenId, value]); const { tokenValue, tokenValueMaxPrecision, shouldShowUnlimitedValue } = useMemo(() => { @@ -113,7 +113,7 @@ const PermitSimulationValueDisplay: React.FC< canDisplayValueAsUnlimited && Number(value) > TOKEN_VALUE_UNLIMITED_THRESHOLD, }; - }, [tokenDecimals, value]); + }, [tokenDecimals, tokenId, value]); /** Temporary error capturing as we are building out Permit Simulations */ if (!tokenContract) { diff --git a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts index b4ad2b5d51ef..b68430fe3dd0 100644 --- a/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts +++ b/ui/pages/confirmations/hooks/useGetTokenStandardAndDetails.ts @@ -18,17 +18,16 @@ import { export const useGetTokenStandardAndDetails = ( tokenAddress?: Hex | string | undefined, ) => { - if (!tokenAddress) { - return { decimalsNumber: undefined }; - } + const { value: details } = + useAsyncResult(async () => { + if (!tokenAddress) { + return Promise.resolve(null); + } - const { value: details } = useAsyncResult( - async () => - (await memoizedGetTokenStandardAndDetails( + return (await memoizedGetTokenStandardAndDetails( tokenAddress, - )) as TokenDetailsERC20, - [tokenAddress], - ); + )) as TokenDetailsERC20; + }, [tokenAddress]); if (!details) { return { decimalsNumber: undefined }; diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts index 9c98432918e2..dc7ec929072c 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -22,7 +22,7 @@ describe('useTrackERC20WithoutDecimalInformation', () => { const trackEventMock = jest.fn(); - it('should invoke trackEvent method', () => { + it('should invoke trackEvent method only once per instance of the hook', () => { useContextMock.mockImplementation((context) => { if (context === MetaMetricsContext) { return trackEventMock; @@ -36,6 +36,6 @@ describe('useTrackERC20WithoutDecimalInformation', () => { } as TokenDetailsERC20), ); - expect(trackEventMock).toHaveBeenCalled(); + expect(trackEventMock).toHaveBeenCalledTimes(1); }); }); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts index 27417613d3cc..b6924e7869cc 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -1,4 +1,4 @@ -import { useContext, useEffect } from 'react'; +import { useContext, useEffect, useRef } from 'react'; import { Hex } from '@metamask/utils'; import { @@ -26,32 +26,41 @@ const useTrackERC20WithoutDecimalInformation = ( metricLocation = MetaMetricsEventLocation.SignatureConfirmation, ) => { const trackEvent = useContext(MetaMetricsContext); + const hasTracked = useRef(false); useEffect(() => { - if (chainId === undefined || tokenDetails === undefined) { + if ( + chainId === undefined || + tokenDetails === undefined || + hasTracked.current + ) { 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: 'not_available', - asset_address: tokenAddress, - asset_type: TokenStandard.ERC20, - chain_id: chainId, - location: metricLocation, - ui_customizations: [ - MetaMetricsEventUiCustomization.RedesignedConfirmation, - ], - }, - }); - } + if (standard !== TokenStandard.ERC20) { + return; + } + + const parsedDecimals = parseTokenDetailDecimals(decimals); + if (parsedDecimals === undefined) { + trackEvent({ + event: MetaMetricsEventName.SimulationIncompleteAssetDisplayed, + category: MetaMetricsEventCategory.Confirmations, + properties: { + token_decimals_available: 'not_available', + asset_address: tokenAddress, + asset_type: TokenStandard.ERC20, + chain_id: chainId, + location: metricLocation, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + }); + hasTracked.current = true; } - }, [tokenDetails, chainId, tokenAddress, trackEvent]); + }, [tokenDetails, chainId, metricLocation, tokenAddress, trackEvent]); }; export default useTrackERC20WithoutDecimalInformation; From 9deeaddf486dc834cdc3636fa0e6401fb7663a61 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 8 Jan 2025 02:30:47 +0900 Subject: [PATCH 40/71] fix: hide you received until bridge tx done (#29411) 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/29411?quickstart=1) This PR fixes an issue where the "You Received" row in the Activity item for a bridge tx would be partially filled in. Now we just hide it until the bridge tx completes and we have all the necessary data to display the row properly. ## **Related issues** Fixes: ## **Manual testing steps** 1. Start a bridge tx 2. Get navigated to Activity list 3. Click on bridge tx 4. Observe that "You received" is not present until the bridge is completed. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/e8c3eb2e-9fd7-429b-a119-319defc42bda ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../transaction-details.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/pages/bridge/transaction-details/transaction-details.tsx b/ui/pages/bridge/transaction-details/transaction-details.tsx index 762e2c1218d9..ad8e0566b5f3 100644 --- a/ui/pages/bridge/transaction-details/transaction-details.tsx +++ b/ui/pages/bridge/transaction-details/transaction-details.tsx @@ -397,22 +397,25 @@ const CrossChainSwapTxDetails = () => { } /> - - {t('bridgeTxDetailsTokenAmountOnChain', [ - bridgeAmountReceived, - bridgeHistoryItem?.quote.destAsset.symbol, - ])} - {destNetworkIconName} - - } - /> + {bridgeAmountReceived && + bridgeHistoryItem?.quote.destAsset.symbol && ( + + {t('bridgeTxDetailsTokenAmountOnChain', [ + bridgeAmountReceived, + bridgeHistoryItem.quote.destAsset.symbol, + ])} + {destNetworkIconName} + + } + /> + )} Date: Tue, 7 Jan 2025 22:07:44 +0100 Subject: [PATCH 41/71] fix: import all detected tokens automatically (#29357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Now that we have most users autodetect tokens, we can make the UX even more streamlined. This PR automatically add in tokens we detect on behalf of users. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29357?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMASSETS-439 ## **Manual testing steps** 1. import an account that have tokens 2. Notice that tokens are imported automatically Make sure the token detection is working properly: 1. Go to settings and turn off "autodetect tokens" setting 2. Add a new account that have tokens 3. Notice tokens are not added unless you turn the setting back ON ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/3fc491d0-c7ae-48da-ae3d-a0a6e4af0f62 ### **After** https://github.com/user-attachments/assets/61938975-a476-4fd6-a4cf-183c5fb8d96e ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/assets/asset-list/asset-list.test.tsx | 12 ++ .../app/assets/asset-list/asset-list.tsx | 136 ++++++++++++++---- .../account-overview-eth.test.tsx | 1 + .../account-overview-non-evm.test.tsx | 1 + 4 files changed, 120 insertions(+), 30 deletions(-) diff --git a/ui/components/app/assets/asset-list/asset-list.test.tsx b/ui/components/app/assets/asset-list/asset-list.test.tsx index 00a47df1c633..6b6dbbee88ad 100644 --- a/ui/components/app/assets/asset-list/asset-list.test.tsx +++ b/ui/components/app/assets/asset-list/asset-list.test.tsx @@ -72,6 +72,18 @@ jest.mock('../../../../store/actions', () => { })), tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'), tokenBalancesStopPollingByPollingToken: jest.fn(), + addImportedTokens: jest.fn(), + }; +}); + +// Mock the dispatch function +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: () => mockDispatch, }; }); diff --git a/ui/components/app/assets/asset-list/asset-list.tsx b/ui/components/app/assets/asset-list/asset-list.tsx index 19d9ecdd4c0d..5ac7a84e0b29 100644 --- a/ui/components/app/assets/asset-list/asset-list.tsx +++ b/ui/components/app/assets/asset-list/asset-list.tsx @@ -1,14 +1,17 @@ -import React, { useContext, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useContext, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Token } from '@metamask/assets-controllers'; +import { NetworkConfiguration } from '@metamask/network-controller'; import TokenList from '../token-list'; import { PRIMARY } from '../../../../helpers/constants/common'; import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; import { getAllDetectedTokensForSelectedAddress, getDetectedTokensInCurrentNetwork, - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, getIsTokenNetworkFilterEqualCurrentNetwork, getSelectedAccount, + getSelectedAddress, + getUseTokenDetection, } from '../../../../selectors'; import { getMultichainIsEvm, @@ -23,9 +26,10 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, + MetaMetricsTokenEventSource, } from '../../../../../shared/constants/metametrics'; import DetectedToken from '../../detected-token/detected-token'; -import { DetectedTokensBanner, ReceiveModal } from '../../../multichain'; +import { ReceiveModal } from '../../../multichain'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -35,6 +39,16 @@ import { } from '../../../multichain/ramps-card/ramps-card'; import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; ///: END:ONLY_INCLUDE_IF +import { + getCurrentChainId, + getNetworkConfigurationsByChainId, + getSelectedNetworkClientId, +} from '../../../../../shared/modules/selectors/networks'; +import { addImportedTokens } from '../../../../store/actions'; +import { + AssetType, + TokenStandard, +} from '../../../../../shared/constants/transaction'; import AssetListControlBar from './asset-list-control-bar'; import NativeToken from './native-token'; @@ -54,6 +68,7 @@ export type AssetListProps = { }; const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { + const dispatch = useDispatch(); const [showDetectedTokens, setShowDetectedTokens] = useState(false); const selectedAccount = useSelector(getSelectedAccount); const t = useI18nContext(); @@ -74,14 +89,19 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { }); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || []; - const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector( - getIstokenDetectionInactiveOnNonMainnetSupportedNetwork, - ); const isTokenNetworkFilterEqualCurrentNetwork = useSelector( getIsTokenNetworkFilterEqualCurrentNetwork, ); + const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector( + getNetworkConfigurationsByChainId, + ); + const networkClientId = useSelector(getSelectedNetworkClientId); + const selectedAddress = useSelector(getSelectedAddress); + const useTokenDetection = useSelector(getUseTokenDetection); + const currentChainId = useSelector(getCurrentChainId); + const [showFundingMethodModal, setShowFundingMethodModal] = useState(false); const [showReceiveModal, setShowReceiveModal] = useState(false); @@ -104,32 +124,88 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => { // for EVM assets const shouldShowTokensLinks = showTokensLinks ?? isEvm; - const detectedTokensMultichain = useSelector( - getAllDetectedTokensForSelectedAddress, - ); + const detectedTokensMultichain: { + [key: `0x${string}`]: Token[]; + } = useSelector(getAllDetectedTokensForSelectedAddress); + + const multichainDetectedTokensLength = Object.values( + detectedTokensMultichain || {}, + ).reduce((acc, tokens) => acc + tokens.length, 0); + + // Add detected tokens to sate + useEffect(() => { + const importAllDetectedTokens = async () => { + // If autodetect tokens toggle is OFF, return + if (!useTokenDetection) { + return; + } + // TODO add event for MetaMetricsEventName.TokenAdded + + if ( + process.env.PORTFOLIO_VIEW && + !isTokenNetworkFilterEqualCurrentNetwork + ) { + const importPromises = Object.entries(detectedTokensMultichain).map( + async ([networkId, tokens]) => { + const chainConfig = allNetworks[networkId as `0x${string}`]; + const { defaultRpcEndpointIndex } = chainConfig; + const { networkClientId: networkInstanceId } = + chainConfig.rpcEndpoints[defaultRpcEndpointIndex]; + + await dispatch( + addImportedTokens(tokens as Token[], networkInstanceId), + ); + tokens.forEach((importedToken) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: chainConfig.chainId, + }, + }); + }); + }, + ); + + await Promise.all(importPromises); + } else if (detectedTokens.length > 0) { + await dispatch(addImportedTokens(detectedTokens, networkClientId)); + detectedTokens.forEach((importedToken: Token) => { + trackEvent({ + event: MetaMetricsEventName.TokenAdded, + category: MetaMetricsEventCategory.Wallet, + sensitiveProperties: { + token_symbol: importedToken.symbol, + token_contract_address: importedToken.address, + token_decimal_precision: importedToken.decimals, + source: MetaMetricsTokenEventSource.Detected, + token_standard: TokenStandard.ERC20, + asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: currentChainId, + }, + }); + }); + } + }; + importAllDetectedTokens(); + }, [ + isTokenNetworkFilterEqualCurrentNetwork, + selectedAddress, + networkClientId, + detectedTokens.length, + multichainDetectedTokensLength, + ]); - const totalTokens = - process.env.PORTFOLIO_VIEW && - !isTokenNetworkFilterEqualCurrentNetwork && - detectedTokensMultichain - ? (Object.values(detectedTokensMultichain).reduce( - // @ts-expect-error TS18046: 'tokenArray' is of type 'unknown' - (count, tokenArray) => count + tokenArray.length, - 0, - ) as number) - : detectedTokens.length; return ( <> - {totalTokens && - totalTokens > 0 && - !isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? ( - setShowDetectedTokens(true)} - margin={4} - marginBottom={1} - /> - ) : null} ({ setTokenNetworkFilter: jest.fn(), updateSlides: jest.fn(), removeSlide: jest.fn(), + addImportedTokens: jest.fn(), })); // Mock the dispatch function diff --git a/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx index df7c2ef56d69..68a7978ffb53 100644 --- a/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-non-evm.test.tsx @@ -15,6 +15,7 @@ jest.mock('../../../store/actions', () => ({ setTokenNetworkFilter: jest.fn(), updateSlides: jest.fn(), removeSlide: jest.fn(), + addImportedTokens: jest.fn(), })); // Mock the dispatch function From dabf1732e2a5448cc9935bc48aa9d34c8f7378b8 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 8 Jan 2025 06:19:39 +0900 Subject: [PATCH 42/71] chore: remove second inner scroll bar from tx details (#29412) 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/29412?quickstart=1) This PR removes the 2nd inner scrollbar from Bridge tx details. ## **Related issues** Fixes: ## **Manual testing steps** 1. Do a bridge tx 2. Get navigated to Activity list 3. Click on a bridge tx 4. Observe only 1 scroll bar ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-20 at 5 03 15 PM](https://github.com/user-attachments/assets/52904de2-7264-4959-bf23-6301eb1b7000) ### **After** ![Screenshot 2024-12-20 at 5 19 06 PM](https://github.com/user-attachments/assets/744401eb-80bf-4726-8ee4-844068a54978) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../bridge/transaction-details/index.scss | 4 + .../transaction-details.tsx | 371 +++++++++--------- 2 files changed, 186 insertions(+), 189 deletions(-) diff --git a/ui/pages/bridge/transaction-details/index.scss b/ui/pages/bridge/transaction-details/index.scss index 7832fffdd6a5..8f7a53abac4c 100644 --- a/ui/pages/bridge/transaction-details/index.scss +++ b/ui/pages/bridge/transaction-details/index.scss @@ -1,4 +1,8 @@ .bridge-transaction-details { + &__content { + overflow-y: hidden; + } + &__icon-loading { animation: loading-dot 1.2s linear infinite; } diff --git a/ui/pages/bridge/transaction-details/transaction-details.tsx b/ui/pages/bridge/transaction-details/transaction-details.tsx index ad8e0566b5f3..48804dd3283f 100644 --- a/ui/pages/bridge/transaction-details/transaction-details.tsx +++ b/ui/pages/bridge/transaction-details/transaction-details.tsx @@ -254,208 +254,201 @@ const CrossChainSwapTxDetails = () => { ); return ( -
-
-
history.goBack()} - /> - } +
+
history.goBack()} + /> + } + > + {t('bridge')} details +
+ + - {t('bridge')} details -
- + {/* Delayed banner */} + {isDelayed && ( + + + {t('bridgeTxDetailsDelayedDescription')}  + { + trackEvent( + { + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.SupportLinkClicked, + properties: { + url: SUPPORT_REQUEST_LINK, + location: 'Bridge Tx Details', + }, + }, + { + contextPropsIntoEventProperties: [ + MetaMetricsContextProp.PageTitle, + ], + }, + ); + }} + > + {t('bridgeTxDetailsDelayedDescriptionSupport')} + + . + + + )} + + {/* Bridge step list */} + {status !== StatusTypes.COMPLETE && + (bridgeHistoryItem || srcChainTxMeta) && ( + + )} + + {/* Links to block explorers */} + + + + + {/* Bridge tx details */} - {/* Delayed banner */} - {isDelayed && ( - - - {t('bridgeTxDetailsDelayedDescription')}  - { - trackEvent( - { - category: MetaMetricsEventCategory.Home, - event: MetaMetricsEventName.SupportLinkClicked, - properties: { - url: SUPPORT_REQUEST_LINK, - location: 'Bridge Tx Details', - }, - }, - { - contextPropsIntoEventProperties: [ - MetaMetricsContextProp.PageTitle, - ], - }, - ); - }} - > - {t('bridgeTxDetailsDelayedDescriptionSupport')} - - . + + {status?.toLowerCase()} - - )} - - {/* Bridge step list */} - {status !== StatusTypes.COMPLETE && - (bridgeHistoryItem || srcChainTxMeta) && ( - - )} - - {/* Links to block explorers */} - + + {srcNetworkIconName} + + {destNetworkIconName} + + } + /> + + - - - {/* Bridge tx details */} - - - {status?.toLowerCase()} - - } - /> - - {srcNetworkIconName} - - {destNetworkIconName} - - } - /> - - - - + - {/* Bridge tx details 2 */} - - - {t('bridgeTxDetailsTokenAmountOnChain', [ - bridgeAmountSent, - bridgeHistoryItem?.quote.srcAsset.symbol, - ])} - {srcNetworkIconName} - - } - /> - {bridgeAmountReceived && - bridgeHistoryItem?.quote.destAsset.symbol && ( - - {t('bridgeTxDetailsTokenAmountOnChain', [ - bridgeAmountReceived, - bridgeHistoryItem.quote.destAsset.symbol, - ])} - {destNetworkIconName} - - } - /> - )} - - } - /> - + {/* Bridge tx details 2 */} + + + {t('bridgeTxDetailsTokenAmountOnChain', [ + bridgeAmountSent, + bridgeHistoryItem?.quote.srcAsset.symbol, + ])} + {srcNetworkIconName} + + } + /> + + {t('bridgeTxDetailsTokenAmountOnChain', [ + bridgeAmountReceived, + bridgeHistoryItem?.quote.destAsset.symbol, + ])} + {destNetworkIconName} + + } + /> + + } + /> + - + - {/* Generic tx details */} - - + {/* Generic tx details */} + + - - + - -
+ +
); }; From 8d9fa1aa56b259b29df8ef2266e8c657b03f9ed0 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 8 Jan 2025 11:32:58 +0530 Subject: [PATCH 43/71] fix: Fixes in NFT listing label and values (#29046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes in NFT listing: 1. Fix order of listing state changes 2. Change label from `You receive` to `Listing price` 3. Show received value in gray background ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28977 ## **Manual testing steps** 1. Submit NFT listing confirmation 2. Check the confirmations page 3. Ensure that order of state changes is correct and label is also correct ## **Screenshots/Recordings** Screenshot 2024-12-10 at 4 03 42 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/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_locales/en/messages.json | 3 + .../signatures/nft-permit.spec.ts | 4 +- .../confirmations/signatures/permit.spec.ts | 8 +- .../decoded-simulation.test.tsx | 107 +++++++++++++++--- .../decoded-simulation/decoded-simulation.tsx | 74 +++++++++--- .../typed-sign-v4-simulation.test.tsx | 2 +- 6 files changed, 160 insertions(+), 38 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index be2f7e4f6046..a45457e8b4c1 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4270,6 +4270,9 @@ "permitSimulationChange_listing": { "message": "You list" }, + "permitSimulationChange_nft_listing": { + "message": "Listing price" + }, "permitSimulationChange_receive": { "message": "You receive" }, diff --git a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts index 4f1afe586116..cf153761b60d 100644 --- a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts @@ -72,7 +72,7 @@ describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { signatureType: 'eth_signTypedData_v4', primaryType: 'Permit', uiCustomizations: ['redesigned_confirmation', 'permit'], - decodingChangeTypes: ['RECEIVE', 'LISTING'], + decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', }); @@ -115,7 +115,7 @@ describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { primaryType: 'Permit', uiCustomizations: ['redesigned_confirmation', 'permit'], location: 'confirmation', - decodingChangeTypes: ['RECEIVE', 'LISTING'], + decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', }); }, diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index ea375b0c1100..5f6ad8a9e16c 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -69,7 +69,7 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { signatureType: 'eth_signTypedData_v4', primaryType: 'Permit', uiCustomizations: ['redesigned_confirmation', 'permit'], - decodingChangeTypes: ['RECEIVE', 'LISTING'], + decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', }); @@ -107,7 +107,7 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { primaryType: 'Permit', uiCustomizations: ['redesigned_confirmation', 'permit'], location: 'confirmation', - decodingChangeTypes: ['RECEIVE', 'LISTING'], + decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', }); }, @@ -126,12 +126,12 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { const simulationSection = driver.findElement({ text: 'Estimated changes', }); - const receiveChange = driver.findElement({ text: 'You receive' }); + const receiveChange = driver.findElement({ text: 'Listing price' }); const listChange = driver.findElement({ text: 'You list' }); const listChangeValue = driver.findElement({ text: '#2101' }); assert.ok(await simulationSection, 'Estimated changes'); - assert.ok(await receiveChange, 'You receive'); + assert.ok(await receiveChange, 'Listing price'); assert.ok(await listChange, 'You list'); assert.ok(await listChangeValue, '#2101'); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.test.tsx index 0cfd564d8a69..be052c6adba0 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.test.tsx @@ -8,7 +8,11 @@ import { import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../../../test/lib/confirmations/render-helpers'; import { permitSignatureMsg } from '../../../../../../../../../test/data/confirmations/typed_sign'; -import PermitSimulation, { getStateChangeToolip } from './decoded-simulation'; +import PermitSimulation, { + getStateChangeType, + getStateChangeToolip, + StateChangeType, +} from './decoded-simulation'; const decodingData = { stateChanges: [ @@ -57,21 +61,40 @@ const decodingDataListingERC1155: DecodingDataStateChanges = [ tokenID: '2233', }, ]; -const decodingDataBidding: DecodingDataStateChanges = [ + +const nftListing: DecodingDataStateChanges = [ { assetType: 'ERC721', + changeType: DecodingDataChangeType.Listing, + address: '', + amount: '', + contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', + tokenID: '189', + }, + { + assetType: 'ERC20', changeType: DecodingDataChangeType.Receive, address: '', - amount: '900000000000000000', - contractAddress: '', + amount: '950000000000000000', + contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, +]; + +const nftBidding: DecodingDataStateChanges = [ { - assetType: 'Native', + assetType: 'ERC20', changeType: DecodingDataChangeType.Bidding, address: '', amount: '', - contractAddress: '0xafd4896984CA60d2feF66136e57f958dCe9482d5', - tokenID: '2101', + contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', + tokenID: '189', + }, + { + assetType: 'ERC721', + changeType: DecodingDataChangeType.Receive, + address: '', + amount: '950000000000000000', + contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, ]; @@ -137,7 +160,7 @@ describe('DecodedSimulation', () => { ); expect(await findByText('Estimated changes')).toBeInTheDocument(); - expect(await findByText('You receive')).toBeInTheDocument(); + expect(await findByText('Listing price')).toBeInTheDocument(); expect(await findByText('You list')).toBeInTheDocument(); expect(await findByText('#2101')).toBeInTheDocument(); }); @@ -174,24 +197,74 @@ describe('DecodedSimulation', () => { expect(await findByText('Unavailable')).toBeInTheDocument(); }); + it('renders label only once if there are multiple state changes of same changeType', async () => { + const state = getMockTypedSignConfirmStateForRequest({ + ...permitSignatureMsg, + decodingLoading: false, + decodingData: { + stateChanges: [ + decodingData.stateChanges[0], + decodingData.stateChanges[0], + decodingData.stateChanges[0], + ], + }, + }); + const mockStore = configureMockStore([])(state); + + const { findAllByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(await findAllByText('Spending cap')).toHaveLength(1); + }); + + it('for NFT permit label for receive should be "Listing price"', async () => { + const state = getMockTypedSignConfirmStateForRequest({ + ...permitSignatureMsg, + decodingLoading: false, + decodingData: { + stateChanges: nftListing, + }, + }); + const mockStore = configureMockStore([])(state); + + const { findAllByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(await findAllByText('Listing price')).toHaveLength(1); + }); + describe('getStateChangeToolip', () => { it('return correct tooltip when permit is for listing NFT', async () => { const tooltip = getStateChangeToolip( - decodingDataListing, - decodingDataListing?.[0], + StateChangeType.NFTListingReceive, (str: string) => str, ); expect(tooltip).toBe('signature_decoding_list_nft_tooltip'); }); + + it('return correct tooltip when permit is for bidding NFT', async () => { + const tooltip = getStateChangeToolip( + StateChangeType.NFTBiddingReceive, + (str: string) => str, + ); + expect(tooltip).toBe('signature_decoding_bid_nft_tooltip'); + }); }); - it('return correct tooltip when permit is for bidding NFT', async () => { - const tooltip = getStateChangeToolip( - decodingDataBidding, - decodingDataBidding?.[0], - (str: string) => str, - ); - expect(tooltip).toBe('signature_decoding_bid_nft_tooltip'); + describe('getStateChangeType', () => { + it('return correct state change type for NFT listing receive', async () => { + const stateChange = getStateChangeType(nftListing, nftListing[1]); + expect(stateChange).toBe(StateChangeType.NFTListingReceive); + }); + + it('return correct state change type for NFT bidding receive', async () => { + const stateChange = getStateChangeType(nftBidding, nftBidding[1]); + expect(stateChange).toBe(StateChangeType.NFTBiddingReceive); + }); }); it('renders label only once if there are multiple state changes of same changeType', async () => { diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.tsx index 9cfc0bdc1ae7..d89e42fdeb61 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/decoded-simulation/decoded-simulation.tsx @@ -16,11 +16,15 @@ import StaticSimulation from '../../../shared/static-simulation/static-simulatio import TokenValueDisplay from '../value-display/value-display'; import NativeValueDisplay from '../native-value-display/native-value-display'; -export const getStateChangeToolip = ( +export enum StateChangeType { + NFTListingReceive = 'NFTListingReceive', + NFTBiddingReceive = 'NFTBiddingReceive', +} + +export const getStateChangeType = ( stateChangeList: DecodingDataStateChanges | null, stateChange: DecodingDataStateChange, - t: ReturnType, -): string | undefined => { +): StateChangeType | undefined => { if (stateChange.changeType === DecodingDataChangeType.Receive) { if ( stateChangeList?.some( @@ -29,7 +33,7 @@ export const getStateChangeToolip = ( change.assetType === TokenStandard.ERC721, ) ) { - return t('signature_decoding_list_nft_tooltip'); + return StateChangeType.NFTListingReceive; } if ( stateChange.assetType === TokenStandard.ERC721 && @@ -37,24 +41,50 @@ export const getStateChangeToolip = ( (change) => change.changeType === DecodingDataChangeType.Bidding, ) ) { - return t('signature_decoding_bid_nft_tooltip'); + return StateChangeType.NFTBiddingReceive; } } return undefined; }; +export const getStateChangeToolip = ( + nftTransactionType: StateChangeType | undefined, + t: ReturnType, +): string | undefined => { + if (nftTransactionType === StateChangeType.NFTListingReceive) { + return t('signature_decoding_list_nft_tooltip'); + } else if (nftTransactionType === StateChangeType.NFTBiddingReceive) { + return t('signature_decoding_bid_nft_tooltip'); + } + return undefined; +}; + +const stateChangeOrder = { + [DecodingDataChangeType.Transfer]: 1, + [DecodingDataChangeType.Listing]: 2, + [DecodingDataChangeType.Approve]: 3, + [DecodingDataChangeType.Revoke]: 4, + [DecodingDataChangeType.Bidding]: 5, + [DecodingDataChangeType.Receive]: 6, +}; + const getStateChangeLabelMap = ( t: ReturnType, changeType: string, -) => - ({ + stateChangeType?: StateChangeType, +) => { + return { [DecodingDataChangeType.Transfer]: t('permitSimulationChange_transfer'), - [DecodingDataChangeType.Receive]: t('permitSimulationChange_receive'), + [DecodingDataChangeType.Receive]: + stateChangeType === StateChangeType.NFTListingReceive + ? t('permitSimulationChange_nft_listing') + : t('permitSimulationChange_receive'), [DecodingDataChangeType.Approve]: t('permitSimulationChange_approve'), [DecodingDataChangeType.Revoke]: t('permitSimulationChange_revoke'), [DecodingDataChangeType.Bidding]: t('permitSimulationChange_bidding'), [DecodingDataChangeType.Listing]: t('permitSimulationChange_listing'), - }[changeType]); + }[changeType]; +}; const StateChangeRow = ({ stateChangeList, @@ -70,14 +100,19 @@ const StateChangeRow = ({ const t = useI18nContext(); const { assetType, changeType, amount, contractAddress, tokenID } = stateChange; - const tooltip = getStateChangeToolip(stateChangeList, stateChange, t); + const nftTransactionType = getStateChangeType(stateChangeList, stateChange); + const tooltip = getStateChangeToolip(nftTransactionType, t); const canDisplayValueAsUnlimited = assetType === TokenStandard.ERC20 && (changeType === DecodingDataChangeType.Approve || changeType === DecodingDataChangeType.Revoke); return ( {(assetType === TokenStandard.ERC20 || @@ -88,7 +123,10 @@ const StateChangeRow = ({ value={amount} chainId={chainId} tokenId={tokenID} - credit={changeType === DecodingDataChangeType.Receive} + credit={ + nftTransactionType !== StateChangeType.NFTListingReceive && + changeType === DecodingDataChangeType.Receive + } debit={changeType === DecodingDataChangeType.Transfer} canDisplayValueAsUnlimited={canDisplayValueAsUnlimited} /> @@ -97,7 +135,10 @@ const StateChangeRow = ({ )} @@ -112,8 +153,13 @@ const DecodedSimulation: React.FC = () => { const { decodingLoading, decodingData } = currentConfirmation; const stateChangeFragment = useMemo(() => { + const orderedStateChanges = decodingData?.stateChanges?.sort((c1, c2) => + stateChangeOrder[c1.changeType] > stateChangeOrder[c2.changeType] + ? 1 + : -1, + ); const stateChangesGrouped: Record = ( - decodingData?.stateChanges ?? [] + orderedStateChanges ?? [] ).reduce>( (result, stateChange) => { result[stateChange.changeType] = [ diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/typed-sign-v4-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/typed-sign-v4-simulation.test.tsx index 756d54f57a9b..11b5681ef886 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/typed-sign-v4-simulation.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign-v4-simulation/typed-sign-v4-simulation.test.tsx @@ -167,7 +167,7 @@ describe('PermitSimulation', () => { mockStore, ); - expect(await findByText('You receive')).toBeInTheDocument(); + expect(await findByText('Listing price')).toBeInTheDocument(); expect(await findByText('You list')).toBeInTheDocument(); }); }); From 40417b086aa97bff50a6cb01ba7ef8eb7130d424 Mon Sep 17 00:00:00 2001 From: Davide Brocchetto Date: Wed, 8 Jan 2025 09:53:28 +0100 Subject: [PATCH 44/71] test: Swap tests addition (#29442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding Linea Tests as well as test for getting a quote on Mainnet. To run the tests locally use `yarn playwright test --project=swap` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29442?quickstart=1) ## **Related issues** Fixes: ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../pageObjects/network-controller-page.ts | 19 ++++--- .../shared/pageObjects/signup-page.ts | 4 ++ .../playwright/swap/pageObjects/swap-page.ts | 9 ++- test/e2e/playwright/swap/specs/swap.spec.ts | 55 ++++++++++++++++--- test/e2e/playwright/swap/tenderly-network.ts | 16 +++--- 5 files changed, 79 insertions(+), 24 deletions(-) diff --git a/test/e2e/playwright/shared/pageObjects/network-controller-page.ts b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts index cc496f2c9a36..e2116bcac676 100644 --- a/test/e2e/playwright/shared/pageObjects/network-controller-page.ts +++ b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts @@ -24,8 +24,6 @@ export class NetworkController { readonly dismissBtn: Locator; - readonly networkList: Locator; - readonly networkListEdit: Locator; readonly rpcName: Locator; @@ -39,9 +37,6 @@ export class NetworkController { constructor(page: Page) { this.page = page; this.networkDisplay = this.page.getByTestId('network-display'); - this.networkList = this.page.getByTestId( - 'network-list-item-options-button-0x1', - ); this.networkListEdit = this.page.getByTestId( 'network-list-item-options-edit', ); @@ -69,9 +64,14 @@ export class NetworkController { }) { let rpcName = options.name; await this.networkDisplay.click(); - if (options.name === Tenderly.Mainnet.name) { + if ( + options.name === Tenderly.Mainnet.name || + options.name === Tenderly.Linea.name + ) { rpcName = options.rpcName; - await this.networkList.click(); + await this.page + .getByTestId(`network-list-item-options-button-${options.chainID}`) + .click(); await this.networkListEdit.click(); } else { await this.addNetworkButton.click(); @@ -82,7 +82,10 @@ export class NetworkController { await this.networkRpc.fill(options.url); await this.rpcName.fill(rpcName); await this.addURLBtn.click(); - if (options.name !== Tenderly.Mainnet.name) { + if ( + options.name !== Tenderly.Mainnet.name && + options.name !== Tenderly.Linea.name + ) { await this.networkChainId.fill(options.chainID); } await this.networkTicker.fill(options.symbol); diff --git a/test/e2e/playwright/shared/pageObjects/signup-page.ts b/test/e2e/playwright/shared/pageObjects/signup-page.ts index 28909f23ba20..692ca2ea70a4 100644 --- a/test/e2e/playwright/shared/pageObjects/signup-page.ts +++ b/test/e2e/playwright/shared/pageObjects/signup-page.ts @@ -49,6 +49,8 @@ export class SignUpPage { readonly skipSrpBackupBtn: Locator; + readonly popOverBtn: Locator; + constructor(page: Page) { this.page = page; this.getStartedBtn = page.locator('button:has-text("Get started")'); @@ -77,6 +79,7 @@ export class SignUpPage { this.nextBtn = page.getByTestId('pin-extension-next'); this.agreeBtn = page.locator('button:has-text("I agree")'); this.enableBtn = page.locator('button:has-text("Enable")'); + this.popOverBtn = page.getByTestId('popover-close'); } async importWallet() { @@ -114,5 +117,6 @@ export class SignUpPage { await this.gotItBtn.click(); await this.nextBtn.click(); await this.doneBtn.click(); + await this.popOverBtn.click(); } } diff --git a/test/e2e/playwright/swap/pageObjects/swap-page.ts b/test/e2e/playwright/swap/pageObjects/swap-page.ts index c10536d37f05..2f92597a2620 100644 --- a/test/e2e/playwright/swap/pageObjects/swap-page.ts +++ b/test/e2e/playwright/swap/pageObjects/swap-page.ts @@ -63,7 +63,12 @@ export class SwapPage { ); } - async enterQuote(options: { from?: string; to: string; qty: string }) { + async enterQuote(options: { + from?: string; + to: string; + qty: string; + checkBalance: boolean; + }) { // Enter source token const native = await this.page.$(`text=/${options.from}/`); if (!native && options.from) { @@ -75,7 +80,7 @@ export class SwapPage { .locator('[class*="balance"]') .first() .textContent(); - if (balanceString) { + if (balanceString && options.checkBalance) { if (parseFloat(balanceString.split(' ')[1]) <= parseFloat(options.qty)) { await this.goBack(); // not enough balance so cancel out diff --git a/test/e2e/playwright/swap/specs/swap.spec.ts b/test/e2e/playwright/swap/specs/swap.spec.ts index ec9cd73858da..bbfbfa3d8c98 100644 --- a/test/e2e/playwright/swap/specs/swap.spec.ts +++ b/test/e2e/playwright/swap/specs/swap.spec.ts @@ -14,6 +14,7 @@ let swapPage: SwapPage; let networkController: NetworkController; let walletPage: WalletPage; let activityListPage: ActivityListPage; +let wallet: ethers.Wallet; const testSet = [ { @@ -23,6 +24,20 @@ const testSet = [ destination: 'DAI', network: Tenderly.Mainnet, }, + { + quantity: '.5', + source: 'ETH', + type: 'native', + destination: 'DAI', + network: Tenderly.Linea, + }, + { + quantity: '10', + source: 'DAI', + type: 'unapproved', + destination: 'USDC', + network: Tenderly.Linea, + }, { quantity: '50', source: 'DAI', @@ -61,9 +76,6 @@ test.beforeAll( const page = await extension.initExtension(); page.setDefaultTimeout(15000); - const wallet = ethers.Wallet.createRandom(); - await addFundsToAccount(Tenderly.Mainnet.url, wallet.address); - const signUp = new SignUpPage(page); await signUp.createWallet(); @@ -71,13 +83,41 @@ test.beforeAll( swapPage = new SwapPage(page); activityListPage = new ActivityListPage(page); walletPage = new WalletPage(page); - - await networkController.addCustomNetwork(Tenderly.Mainnet); - await walletPage.importAccount(wallet.privateKey); - expect(walletPage.accountMenu).toHaveText('Account 2', { timeout: 30000 }); }, ); +test(`Get quote on Mainnet Network`, async () => { + await walletPage.selectSwapAction(); + await walletPage.page.waitForTimeout(3000); + await swapPage.enterQuote({ + from: 'ETH', + to: 'USDC', + qty: '.01', + checkBalance: false, + }); + await walletPage.page.waitForTimeout(3000); + const quoteFound = await swapPage.waitForQuote(); + expect(quoteFound).toBeTruthy(); + await swapPage.goBack(); +}); + +test(`Add Custom Networks and import test account`, async () => { + let response; + wallet = ethers.Wallet.createRandom(); + + response = await addFundsToAccount(Tenderly.Mainnet.url, wallet.address); + expect(response.error).toBeUndefined(); + + response = await addFundsToAccount(Tenderly.Linea.url, wallet.address); + expect(response.error).toBeUndefined(); + + await networkController.addCustomNetwork(Tenderly.Linea); + await networkController.addCustomNetwork(Tenderly.Mainnet); + + await walletPage.importAccount(wallet.privateKey); + expect(walletPage.accountMenu).toHaveText('Account 2', { timeout: 30000 }); +}); + testSet.forEach((options) => { test(`should swap ${options.type} token ${options.source} to ${options.destination} on ${options.network.name}'`, async () => { await walletPage.selectTokenWallet(); @@ -94,6 +134,7 @@ testSet.forEach((options) => { from: options.source, to: options.destination, qty: options.quantity, + checkBalance: true, }); if (quoteEntered) { diff --git a/test/e2e/playwright/swap/tenderly-network.ts b/test/e2e/playwright/swap/tenderly-network.ts index 996dee47a81a..3b6ef8b8a62b 100644 --- a/test/e2e/playwright/swap/tenderly-network.ts +++ b/test/e2e/playwright/swap/tenderly-network.ts @@ -1,12 +1,11 @@ import axios from 'axios'; -import log from 'loglevel'; export const Tenderly = { Mainnet: { name: 'Ethereum Mainnet', rpcName: 'Tenderly - Mainnet', url: 'https://virtual.mainnet.rpc.tenderly.co/03bb8912-7505-4856-839f-52819a26d0cd', - chainID: '1', + chainID: '0x1', symbol: 'ETH', }, Optimism: { @@ -23,6 +22,13 @@ export const Tenderly = { chainID: '137', symbol: 'ETH', }, + Linea: { + name: 'Linea', + rpcName: '', + url: 'https://virtual.linea.rpc.tenderly.co/2c429ceb-43db-45bc-9d84-21a40d21e0d2', + chainID: '0xe708', + symbol: 'ETH', + }, }; export async function addFundsToAccount( @@ -42,9 +48,5 @@ export async function addFundsToAccount( }, }); - if (response.data.error) { - log.error( - `\tERROR: RROR: Failed to add funds to Tenderly VirtualTestNet\n${response.data.error}`, - ); - } + return response.data; } From e5ff4713aa1f84fc6bb0121416fda693f74448b8 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 8 Jan 2025 15:23:56 +0530 Subject: [PATCH 45/71] feat: Adding more metrics parameters for signature decoding (#29197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding more parameters to signature decoding metrics: 1. decoding_latency - this should capture the amount time it takes to display the decoding results to users. This is similar to the existing simulation_latency property for simulations. 2. decoding_description - this should capture any error message sent along by the API. This is similar to how security_alerts_description property works for blockaid. 3. decoding_in_progress - this value should be passed to decoding_response property when the user approves or rejects the signature request before we display the decoding output. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3778 ## **Manual testing steps** 1. Enable metrics locally, go to test dapp 2. Submit permit sign 3. Check metrics for signature ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../signatures/nft-permit.spec.ts | 2 ++ .../confirmations/signatures/permit.spec.ts | 2 ++ .../signatures/signature-helpers.ts | 18 ++++++++++++++++++ .../hooks/useDecodedSignatureMetrics.test.ts | 6 ++++++ .../hooks/useDecodedSignatureMetrics.ts | 13 ++++++++++++- 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts index cf153761b60d..cd485080208b 100644 --- a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts @@ -74,6 +74,7 @@ describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { uiCustomizations: ['redesigned_confirmation', 'permit'], decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', + decodingDescription: null, }); await assertVerifiedResults(driver, publicAddress); @@ -117,6 +118,7 @@ describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { location: 'confirmation', decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', + decodingDescription: null, }); }, mockSignatureRejectedWithDecoding, diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 5f6ad8a9e16c..d54ba056242d 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -71,6 +71,7 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { uiCustomizations: ['redesigned_confirmation', 'permit'], decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', + decodingDescription: null, }); await assertVerifiedResults(driver, publicAddress); @@ -109,6 +110,7 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { location: 'confirmation', decodingChangeTypes: ['LISTING', 'RECEIVE'], decodingResponse: 'CHANGE', + decodingDescription: null, }); }, mockSignatureRejectedWithDecoding, diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index d341a0020e56..a4c9dd31b2ca 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -41,6 +41,7 @@ type AssertSignatureMetricsOptions = { securityAlertResponse?: string; decodingChangeTypes?: string[]; decodingResponse?: string; + decodingDescription?: string | null; }; type SignatureEventProperty = { @@ -55,6 +56,7 @@ type SignatureEventProperty = { eip712_primary_type?: string; decoding_change_types?: string[]; decoding_response?: string; + decoding_description?: string | null; ui_customizations?: string[]; location?: string; }; @@ -83,6 +85,7 @@ export async function initializePages(driver: Driver) { * @param securityAlertResponse * @param decodingChangeTypes * @param decodingResponse + * @param decodingDescription */ function getSignatureEventProperty( signatureType: string, @@ -92,6 +95,7 @@ function getSignatureEventProperty( securityAlertResponse: string = BlockaidResultType.Loading, decodingChangeTypes?: string[], decodingResponse?: string, + decodingDescription?: string | null, ): SignatureEventProperty { const signatureEventProperty: SignatureEventProperty = { account_type: 'MetaMask', @@ -112,6 +116,7 @@ function getSignatureEventProperty( if (decodingResponse) { signatureEventProperty.decoding_change_types = decodingChangeTypes; signatureEventProperty.decoding_response = decodingResponse; + signatureEventProperty.decoding_description = decodingDescription; } return signatureEventProperty; } @@ -147,6 +152,7 @@ export async function assertSignatureConfirmedMetrics({ securityAlertResponse, decodingChangeTypes, decodingResponse, + decodingDescription, }: AssertSignatureMetricsOptions) { const events = await getEventPayloads(driver, mockedEndpoints); const signatureEventProperty = getSignatureEventProperty( @@ -157,6 +163,7 @@ export async function assertSignatureConfirmedMetrics({ securityAlertResponse, decodingChangeTypes, decodingResponse, + decodingDescription, ); assertSignatureRequestedMetrics( @@ -192,6 +199,7 @@ export async function assertSignatureRejectedMetrics({ securityAlertResponse, decodingChangeTypes, decodingResponse, + decodingDescription, }: AssertSignatureMetricsOptions) { const events = await getEventPayloads(driver, mockedEndpoints); const signatureEventProperty = getSignatureEventProperty( @@ -202,6 +210,7 @@ export async function assertSignatureRejectedMetrics({ securityAlertResponse, decodingChangeTypes, decodingResponse, + decodingDescription, ); assertSignatureRequestedMetrics( @@ -314,12 +323,21 @@ function compareDecodingAPIResponse( expectedProperties.decoding_response, `${eventName} event properties do not match: decoding_response is ${actualProperties.decoding_response}`, ); + assert.equal( + actualProperties.decoding_description, + expectedProperties.decoding_description, + `${eventName} event properties do not match: decoding_response is ${actualProperties.decoding_description}`, + ); } // Remove the property from both objects to avoid comparison delete expectedProperties.decoding_change_types; delete expectedProperties.decoding_response; + delete expectedProperties.decoding_description; + delete expectedProperties.decoding_latency; delete actualProperties.decoding_change_types; delete actualProperties.decoding_response; + delete actualProperties.decoding_description; + delete actualProperties.decoding_latency; } export async function clickHeaderInfoBtn(driver: Driver) { diff --git a/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.test.ts b/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.test.ts index b378343bc274..f93091bfab0e 100644 --- a/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.test.ts +++ b/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.test.ts @@ -87,6 +87,8 @@ describe('useDecodedSignatureMetrics', () => { properties: { decoding_change_types: [], decoding_response: 'NO_CHANGE', + decoding_description: null, + decoding_latency: 0, }, }); }); @@ -115,6 +117,8 @@ describe('useDecodedSignatureMetrics', () => { properties: { decoding_change_types: ['APPROVE'], decoding_response: 'CHANGE', + decoding_description: null, + decoding_latency: 0, }, }); }); @@ -149,6 +153,8 @@ describe('useDecodedSignatureMetrics', () => { properties: { decoding_change_types: [], decoding_response: 'SOME_ERROR', + decoding_description: 'some message', + decoding_latency: 0, }, }); }); diff --git a/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.ts b/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.ts index 98fd07984a9d..fa16f1db0d9a 100644 --- a/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.ts +++ b/ui/pages/confirmations/hooks/useDecodedSignatureMetrics.ts @@ -3,6 +3,7 @@ import { useEffect } from 'react'; import { SignatureRequestType } from '../types/confirm'; import { useConfirmContext } from '../context/confirm'; +import { useLoadingTime } from '../components/simulation-details/useLoadingTime'; import { useSignatureEventFragment } from './useSignatureEventFragment'; enum DecodingResponseType { @@ -13,8 +14,13 @@ enum DecodingResponseType { export function useDecodedSignatureMetrics(supportedByDecodingAPI: boolean) { const { updateSignatureEventFragment } = useSignatureEventFragment(); const { currentConfirmation } = useConfirmContext(); + const { loadingTime, setLoadingComplete } = useLoadingTime(); const { decodingLoading, decodingData } = currentConfirmation; + if (decodingLoading === false) { + setLoadingComplete(); + } + const decodingChangeTypes = (decodingData?.stateChanges ?? []).map( (change: DecodingDataStateChange) => change.changeType, ); @@ -32,14 +38,19 @@ export function useDecodedSignatureMetrics(supportedByDecodingAPI: boolean) { updateSignatureEventFragment({ properties: { - decoding_response: decodingResponse, decoding_change_types: decodingChangeTypes, + decoding_description: decodingData?.error?.message ?? null, + decoding_latency: loadingTime ?? null, + decoding_response: decodingLoading + ? 'decoding_in_progress' + : decodingResponse, }, }); }, [ decodingResponse, decodingLoading, decodingChangeTypes, + loadingTime, updateSignatureEventFragment, ]); } From 7fe00dc2803b845118f48c12232edfe0f447bc47 Mon Sep 17 00:00:00 2001 From: cmd-ob Date: Wed, 8 Jan 2025 10:05:38 +0000 Subject: [PATCH 46/71] test: [POM] Migrate token tests (#29375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** * Updates `asset-list` Page Object - Adds methods for interacting with token list (i.e sorting, getting list, assertion on increase/decrease price and percentage) * Adds new method for importing a custom token using contract address * Adds new method for adding multiple tokens by search in one step * Minor update to `send-token` page for warning message * New page object for `token-overview` * Tests Updated - `import-tokens`, `send-erc20-to-contract`, `token-list` and `token-sort` ## **Related issues** Fixes: ## **Manual testing steps** * All tests must pass on CI ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Chloe Gao Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> Co-authored-by: MetaMask Bot --- .../e2e/page-objects/pages/home/asset-list.ts | 153 +++++++++++ .../pages/send/send-token-page.ts | 19 ++ .../page-objects/pages/token-overview-page.ts | 54 ++++ ...t-tokens.spec.js => import-tokens.spec.ts} | 63 ++--- .../tokens/send-erc20-to-contract.spec.js | 53 ---- .../tokens/send-erc20-to-contract.spec.ts | 52 ++++ test/e2e/tests/tokens/token-list.spec.ts | 245 ++++++++---------- test/e2e/tests/tokens/token-sort.spec.ts | 108 +++----- 8 files changed, 447 insertions(+), 300 deletions(-) create mode 100644 test/e2e/page-objects/pages/token-overview-page.ts rename test/e2e/tests/tokens/{import-tokens.spec.js => import-tokens.spec.ts} (54%) delete mode 100644 test/e2e/tests/tokens/send-erc20-to-contract.spec.js create mode 100644 test/e2e/tests/tokens/send-erc20-to-contract.spec.ts diff --git a/test/e2e/page-objects/pages/home/asset-list.ts b/test/e2e/page-objects/pages/home/asset-list.ts index 9db896e3a035..8c120b9879b7 100644 --- a/test/e2e/page-objects/pages/home/asset-list.ts +++ b/test/e2e/page-objects/pages/home/asset-list.ts @@ -24,6 +24,11 @@ class AssetListPage { private readonly currentNetworksTotal = `${this.currentNetworkOption} [data-testid="account-value-and-suffix"]`; + private readonly customTokenModalOption = { + text: 'Custom token', + tag: 'button', + }; + private readonly hideTokenButton = '[data-testid="asset-options__hide"]'; private readonly hideTokenConfirmationButton = @@ -43,16 +48,42 @@ class AssetListPage { private readonly networksToggle = '[data-testid="sort-by-networks"]'; + private sortByAlphabetically = '[data-testid="sortByAlphabetically"]'; + + private sortByDecliningBalance = '[data-testid="sortByDecliningBalance"]'; + + private sortByPopoverToggle = '[data-testid="sort-by-popover-toggle"]'; + + private readonly tokenAddressInput = + '[data-testid="import-tokens-modal-custom-address"]'; + private readonly tokenAmountValue = '[data-testid="multichain-token-list-item-value"]'; + private readonly tokenImportedSuccessMessage = { + text: 'Token imported', + tag: 'h6', + }; + private readonly tokenListItem = '[data-testid="multichain-token-list-button"]'; private readonly tokenOptionsButton = '[data-testid="import-token-button"]'; + private tokenPercentage(address: string): string { + return `[data-testid="token-increase-decrease-percentage-${address}"]`; + } + private readonly tokenSearchInput = 'input[placeholder="Search tokens"]'; + private readonly tokenSymbolInput = + '[data-testid="import-tokens-modal-custom-symbol"]'; + + private readonly modalWarningBanner = 'div.mm-banner-alert--severity-warning'; + + private readonly tokenIncreaseDecreaseValue = + '[data-testid="token-increase-decrease-value"]'; + constructor(driver: Driver) { this.driver = driver; } @@ -103,6 +134,29 @@ class AssetListPage { return assets.length; } + async getTokenListNames(): Promise { + console.log(`Retrieving the list of token names`); + const tokenElements = await this.driver.findElements(this.tokenListItem); + const tokenNames = await Promise.all( + tokenElements.map(async (element) => { + return await element.getText(); + }), + ); + return tokenNames; + } + + async sortTokenList( + sortBy: 'alphabetically' | 'decliningBalance', + ): Promise { + console.log(`Sorting the token list by ${sortBy}`); + await this.driver.clickElement(this.sortByPopoverToggle); + if (sortBy === 'alphabetically') { + await this.driver.clickElement(this.sortByAlphabetically); + } else if (sortBy === 'decliningBalance') { + await this.driver.clickElement(this.sortByDecliningBalance); + } + } + /** * Hides a token by clicking on the token name, and confirming the hide modal. * @@ -119,6 +173,22 @@ class AssetListPage { ); } + async importCustomToken(tokenAddress: string, symbol: string): Promise { + console.log(`Creating custom token ${symbol} on homepage`); + await this.driver.clickElement(this.tokenOptionsButton); + await this.driver.clickElement(this.importTokensButton); + await this.driver.waitForSelector(this.importTokenModalTitle); + await this.driver.clickElement(this.customTokenModalOption); + await this.driver.waitForSelector(this.modalWarningBanner); + await this.driver.fill(this.tokenAddressInput, tokenAddress); + await this.driver.fill(this.tokenSymbolInput, symbol); + await this.driver.clickElement(this.importTokensNextButton); + await this.driver.clickElementAndWaitToDisappear( + this.confirmImportTokenButton, + ); + await this.driver.waitForSelector(this.tokenImportedSuccessMessage); + } + async importTokenBySearch(tokenName: string) { console.log(`Import token ${tokenName} on homepage by search`); await this.driver.clickElement(this.tokenOptionsButton); @@ -133,6 +203,24 @@ class AssetListPage { ); } + async importMultipleTokensBySearch(tokenNames: string[]) { + console.log( + `Importing tokens ${tokenNames.join(', ')} on homepage by search`, + ); + await this.driver.clickElement(this.tokenOptionsButton); + await this.driver.clickElement(this.importTokensButton); + await this.driver.waitForSelector(this.importTokenModalTitle); + + for (const name of tokenNames) { + await this.driver.fill(this.tokenSearchInput, name); + await this.driver.clickElement({ text: name, tag: 'p' }); + } + await this.driver.clickElement(this.importTokensNextButton); + await this.driver.clickElementAndWaitToDisappear( + this.confirmImportTokenButton, + ); + } + async openNetworksFilter(): Promise { console.log(`Opening the network filter`); await this.driver.clickElement(this.networksToggle); @@ -235,6 +323,71 @@ class AssetListPage { `Expected number of token items ${expectedNumber} is displayed.`, ); } + + /** + * Checks if the token's general increase or decrease percentage is displayed correctly + * + * @param address - The token address to check + * @param expectedChange - The expected change percentage value (e.g. '+0.02%' or '-0.03%') + */ + async check_tokenGeneralChangePercentage( + address: string, + expectedChange: string, + ): Promise { + console.log( + `Checking token general change percentage for address ${address}`, + ); + const isPresent = await this.driver.isElementPresentAndVisible({ + css: this.tokenPercentage(address), + text: expectedChange, + }); + if (!isPresent) { + throw new Error( + `Token general change percentage ${expectedChange} not found for address ${address}`, + ); + } + } + + /** + * Checks if the token's percentage change element does not exist + * + * @param address - The token address to check + */ + async check_tokenGeneralChangePercentageNotPresent( + address: string, + ): Promise { + console.log( + `Checking token general change percentage is not present for address ${address}`, + ); + const isPresent = await this.driver.isElementPresent({ + css: this.tokenPercentage(address), + }); + if (isPresent) { + throw new Error( + `Token general change percentage element should not exist for address ${address}`, + ); + } + } + + /** + * Checks if the token's general increase or decrease value is displayed correctly + * + * @param expectedChangeValue - The expected change value (e.g. '+$50.00' or '-$30.00') + */ + async check_tokenGeneralChangeValue( + expectedChangeValue: string, + ): Promise { + console.log(`Checking token general change value ${expectedChangeValue}`); + const isPresent = await this.driver.isElementPresentAndVisible({ + css: this.tokenIncreaseDecreaseValue, + text: expectedChangeValue, + }); + if (!isPresent) { + throw new Error( + `Token general change value ${expectedChangeValue} not found`, + ); + } + } } export default AssetListPage; 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 3c1d96618556..6b3dd2a78d78 100644 --- a/test/e2e/page-objects/pages/send/send-token-page.ts +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -40,6 +40,9 @@ class SendTokenPage { private readonly toastText = '.toast-text'; + private readonly warning = + '[data-testid="send-warning"] .mm-box--min-width-0 span'; + constructor(driver: Driver) { this.driver = driver; } @@ -196,6 +199,22 @@ class SendTokenPage { text: address, }); } + + /** + * Verifies that a specific warning message is displayed on the send token screen. + * + * @param warningText - The expected warning text to validate against. + * @returns A promise that resolves if the warning message matches the expected text. + * @throws Assertion error if the warning message does not match the expected text. + */ + async check_warningMessage(warningText: string): Promise { + console.log(`Checking if warning message "${warningText}" is displayed`); + await this.driver.waitForSelector({ + css: this.warning, + text: warningText, + }); + console.log('Warning message validation successful'); + } } export default SendTokenPage; diff --git a/test/e2e/page-objects/pages/token-overview-page.ts b/test/e2e/page-objects/pages/token-overview-page.ts new file mode 100644 index 000000000000..46e93ed490c7 --- /dev/null +++ b/test/e2e/page-objects/pages/token-overview-page.ts @@ -0,0 +1,54 @@ +import { Driver } from '../../webdriver/driver'; + +class TokenOverviewPage { + private driver: Driver; + + private readonly receiveButton = { + text: 'Receive', + css: '.icon-button', + }; + + private readonly sendButton = { + text: 'Send', + css: '.icon-button', + }; + + private readonly swapButton = { + text: 'Swap', + css: '.icon-button', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.sendButton, + this.swapButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for Token overview page to be loaded', + e, + ); + throw e; + } + console.log('Token overview page is loaded'); + } + + async clickReceive(): Promise { + await this.driver.clickElement(this.receiveButton); + } + + async clickSend(): Promise { + await this.driver.clickElement(this.sendButton); + } + + async clickSwap(): Promise { + await this.driver.clickElement(this.swapButton); + } +} + +export default TokenOverviewPage; diff --git a/test/e2e/tests/tokens/import-tokens.spec.js b/test/e2e/tests/tokens/import-tokens.spec.ts similarity index 54% rename from test/e2e/tests/tokens/import-tokens.spec.js rename to test/e2e/tests/tokens/import-tokens.spec.ts index b15c9ffb100a..0a840a1ee304 100644 --- a/test/e2e/tests/tokens/import-tokens.spec.js +++ b/test/e2e/tests/tokens/import-tokens.spec.ts @@ -1,13 +1,16 @@ -const { strict: assert } = require('assert'); -const { +import AssetListPage from '../../page-objects/pages/home/asset-list'; +import HomePage from '../../page-objects/pages/home/homepage'; + +import { defaultGanacheOptions, withFixtures, unlockWallet, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Mockttp } from '../../mock-e2e'; describe('Import flow', function () { - async function mockPriceFetch(mockServer) { + async function mockPriceFetch(mockServer: Mockttp) { return [ await mockServer .forGet('https://price.api.cx.metamask.io/v2/chains/1/spot-prices') @@ -60,47 +63,27 @@ describe('Import flow', function () { }) .build(), ganacheOptions: defaultGanacheOptions, - title: this.test.fullTitle(), + title: this.test?.fullTitle(), testSpecificMock: mockPriceFetch, }, async ({ driver }) => { await unlockWallet(driver); - await driver.assertElementNotPresent('.loading-overlay'); - - await driver.clickElement('[data-testid="import-token-button"]'); - await driver.clickElement('[data-testid="importTokens"]'); - - await driver.fill('input[placeholder="Search tokens"]', 'cha'); - - await driver.clickElement('.token-list__token_component'); - await driver.clickElement( - '.token-list__token_component:nth-of-type(2)', - ); - await driver.clickElement( - '.token-list__token_component:nth-of-type(3)', - ); - - await driver.clickElement('[data-testid="import-tokens-button-next"]'); - await driver.clickElement( - '[data-testid="import-tokens-modal-import-button"]', - ); - - // Wait for "loading tokens" to be gone - await driver.assertElementNotPresent( - '[data-testid="token-list-loading-message"]', - ); - - await driver.assertElementNotPresent( - '[data-testid="token-list-loading-message"]', - ); - - await driver.clickElement('[data-testid="sort-by-networks"]'); - await driver.clickElement('[data-testid="network-filter-current"]'); + const homePage = new HomePage(driver); + const assetListPage = new AssetListPage(driver); + await homePage.check_pageIsLoaded(); + await assetListPage.importMultipleTokensBySearch([ + 'CHAIN', + 'CHANGE', + 'CHAI', + ]); - const expectedTokenListElementsAreFound = - await driver.elementCountBecomesN('.multichain-token-list-item', 4); - assert.equal(expectedTokenListElementsAreFound, true); + const tokenList = new AssetListPage(driver); + await tokenList.check_tokenItemNumber(5); // Linea & Mainnet Eth + await tokenList.check_tokenIsDisplayed('Ethereum'); + await tokenList.check_tokenIsDisplayed('Chain Games'); + await tokenList.check_tokenIsDisplayed('Changex'); + await tokenList.check_tokenIsDisplayed('Chai'); }, ); }); diff --git a/test/e2e/tests/tokens/send-erc20-to-contract.spec.js b/test/e2e/tests/tokens/send-erc20-to-contract.spec.js deleted file mode 100644 index 6e94b6377e67..000000000000 --- a/test/e2e/tests/tokens/send-erc20-to-contract.spec.js +++ /dev/null @@ -1,53 +0,0 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - withFixtures, - unlockWallet, -} = require('../../helpers'); -const { SMART_CONTRACTS } = require('../../seeder/smart-contracts'); -const FixtureBuilder = require('../../fixture-builder'); - -describe('Send ERC20 token to contract address', function () { - const smartContract = SMART_CONTRACTS.HST; - - it('should display the token contract warning to the user', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().withTokensControllerERC20().build(), - ganacheOptions: defaultGanacheOptions, - smartContract, - title: this.test.fullTitle(), - }, - async ({ driver, contractRegistry }) => { - const contractAddress = await contractRegistry.getContractAddress( - smartContract, - ); - await unlockWallet(driver); - - // Send TST - await driver.clickElement( - '[data-testid="account-overview__asset-tab"]', - ); - await driver.clickElement( - '[data-testid="multichain-token-list-button"]', - ); - await driver.clickElement('[data-testid="coin-overview-send"]'); - - // Type contract address - await driver.fill( - 'input[placeholder="Enter public address (0x) or domain name"]', - contractAddress, - ); - - // Verify warning - const warningText = - 'Warning: you are about to send to a token contract which could result in a loss of funds. Learn more'; - const warning = await driver.findElement( - '[data-testid="send-warning"] .mm-box--min-width-0 span', - ); - assert.equal(await warning.getText(), warningText); - }, - ); - }); -}); diff --git a/test/e2e/tests/tokens/send-erc20-to-contract.spec.ts b/test/e2e/tests/tokens/send-erc20-to-contract.spec.ts new file mode 100644 index 000000000000..5c7b59cbc215 --- /dev/null +++ b/test/e2e/tests/tokens/send-erc20-to-contract.spec.ts @@ -0,0 +1,52 @@ +import { + defaultGanacheOptions, + withFixtures, + unlockWallet, +} from '../../helpers'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import FixtureBuilder from '../../fixture-builder'; + +import AssetListPage from '../../page-objects/pages/home/asset-list'; +import HomePage from '../../page-objects/pages/home/homepage'; +import SendTokenPage from '../../page-objects/pages/send/send-token-page'; +import TokenOverviewPage from '../../page-objects/pages/token-overview-page'; + +describe('Send ERC20 token to contract address', function () { + const smartContract = SMART_CONTRACTS.HST; + + it('should display the token contract warning to the user', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().withTokensControllerERC20().build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }) => { + const contractAddress: string = + await contractRegistry.getContractAddress(smartContract); + await unlockWallet(driver); + + const homePage = new HomePage(driver); + const assetListPage = new AssetListPage(driver); + await homePage.check_pageIsLoaded(); + await assetListPage.clickOnAsset('TST'); + + // Send TST + const tokenOverviewPage = new TokenOverviewPage(driver); + await tokenOverviewPage.check_pageIsLoaded(); + await tokenOverviewPage.clickSend(); + + const sendTokenPage = new SendTokenPage(driver); + await sendTokenPage.check_pageIsLoaded(); + await sendTokenPage.fillRecipient(contractAddress); + + // Verify warning + const warningText = + 'Warning: you are about to send to a token contract which could result in a loss of funds. Learn more'; + await sendTokenPage.check_warningMessage(warningText); + }, + ); + }); +}); diff --git a/test/e2e/tests/tokens/token-list.spec.ts b/test/e2e/tests/tokens/token-list.spec.ts index f7b032c92a4c..4002142d595e 100644 --- a/test/e2e/tests/tokens/token-list.spec.ts +++ b/test/e2e/tests/tokens/token-list.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import { Mockttp } from 'mockttp'; import { Context } from 'mocha'; import { zeroAddress } from 'ethereumjs-util'; @@ -6,15 +5,17 @@ import { CHAIN_IDS } from '../../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import FixtureBuilder from '../../fixture-builder'; import { - clickNestedButton, defaultGanacheOptions, unlockWallet, withFixtures, } from '../../helpers'; import { Driver } from '../../webdriver/driver'; +import HomePage from '../../page-objects/pages/home/homepage'; +import AssetListPage from '../../page-objects/pages/home/asset-list'; describe('Token List', function () { const chainId = CHAIN_IDS.MAINNET; + const lineaChainId = CHAIN_IDS.LINEA_MAINNET; const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; const symbol = 'foo'; @@ -26,88 +27,118 @@ describe('Token List', function () { }, }; - const importToken = async (driver: Driver) => { - await driver.clickElement(`[data-testid="import-token-button"]`); - await driver.clickElement(`[data-testid="importTokens"]`); - 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.clickElement({ text: 'Next', tag: 'button' }); - await driver.clickElement( - '[data-testid="import-tokens-modal-import-button"]', - ); - await driver.findElement({ text: 'Token imported', tag: 'h6' }); + const mockEmptyPrices = async ( + mockServer: Mockttp, + chainIdToMock: string, + ) => { + return mockServer + .forGet( + `https://price.api.cx.metamask.io/v2/chains/${parseInt( + chainIdToMock, + 16, + )}/spot-prices`, + ) + .thenCallback(() => ({ + statusCode: 200, + json: {}, + })); + }; + + const mockEmptyHistoricalPrices = async ( + mockServer: Mockttp, + address: string, + ) => { + return mockServer + .forGet( + `https://price.api.cx.metamask.io/v1/chains/${chainId}/historical-prices/${address}`, + ) + .thenCallback(() => ({ + statusCode: 200, + json: {}, + })); + }; + + const mockSpotPrices = async ( + mockServer: Mockttp, + chainIdToMock: string, + prices: Record< + string, + { price: number; pricePercentChange1d: number; marketCap: number } + >, + ) => { + return mockServer + .forGet( + `https://price.api.cx.metamask.io/v2/chains/${parseInt( + chainIdToMock, + 16, + )}/spot-prices`, + ) + .thenCallback(() => ({ + statusCode: 200, + json: prices, + })); + }; + + const mockHistoricalPrices = async ( + mockServer: Mockttp, + address: string, + price: number, + ) => { + return mockServer + .forGet( + `https://price.api.cx.metamask.io/v1/chains/${chainId}/historical-prices/${toChecksumHexAddress( + address, + )}`, + ) + .thenCallback(() => ({ + statusCode: 200, + json: { + prices: [ + [1717566000000, price * 0.9], + [1717566322300, price], + [1717566611338, price * 1.1], + ], + }, + })); }; - it('should not shows percentage increase for an ERC20 token without prices available', async function () { + it('should not show percentage increase for an ERC20 token without prices available', async function () { await withFixtures( { ...fixtures, title: (this as Context).test?.fullTitle(), testSpecificMock: async (mockServer: Mockttp) => [ - // Mock no current price - await mockServer - .forGet( - `https://price.api.cx.metamask.io/v2/chains/${parseInt( - chainId, - 16, - )}/spot-prices`, - ) - .thenCallback(() => ({ - statusCode: 200, - json: {}, - })), - // Mock no historical prices - await mockServer - .forGet( - `https://price.api.cx.metamask.io/v1/chains/${chainId}/historical-prices/${tokenAddress}`, - ) - .thenCallback(() => ({ - statusCode: 200, - json: {}, - })), + await mockEmptyPrices(mockServer, chainId), + await mockEmptyPrices(mockServer, lineaChainId), + await mockEmptyHistoricalPrices(mockServer, tokenAddress), ], }, async ({ driver }: { driver: Driver }) => { await unlockWallet(driver); - await importToken(driver); - // Verify native token increase - const testIdNative = `token-increase-decrease-percentage-${zeroAddress()}`; + const homePage = new HomePage(driver); + const assetListPage = new AssetListPage(driver); - // Verify native token increase - const testId = `token-increase-decrease-percentage-${tokenAddress}`; + await homePage.check_pageIsLoaded(); + await assetListPage.importCustomToken(tokenAddress, symbol); - const percentageNative = await ( - await driver.findElement(`[data-testid="${testIdNative}"]`) - ).getText(); - assert.equal(percentageNative, ''); - - const percentage = await ( - await driver.findElement(`[data-testid="${testId}"]`) - ).getText(); - assert.equal(percentage, ''); + await assetListPage.check_tokenGeneralChangePercentageNotPresent( + zeroAddress(), + ); + await assetListPage.check_tokenGeneralChangePercentageNotPresent( + tokenAddress, + ); }, ); }); it('shows percentage increase for an ERC20 token with prices available', async function () { const ethConversionInUsd = 10000; - - // Prices are in ETH const marketData = { price: 0.123, marketCap: 12, pricePercentChange1d: 0.05, }; - const marketDataNative = { price: 0.123, marketCap: 12, @@ -120,91 +151,35 @@ describe('Token List', function () { title: (this as Context).test?.fullTitle(), ethConversionInUsd, testSpecificMock: async (mockServer: Mockttp) => [ - // Mock current price - await mockServer - .forGet( - `https://price.api.cx.metamask.io/v2/chains/${parseInt( - chainId, - 16, - )}/spot-prices`, - ) - .thenCallback(() => ({ - statusCode: 200, - json: { - [zeroAddress()]: marketDataNative, - [tokenAddress.toLowerCase()]: marketData, - }, - })), - // Mock historical prices - await mockServer - .forGet( - `https://price.api.cx.metamask.io/v1/chains/${chainId}/historical-prices/${toChecksumHexAddress( - tokenAddress, - )}`, - ) - .thenCallback(() => ({ - statusCode: 200, - json: { - prices: [ - [1717566000000, marketData.price * 0.9], - [1717566322300, marketData.price], - [1717566611338, marketData.price * 1.1], - ], - }, - })), + await mockSpotPrices(mockServer, chainId, { + [zeroAddress()]: marketDataNative, + [tokenAddress.toLowerCase()]: marketData, + }), + await mockHistoricalPrices( + mockServer, + tokenAddress, + marketData.price, + ), ], }, async ({ driver }: { driver: Driver }) => { await unlockWallet(driver); - await importToken(driver); - // Verify native token increase - const testIdBase = 'token-increase-decrease-percentage'; + const homePage = new HomePage(driver); + const assetListPage = new AssetListPage(driver); - const isETHIncreaseDOMPresentAndVisible = - await driver.isElementPresentAndVisible({ - css: `[data-testid="${testIdBase}-${zeroAddress()}"]`, - text: '+0.02%', - }); - assert.equal( - isETHIncreaseDOMPresentAndVisible, - true, - 'Invalid eth increase dom text content', - ); - - const isTokenIncreaseDecreasePercentageDOMPresent = - await driver.isElementPresentAndVisible({ - css: `[data-testid="${testIdBase}-${tokenAddress}"]`, - text: '+0.05%', - }); - assert.equal( - isTokenIncreaseDecreasePercentageDOMPresent, - true, - 'Invalid token increase dom text content', - ); + await homePage.check_pageIsLoaded(); + await assetListPage.importCustomToken(tokenAddress, symbol); - // check increase balance for native token eth - const isExpectedIncreaseDecreaseValueDOMPresentAndVisible = - await driver.isElementPresentAndVisible({ - css: '[data-testid="token-increase-decrease-value"]', - text: '+$50.00', - }); - assert.equal( - isExpectedIncreaseDecreaseValueDOMPresentAndVisible, - true, - 'Invalid increase-decrease-value dom text content', + await assetListPage.check_tokenGeneralChangePercentage( + zeroAddress(), + '+0.02%', ); - - const isExpectedIncreaseDecreasePercentageDOMPresentAndVisible = - await driver.isElementPresentAndVisible({ - css: '[data-testid="token-increase-decrease-percentage"]', - text: '(+0.02%)', - }); - assert.equal( - isExpectedIncreaseDecreasePercentageDOMPresentAndVisible, - true, - 'Invalid increase-decrease-percentage dom text content', + await assetListPage.check_tokenGeneralChangePercentage( + tokenAddress, + '+0.05%', ); + await assetListPage.check_tokenGeneralChangeValue('+$50.00'); }, ); }); diff --git a/test/e2e/tests/tokens/token-sort.spec.ts b/test/e2e/tests/tokens/token-sort.spec.ts index ff2d35a917dc..ddf9c33cceb1 100644 --- a/test/e2e/tests/tokens/token-sort.spec.ts +++ b/test/e2e/tests/tokens/token-sort.spec.ts @@ -3,102 +3,66 @@ import { Context } from 'mocha'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import FixtureBuilder from '../../fixture-builder'; import { - clickNestedButton, defaultGanacheOptions, - regularDelayMs, unlockWallet, withFixtures, + largeDelayMs, } from '../../helpers'; import { Driver } from '../../webdriver/driver'; +import HomePage from '../../page-objects/pages/home/homepage'; +import AssetListPage from '../../page-objects/pages/home/asset-list'; -describe('Token List', function () { - const chainId = CHAIN_IDS.MAINNET; - const tokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; - const symbol = 'ABC'; +describe('Token List Sorting', function () { + const mainnetChainId = CHAIN_IDS.MAINNET; + const customTokenAddress = '0x2EFA2Cb29C2341d8E5Ba7D3262C9e9d6f1Bf3711'; + const customTokenSymbol = 'ABC'; - const fixtures = { - fixtures: new FixtureBuilder({ inputChainId: chainId }).build(), + const testFixtures = { + fixtures: new FixtureBuilder({ inputChainId: mainnetChainId }).build(), ganacheOptions: { ...defaultGanacheOptions, - chainId: parseInt(chainId, 16), + chainId: parseInt(mainnetChainId, 16), }, }; - const importToken = async (driver: Driver) => { - await driver.clickElement(`[data-testid="import-token-button"]`); - await driver.clickElement(`[data-testid="importTokens"]`); - 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 () { + it('should sort tokens alphabetically and by decreasing balance', async function () { await withFixtures( { - ...fixtures, + ...testFixtures, 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 driver.clickElement('[data-testid="sort-by-popover-toggle"]'); - await driver.clickElement('[data-testid="sortByAlphabetically"]'); + const homePage = new HomePage(driver); + const assetListPage = new AssetListPage(driver); - 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(); - }), + await homePage.check_pageIsLoaded(); + await assetListPage.importCustomToken( + customTokenAddress, + customTokenSymbol, ); - assert.ok( - tokenListSymbolsAfterSortingAlphabetically[0].includes('ABC'), - ); + const initialTokenList = await assetListPage.getTokenListNames(); + assert.ok(initialTokenList[0].includes('Ethereum')); + await assetListPage.sortTokenList('alphabetically'); - await driver.clickElement('[data-testid="sort-by-popover-toggle"]'); - 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(); - }), + await driver.waitUntil( + async () => { + const sortedTokenList = await assetListPage.getTokenListNames(); + return sortedTokenList[0].includes(customTokenSymbol); + }, + { timeout: largeDelayMs, interval: 100 }, ); - assert.ok( - tokenListAfterSortingByDecliningBalance[0].includes('Ethereum'), + + await assetListPage.sortTokenList('decliningBalance'); + await driver.waitUntil( + async () => { + const sortedTokenListByBalance = + await assetListPage.getTokenListNames(); + return sortedTokenListByBalance[0].includes('Ethereum'); + }, + { timeout: largeDelayMs, interval: 100 }, ); }, ); From 36c5fbfacc14aab3d8734c46eae5b990f8763645 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 8 Jan 2025 12:31:48 +0100 Subject: [PATCH 47/71] fix: Bump Snap UI selector `min-height` (#29496) ## **Description** Bump `min-height` of the Snaps UI selector component to be at least as tall as the Snaps UI input. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29496?quickstart=1) ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/3051b905-9f77-4b27-836b-3baf1db40725) ### **After** ![image](https://github.com/user-attachments/assets/a36dbb3c-7e62-4a8d-a65b-8204a4ab05d2) --- ui/components/app/snaps/snap-ui-selector/snap-ui-selector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a0869ff0b46b..e5722cfd763d 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 @@ -75,7 +75,7 @@ const SelectorItem: React.FunctionComponent = ({ justifyContent: 'inherit', textAlign: 'inherit', height: 'inherit', - minHeight: '32px', + minHeight: '48px', maxHeight: '64px', }} > @@ -155,7 +155,7 @@ export const SnapUISelector: React.FunctionComponent = ({ justifyContent: 'inherit', textAlign: 'inherit', height: 'inherit', - minHeight: '32px', + minHeight: '48px', maxHeight: '64px', }} > From 0861cb8775f0e9cc1384af16a7e8d9ffbed94e01 Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Wed, 8 Jan 2025 13:25:31 +0100 Subject: [PATCH 48/71] fix: add lnk network logo (#29493) 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/29368?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Add Ink network according to (network information)[https://docs.inkonchain.com/general/network-information] and ensure ink.svg is displayed properly 2. Add Ink Sepolia network according to (network information)[https://docs.inkonchain.com/general/network-information] and ensure ink-sepolia.svg is supported ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-12-19 at 3 35 46 PM](https://github.com/user-attachments/assets/4ba64987-ba8b-49c4-af10-4c0e9f3e1e4a) ### **After** ![Screenshot 2024-12-19 at 3 35 12 PM](https://github.com/user-attachments/assets/cd27a1ed-cbd5-4206-8431-a759bffc6002) ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [X] 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/ink-sepolia.svg | 22 ++++++++++++++++++++++ app/images/ink.svg | 11 +++++++++++ shared/constants/network.ts | 11 +++++++++++ 3 files changed, 44 insertions(+) create mode 100644 app/images/ink-sepolia.svg create mode 100644 app/images/ink.svg diff --git a/app/images/ink-sepolia.svg b/app/images/ink-sepolia.svg new file mode 100644 index 000000000000..7e6dfe3ca85d --- /dev/null +++ b/app/images/ink-sepolia.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/ink.svg b/app/images/ink.svg new file mode 100644 index 000000000000..a42ada4be3dc --- /dev/null +++ b/app/images/ink.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 787322d79dac..124fcd5eedd2 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -169,6 +169,8 @@ export const CHAIN_IDS = { B3_TESTNET: '0x7c9', GRAVITY_ALPHA_MAINNET: '0x659', GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', + INK_SEPOLIA: '0xba5eD', + INK: '0xdef1', } as const; export const CHAINLIST_CHAIN_IDS_MAP = { @@ -229,6 +231,8 @@ export const CHAINLIST_CHAIN_IDS_MAP = { APE: '0x8173', GRAVITY_ALPHA_MAINNET: '0x659', GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', + INK_SEPOLIA: '0xba5ed', + INK: '0xdef1', } as const; // To add a deprecation warning to a network, add it to the array @@ -278,6 +282,8 @@ export const SCROLL_SEPOLIA_DISPLAY_NAME = 'Scroll Sepolia'; export const OP_BNB_DISPLAY_NAME = 'opBNB'; export const BERACHAIN_DISPLAY_NAME = 'Berachain Artio'; export const METACHAIN_ONE_DISPLAY_NAME = 'Metachain One Mainnet'; +export const INK_SEPOLIA_DISPLAY_NAME = 'Ink Sepolia'; +export const INK_DISPLAY_NAME = 'Ink Mainnet'; export const infuraProjectId = process.env.INFURA_PROJECT_ID; export const getRpcUrl = ({ @@ -483,6 +489,8 @@ export const B3_IMAGE_URL = './images/b3.svg'; export const APE_IMAGE_URL = './images/ape.svg'; export const GRAVITY_ALPHA_MAINNET_IMAGE_URL = './images/gravity.svg'; export const GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL = './images/gravity.svg'; +export const INK_SEPOLIA_IMAGE_URL = './images/ink-sepolia.svg'; +export const INK_IMAGE_URL = './images/ink.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -827,6 +835,8 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { GRAVITY_ALPHA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.GRAVITY_ALPHA_TESTNET_SEPOLIA]: GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.INK_SEPOLIA]: INK_SEPOLIA_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.INK]: INK_IMAGE_URL, } as const; export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = { @@ -868,6 +878,7 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.GRAVITY_ALPHA_TESTNET_SEPOLIA]: GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ZORA_MAINNET]: ETH_TOKEN_IMAGE_URL, + [CHAIN_IDS.INK]: ETH_TOKEN_IMAGE_URL, } as const; export const INFURA_BLOCKED_KEY = 'countryBlocked'; From 78c00b3f4501ddfe598b7bbe7a3796ebfea6a59f Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Wed, 8 Jan 2025 14:13:14 +0100 Subject: [PATCH 49/71] fix: add kaia network logo (#29494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR is to update the Klaytn details to Kaia because of rebranding changes of the network. Please find more details here. https://www.binance.com/en/support/announcement/binance-will-support-the-kaia-klay-rebranding-to-kaia-kaia-f75f933759ee49d0af1dfbce7e32144c?hl=en https://medium.com/klaytn/say-hello-to-kaia-4182ccafe456 https://kaia.io [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27383?quickstart=1) ## **Related issues** https://github.com/MetaMask/metamask-extension/issues/27382 Fixes: ## **Manual testing steps** 1. Go to this networks section. 2. Add kaia network from 3. https://chainlist.org/?search=kaia 4. Currently it shows the Klaytn logo and also shows for KAIA ticker. ## **Screenshots/Recordings** ### **Before** Screenshot 2024-09-25 at 2 59 30 PM ### **After** Once updated it should show with new Kaia logo. ## **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** - [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/images/kaia.svg | 9 ++++++++ app/images/klaytn.svg | 45 ------------------------------------- shared/constants/network.ts | 12 +++++----- 3 files changed, 15 insertions(+), 51 deletions(-) create mode 100644 app/images/kaia.svg delete mode 100644 app/images/klaytn.svg diff --git a/app/images/kaia.svg b/app/images/kaia.svg new file mode 100644 index 000000000000..88a10cd86e68 --- /dev/null +++ b/app/images/kaia.svg @@ -0,0 +1,9 @@ + + image + + + + + + diff --git a/app/images/klaytn.svg b/app/images/klaytn.svg deleted file mode 100644 index 4eb1ff5f107b..000000000000 --- a/app/images/klaytn.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 124fcd5eedd2..38e687e180ce 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -203,7 +203,7 @@ export const CHAINLIST_CHAIN_IDS_MAP = { HAQQ_NETWORK: '0x2be3', IOTEX_MAINNET: '0x1251', KCC_MAINNET: '0x141', - KLAYTN_MAINNET_CYPRESS: '0x2019', + KAIA_MAINNET: '0x2019', KROMA_MAINNET: '0xff', LIGHTLINK_PHOENIX_MAINNET: '0x762', MANTA_PACIFIC_MAINNET: '0xa9', @@ -375,7 +375,7 @@ const CHAINLIST_CURRENCY_SYMBOLS_MAP = { NEAR_AURORA_MAINNET: 'ETH', KROMA_MAINNET: 'ETH', NEBULA_MAINNET: 'sFUEL', - KLAYTN_MAINNET_CYPRESS: 'KLAY', + KAIA_MAINNET: 'KAIA', ENDURANCE_SMART_CHAIN_MAINNET: 'ACE', CRONOS_MAINNET_BETA: 'CRO', FLARE_MAINNET: 'FLR', @@ -453,7 +453,7 @@ export const IOTEX_MAINNET_IMAGE_URL = './images/iotex.svg'; export const IOTEX_TOKEN_IMAGE_URL = './images/iotex-token.svg'; export const APE_TOKEN_IMAGE_URL = './images/ape-token.svg'; export const KCC_MAINNET_IMAGE_URL = './images/kcc-mainnet.svg'; -export const KLAYTN_MAINNET_IMAGE_URL = './images/klaytn.svg'; +export const KAIA_MAINNET_IMAGE_URL = './images/kaia.svg'; export const KROMA_MAINNET_IMAGE_URL = './images/kroma.svg'; export const LIGHT_LINK_IMAGE_URL = './images/lightlink.svg'; export const MANTA_PACIFIC_MAINNET_IMAGE_URL = './images/manta.svg'; @@ -663,8 +663,8 @@ export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { CHAINLIST_CURRENCY_SYMBOLS_MAP.KROMA_MAINNET, [CHAINLIST_CHAIN_IDS_MAP.NEBULA_MAINNET]: CHAINLIST_CURRENCY_SYMBOLS_MAP.NEBULA_MAINNET, - [CHAINLIST_CHAIN_IDS_MAP.KLAYTN_MAINNET_CYPRESS]: - CHAINLIST_CURRENCY_SYMBOLS_MAP.KLAYTN_MAINNET_CYPRESS, + [CHAINLIST_CHAIN_IDS_MAP.KAIA_MAINNET]: + CHAINLIST_CURRENCY_SYMBOLS_MAP.KAIA_MAINNET, [CHAINLIST_CHAIN_IDS_MAP.MOONRIVER]: CHAINLIST_CURRENCY_SYMBOLS_MAP.MOONRIVER, [CHAINLIST_CHAIN_IDS_MAP.ENDURANCE_SMART_CHAIN_MAINNET]: CHAINLIST_CURRENCY_SYMBOLS_MAP.ENDURANCE_SMART_CHAIN_MAINNET, @@ -798,7 +798,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: IOTEX_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.HAQQ_NETWORK]: HAQQ_NETWORK_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.KCC_MAINNET]: KCC_MAINNET_IMAGE_URL, - [CHAINLIST_CHAIN_IDS_MAP.KLAYTN_MAINNET_CYPRESS]: KLAYTN_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.KAIA_MAINNET]: KAIA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.KROMA_MAINNET]: KROMA_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.LIGHTLINK_PHOENIX_MAINNET]: LIGHT_LINK_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.MANTA_PACIFIC_MAINNET]: From 20b417c84f7d9d29593f89d7eeb367261fb8f669 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:37:34 +0100 Subject: [PATCH 50/71] fix: Bump smart-transactions-controller to ^16.0.1 (#29478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Release notes for 16.0.1: https://github.com/MetaMask/smart-transactions-controller/releases/tag/v16.0.1 ## **Related issues** Fixes: ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 8bcb1985e69a..40d8f4845635 100644 --- a/package.json +++ b/package.json @@ -341,7 +341,7 @@ "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^19.0.0", "@metamask/signature-controller": "^23.1.0", - "@metamask/smart-transactions-controller": "^16.0.0", + "@metamask/smart-transactions-controller": "^16.0.1", "@metamask/snaps-controllers": "^9.16.0", "@metamask/snaps-execution-environments": "^6.11.0", "@metamask/snaps-rpc-methods": "^11.8.0", diff --git a/yarn.lock b/yarn.lock index f45eb5cf233e..7f988937d1bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6237,9 +6237,9 @@ __metadata: languageName: node linkType: hard -"@metamask/smart-transactions-controller@npm:^16.0.0": - version: 16.0.0 - resolution: "@metamask/smart-transactions-controller@npm:16.0.0" +"@metamask/smart-transactions-controller@npm:^16.0.1": + version: 16.0.1 + resolution: "@metamask/smart-transactions-controller@npm:16.0.1" dependencies: "@babel/runtime": "npm:^7.24.1" "@ethereumjs/tx": "npm:^5.2.1" @@ -6261,7 +6261,7 @@ __metadata: optional: true "@metamask/approval-controller": optional: true - checksum: 10/5bfe2e459ca75b3de31f28213e908f389a6a7e82b45dd43ee7fd4c46a619a561da030f7f90426af03063ff5dcfc90269a155230db484a2c415db11f8ad8caa02 + checksum: 10/9a4dba47e01c1e47f099faa74a9fdf3378c77e8ba6d699317f9b38df1d71893e5278d210f6f9cd0ff6d2641db502991f48f923e00847410be33ae1df0f69377c languageName: node linkType: hard @@ -26674,7 +26674,7 @@ __metadata: "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^19.0.0" "@metamask/signature-controller": "npm:^23.1.0" - "@metamask/smart-transactions-controller": "npm:^16.0.0" + "@metamask/smart-transactions-controller": "npm:^16.0.1" "@metamask/snaps-controllers": "npm:^9.16.0" "@metamask/snaps-execution-environments": "npm:^6.11.0" "@metamask/snaps-rpc-methods": "npm:^11.8.0" From ae565bc4518fd3d0ee3831088dfddbbb0fe266c1 Mon Sep 17 00:00:00 2001 From: Zbyszek Tenerowicz Date: Wed, 8 Jan 2025 19:45:53 +0100 Subject: [PATCH 51/71] chore: add the new policy.json review process documentation and config (#29383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Introducing the document outlining policy review process. The current change to CODEOWNERS doesn't remove the all devs group - this makes it a soft-launch of the process where a specific approving review is not yet mandatory. We'll be releasing a training video early January. After merging this, I'm hoping to reference the document in multiple places to aid people in discovering and following the process. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29383?quickstart=1) ## **Related issues** Fixes: lack of attention to policy updates ## **Manual testing steps** 1. Follow the process and experience joy ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Mark Stacey --- .github/CODEOWNERS | 2 +- docs/lavamoat-policy-review-process.md | 29 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/lavamoat-policy-review-process.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad36b9b96efd..bb1055ada8f9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,7 +13,7 @@ # audit these changes on their own, and leave their analysis in a comment. # These codeowners will review this analysis, and review the policy changes in # further detail if warranted. -lavamoat/ @MetaMask/extension-devs @MetaMask/supply-chain +lavamoat/ @MetaMask/extension-devs @MetaMask/policy-reviewers @MetaMask/supply-chain # The offscreen.ts script file that is included in the offscreen document html # file is responsible, at present, for loading the snaps execution environment diff --git a/docs/lavamoat-policy-review-process.md b/docs/lavamoat-policy-review-process.md new file mode 100644 index 000000000000..097154e6c268 --- /dev/null +++ b/docs/lavamoat-policy-review-process.md @@ -0,0 +1,29 @@ +# LavaMoat Policy Review process in metamask-extension + +When there's a need to change policy (because of new or updated packages that require a different set of capabilities), please follow these steps: + +> In the initial soft-launch of the process the approval from policy reviewers is not mandatory, but it will be in the near future. + +### Engineer on the dev team: + 1. Notice the `Validate lavamoat policy*` PR status check fail because dependency updates or changes need a policy update. + 2. (optional) Generate an updated policy and give it a cursory look in local development whenever you’re testing the change. + - If you're confident your update is complete, you can push it to the PR branch. + 3. To generate a complete set of new policies, call `metamaskbot` for help: + - put `@metamaskbot update-policies` in a comment on the PR. When it produces changes, they need to be reviewed. The following steps assume update-policies produced changes. + - *Note the response from the bot points to instructions on how to review the policy for your convenience. https://lavamoat.github.io/guides/policy-diff/* + 4. Analyze the diff of policy.json and use the understanding of the codebase and change being made to decide whether the capabilities added make sense in that context. Leave a comment listing any doubts you have with brief explanations or if everything is in order \- saying so and explaining why the most powerful capabilities are there (like access to child_process.exec in node or network access in the browser) + *Remember the Security Reviewer comes with more security expertise but less intimate knowledge of the codebase and the change you’ve built, so you are the most qualified to know whether the dependency needs the powers detected by LavaMoat or not.* + - You can use these questions to guide your analysis: + 1. What new powers (globals and builtins) do you see? Why should the package be allowed to use these new powers? Explain if possible + 2. What new packages do you see? Did you intend to introduce them? If you didn’t, which package did? (can you see them in `packages` field in policy of any other package that you updated or introduced?) + - The comment is mandatory even if you don’t understand the policy change, in which case you’re expected to state so (it’s ok to not understand) + - Note: this could be enforced by a job that is only passing if the comment was made + - Note: we’d introduce more tooling to summarize and analyze policy and post that as a comment on the PR + 5. Mention `policy-reviewers` group in your comment. + policy-reviewers group includes security liaisons and their involvement is not limited to (but likely focused more around) their respective teams’ PRs. + +### L1 Security Reviewer: + 1. Look at the policy and the comment from the Developer. Approve the PR if they match and the policy change seems safe. Address questions the Developer had and discuss if the policy change doesn’t seem right. + 2. If changes are hard to explain or seem dangerous, escalate to a review of the dependency and its powers by mentioning `supply-chain` +### (optional) L2 Security Reviewer: + 1. Review the dependency in question and inform the PR reviewers whether it’s deemed malicious or safe. From ed0362c167323f9e7c079a65a784315cc4cb7f8f Mon Sep 17 00:00:00 2001 From: David Drazic Date: Wed, 8 Jan 2025 20:58:14 +0100 Subject: [PATCH 52/71] fix(snaps): Ensure that adjacent form elements take up to 50% width (#29436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add some SCSS changes to ensure that adjacent form elements take up to 50% width in Snaps custom UI. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29436?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/snaps/issues/2948 ## **Manual testing steps** 1. Install a Snap with custom UI with adjacent form elements like in the example code provided. 2. Make sure that adjacent form elements take up to 50% of the space available. Snaps JSX used for testing: ```typescript
Option 1 Option 2 Option 3 Option 11 Option 22 Option 33
``` ## **Screenshots/Recordings** ### **Before** ![Screenshot 2025-01-06 at 13 16 47](https://github.com/user-attachments/assets/2c1ade00-a5e3-42e8-8619-e6d59262c1fb) ### **After** ![Screenshot 2025-01-08 at 13 45 12](https://github.com/user-attachments/assets/805e67aa-8b43-43fb-a0b5-a0bcbc038d05) ![Screenshot 2025-01-08 at 13 45 31](https://github.com/user-attachments/assets/a7685bc8-c014-41ee-acaa-6987da4c7bb1) ![Screenshot 2025-01-08 at 13 46 51](https://github.com/user-attachments/assets/ef2670a8-1c68-43e3-ae6b-0fcb6ebe9d39) ![Screenshot 2025-01-08 at 13 47 05](https://github.com/user-attachments/assets/585bba34-eaaf-4ddc-9634-a6226f5b5969) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../snaps/snap-ui-checkbox/snap-ui-checkbox.tsx | 5 ++++- .../snaps/snap-ui-dropdown/snap-ui-dropdown.tsx | 5 ++++- .../snap-ui-file-input/snap-ui-file-input.tsx | 16 ++++++++++++++-- .../app/snaps/snap-ui-input/snap-ui-input.tsx | 9 +++++++-- .../snap-ui-radio-group/snap-ui-radio-group.tsx | 5 ++++- .../app/snaps/snap-ui-renderer/index.scss | 8 ++++++++ .../snaps/snap-ui-selector/snap-ui-selector.tsx | 9 ++++++++- 7 files changed, 49 insertions(+), 8 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-checkbox/snap-ui-checkbox.tsx b/ui/components/app/snaps/snap-ui-checkbox/snap-ui-checkbox.tsx index 0d85f8ef60dc..9c2606a2c77f 100644 --- a/ui/components/app/snaps/snap-ui-checkbox/snap-ui-checkbox.tsx +++ b/ui/components/app/snaps/snap-ui-checkbox/snap-ui-checkbox.tsx @@ -1,4 +1,5 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; +import classnames from 'classnames'; import { useSnapInterfaceContext } from '../../../../contexts/snaps'; import { BorderColor, @@ -51,7 +52,9 @@ export const SnapUICheckbox: FunctionComponent = ({ return ( 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 f2cb85cc4ef0..835734b2bf87 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 @@ -1,4 +1,5 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; +import classnames from 'classnames'; import { useSnapInterfaceContext } from '../../../../contexts/snaps'; import { Display, @@ -46,7 +47,9 @@ export const SnapUIDropdown: FunctionComponent = ({ return ( diff --git a/ui/components/app/snaps/snap-ui-file-input/snap-ui-file-input.tsx b/ui/components/app/snaps/snap-ui-file-input/snap-ui-file-input.tsx index 1d254ccaa2e3..cfab068ab6db 100644 --- a/ui/components/app/snaps/snap-ui-file-input/snap-ui-file-input.tsx +++ b/ui/components/app/snaps/snap-ui-file-input/snap-ui-file-input.tsx @@ -146,7 +146,13 @@ export const SnapUIFileInput: FunctionComponent = ({ if (compact) { return ( - + {header} = ({ } return ( - + {header} -> = ({ name, form, ...props }) => { +> = ({ name, form, label, ...props }) => { const { handleInputChange, getValue, focusedInput, setCurrentFocusedInput } = useSnapInterfaceContext(); @@ -54,10 +56,13 @@ export const SnapUIInput: FunctionComponent< ref={inputRef} onFocus={handleFocus} onBlur={handleBlur} - className="snap-ui-renderer__input" + className={classnames('snap-ui-renderer__input', { + 'snap-ui-renderer__field': label !== undefined, + })} id={name} value={value} onChange={handleChange} + label={label} {...props} /> ); diff --git a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx index 4563fbc02fe3..185eed8bddc9 100644 --- a/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx +++ b/ui/components/app/snaps/snap-ui-radio-group/snap-ui-radio-group.tsx @@ -1,4 +1,5 @@ import React, { FunctionComponent, useEffect, useState } from 'react'; +import classnames from 'classnames'; import { useSnapInterfaceContext } from '../../../../contexts/snaps'; import { AlignItems, @@ -75,7 +76,9 @@ export const SnapUIRadioGroup: FunctionComponent = ({ return ( diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index 1b027890b247..cbd4258e1d9d 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -84,4 +84,12 @@ max-width: $width-screen-lg-min; } } + + &__form { + .snap-ui-renderer__panel { + .snap-ui-renderer__field { + flex: 1 1 50%; // Ensure that adjacent form elements take up to 50% width + } + } + } } 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 e5722cfd763d..6f844dc29425 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 @@ -3,6 +3,7 @@ import React, { useEffect, MouseEvent as ReactMouseEvent, } from 'react'; +import classnames from 'classnames'; import { Box, ButtonBase, @@ -128,7 +129,13 @@ export const SnapUISelector: React.FunctionComponent = ({ return ( <> - + {label && } Date: Thu, 9 Jan 2025 09:11:53 +0100 Subject: [PATCH 53/71] test: fix flaky snap signature test (#29480) 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/29480?quickstart=1) ## **Related issues** Fixes: [#29380](https://github.com/MetaMask/metamask-extension/issues/29380) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/helpers.js | 2 +- test/e2e/snaps/test-snap-siginsights.spec.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 24240d6f609a..40e7258ea4a1 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -722,7 +722,7 @@ async function clickSignOnSignatureConfirmation({ if (snapSigInsights) { // there is no condition we can wait for to know the snap is ready, // so we have to add a small delay as the last alternative to avoid flakiness. - await driver.delay(regularDelayMs); + await driver.delay(largeDelayMs); } await driver.waitForSelector( { text: 'Sign', tag: 'button' }, diff --git a/test/e2e/snaps/test-snap-siginsights.spec.js b/test/e2e/snaps/test-snap-siginsights.spec.js index c463d09d864a..3f90b7d56e28 100644 --- a/test/e2e/snaps/test-snap-siginsights.spec.js +++ b/test/e2e/snaps/test-snap-siginsights.spec.js @@ -175,8 +175,13 @@ describe('Test Snap Signature Insights', function () { // switch back to MetaMask window and switch to tx insights pane await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector( + '[data-testid="signature-request-scroll-button"]', + ); // click down arrow - await driver.clickElementSafe('.fa-arrow-down'); + await driver.clickElementSafe( + '[data-testid="signature-request-scroll-button"]', + ); // wait for and click sign await clickSignOnSignatureConfirmation({ @@ -223,7 +228,12 @@ describe('Test Snap Signature Insights', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click down arrow - await driver.clickElementSafe('.fa-arrow-down'); + await driver.waitForSelector( + '[data-testid="signature-request-scroll-button"]', + ); + await driver.clickElementSafe( + '[data-testid="signature-request-scroll-button"]', + ); // wait for and click sign await clickSignOnSignatureConfirmation({ From 00a5db79f52c2e179f70561a72b2ea3463ba7f88 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:50:38 +0100 Subject: [PATCH 54/71] chore(deps): bump `@metamask/eth-trezor-keyring` to `^6.0.0` (#27689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/eth-trezor-keyring` from `^3.1.3` to `^6.0.0`. ``` ## [6.0.0] ### Added - **BREAKING:** Add ESM build ([#40](https://github.com/MetaMask/accounts/pull/40)) - It's no longer possible to import files from `./dist` directly. ## [5.0.0] ### Changed - **BREAKING**: Bump `@metamask/eth-sig-util` dependency from `^7.0.3` to `^8.0.0` ([#79](https://github.com/MetaMask/accounts/pull/79)) - `signTypedData` no longer support `number` for addresses, see [here](https://github.com/MetaMask/eth-sig-util/blob/main/CHANGELOG.md#800). ## [4.0.0] ### Changed - **BREAKING**: `addAccounts` will now only return newly created accounts ([#64](https://github.com/MetaMask/accounts/pull/64)) - This keyring was initially returning every accounts (previous and new ones), which is different from what is expected in the [`Keyring` interface].(https://github.com/MetaMask/utils/blob/v9.2.1/src/keyring.ts#L65) ``` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27689?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** These changes directly impact Trezor devices: 1. Add one or more Trezor accounts 2. Sign message 3. Sign typed data 4. Sign transaction 5. Remove Trezor accounts ## **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. --- lavamoat/browserify/beta/policy.json | 65 ++++++++++++++++++++------- lavamoat/browserify/flask/policy.json | 65 ++++++++++++++++++++------- lavamoat/browserify/main/policy.json | 65 ++++++++++++++++++++------- lavamoat/browserify/mmi/policy.json | 65 ++++++++++++++++++++------- package.json | 13 +++++- yarn.lock | 28 ++++++------ 6 files changed, 222 insertions(+), 79 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index e55d57f5ec0f..430b196d5a7f 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -550,7 +550,7 @@ "console": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/messaging": { @@ -584,7 +584,7 @@ "@metamask/notification-services-controller>firebase>@firebase/installations": true, "@metamask/notification-services-controller>firebase>@firebase/util": true, "@metamask/notification-services-controller>firebase>@firebase/app>idb": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/util": { @@ -634,7 +634,7 @@ "ethereumjs-util>ethereum-cryptography>bs58check": true, "buffer": true, "browserify>buffer": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@keystonehq/metamask-airgapped-keyring": { @@ -815,6 +815,12 @@ "@metamask/eth-snap-keyring>@metamask/eth-sig-util>@metamask/abi-utils>@metamask/utils": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": { + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/abi-utils": { "packages": { "@metamask/utils>@metamask/superstruct": true, @@ -1121,6 +1127,17 @@ "@metamask/eth-sig-util>tweetnacl": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-sig-util>tweetnacl": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1191,6 +1208,7 @@ "packages": { "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": true, "@trezor/connect-web": true, "browserify>buffer": true, @@ -2061,6 +2079,21 @@ "semver": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2684,13 +2717,13 @@ "packages": { "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": { "packages": { - "@metamask/eth-sig-util": true, - "@swc/helpers>tslib": true + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "tslib": true } }, "@trezor/connect-web": { @@ -2721,7 +2754,7 @@ "@trezor/connect-web>@trezor/connect": true, "@trezor/connect-web>@trezor/utils": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect": { @@ -2730,7 +2763,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "@trezor/connect-web>@trezor/connect>@trezor/transport": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": { @@ -2747,7 +2780,7 @@ }, "packages": { "process": true, - "@swc/helpers>tslib": true, + "tslib": true, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils>ua-parser-js": true } }, @@ -2756,7 +2789,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "browserify>buffer": true, "@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": { @@ -2786,7 +2819,7 @@ "@trezor/connect-web>@trezor/utils>bignumber.js": true, "browserify>buffer": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@welldone-software/why-did-you-render": { @@ -2899,7 +2932,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/transaction-controller>@metamask/nonce-tracker>async-mutex": { @@ -2908,7 +2941,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "string.prototype.matchall>es-abstract>available-typed-arrays": { @@ -3810,7 +3843,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "browserify>util>which-typed-array>for-each": { @@ -5324,7 +5357,7 @@ "document.getSelection": true } }, - "@swc/helpers>tslib": { + "tslib": { "globals": { "SuppressedError": true, "define": true @@ -5412,7 +5445,7 @@ "packages": { "react-focus-lock>use-sidecar>detect-node-es": true, "react": true, - "@swc/helpers>tslib": true + "tslib": true } }, "readable-stream>util-deprecate": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index e55d57f5ec0f..430b196d5a7f 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -550,7 +550,7 @@ "console": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/messaging": { @@ -584,7 +584,7 @@ "@metamask/notification-services-controller>firebase>@firebase/installations": true, "@metamask/notification-services-controller>firebase>@firebase/util": true, "@metamask/notification-services-controller>firebase>@firebase/app>idb": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/util": { @@ -634,7 +634,7 @@ "ethereumjs-util>ethereum-cryptography>bs58check": true, "buffer": true, "browserify>buffer": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@keystonehq/metamask-airgapped-keyring": { @@ -815,6 +815,12 @@ "@metamask/eth-snap-keyring>@metamask/eth-sig-util>@metamask/abi-utils>@metamask/utils": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": { + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/abi-utils": { "packages": { "@metamask/utils>@metamask/superstruct": true, @@ -1121,6 +1127,17 @@ "@metamask/eth-sig-util>tweetnacl": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-sig-util>tweetnacl": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1191,6 +1208,7 @@ "packages": { "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": true, "@trezor/connect-web": true, "browserify>buffer": true, @@ -2061,6 +2079,21 @@ "semver": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2684,13 +2717,13 @@ "packages": { "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": { "packages": { - "@metamask/eth-sig-util": true, - "@swc/helpers>tslib": true + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "tslib": true } }, "@trezor/connect-web": { @@ -2721,7 +2754,7 @@ "@trezor/connect-web>@trezor/connect": true, "@trezor/connect-web>@trezor/utils": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect": { @@ -2730,7 +2763,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "@trezor/connect-web>@trezor/connect>@trezor/transport": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": { @@ -2747,7 +2780,7 @@ }, "packages": { "process": true, - "@swc/helpers>tslib": true, + "tslib": true, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils>ua-parser-js": true } }, @@ -2756,7 +2789,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "browserify>buffer": true, "@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": { @@ -2786,7 +2819,7 @@ "@trezor/connect-web>@trezor/utils>bignumber.js": true, "browserify>buffer": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@welldone-software/why-did-you-render": { @@ -2899,7 +2932,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/transaction-controller>@metamask/nonce-tracker>async-mutex": { @@ -2908,7 +2941,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "string.prototype.matchall>es-abstract>available-typed-arrays": { @@ -3810,7 +3843,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "browserify>util>which-typed-array>for-each": { @@ -5324,7 +5357,7 @@ "document.getSelection": true } }, - "@swc/helpers>tslib": { + "tslib": { "globals": { "SuppressedError": true, "define": true @@ -5412,7 +5445,7 @@ "packages": { "react-focus-lock>use-sidecar>detect-node-es": true, "react": true, - "@swc/helpers>tslib": true + "tslib": true } }, "readable-stream>util-deprecate": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index e55d57f5ec0f..430b196d5a7f 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -550,7 +550,7 @@ "console": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/messaging": { @@ -584,7 +584,7 @@ "@metamask/notification-services-controller>firebase>@firebase/installations": true, "@metamask/notification-services-controller>firebase>@firebase/util": true, "@metamask/notification-services-controller>firebase>@firebase/app>idb": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/util": { @@ -634,7 +634,7 @@ "ethereumjs-util>ethereum-cryptography>bs58check": true, "buffer": true, "browserify>buffer": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@keystonehq/metamask-airgapped-keyring": { @@ -815,6 +815,12 @@ "@metamask/eth-snap-keyring>@metamask/eth-sig-util>@metamask/abi-utils>@metamask/utils": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": { + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/abi-utils": { "packages": { "@metamask/utils>@metamask/superstruct": true, @@ -1121,6 +1127,17 @@ "@metamask/eth-sig-util>tweetnacl": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-sig-util>tweetnacl": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1191,6 +1208,7 @@ "packages": { "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": true, "@trezor/connect-web": true, "browserify>buffer": true, @@ -2061,6 +2079,21 @@ "semver": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2684,13 +2717,13 @@ "packages": { "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": { "packages": { - "@metamask/eth-sig-util": true, - "@swc/helpers>tslib": true + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "tslib": true } }, "@trezor/connect-web": { @@ -2721,7 +2754,7 @@ "@trezor/connect-web>@trezor/connect": true, "@trezor/connect-web>@trezor/utils": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect": { @@ -2730,7 +2763,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "@trezor/connect-web>@trezor/connect>@trezor/transport": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": { @@ -2747,7 +2780,7 @@ }, "packages": { "process": true, - "@swc/helpers>tslib": true, + "tslib": true, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils>ua-parser-js": true } }, @@ -2756,7 +2789,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "browserify>buffer": true, "@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": { @@ -2786,7 +2819,7 @@ "@trezor/connect-web>@trezor/utils>bignumber.js": true, "browserify>buffer": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@welldone-software/why-did-you-render": { @@ -2899,7 +2932,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/transaction-controller>@metamask/nonce-tracker>async-mutex": { @@ -2908,7 +2941,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "string.prototype.matchall>es-abstract>available-typed-arrays": { @@ -3810,7 +3843,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "browserify>util>which-typed-array>for-each": { @@ -5324,7 +5357,7 @@ "document.getSelection": true } }, - "@swc/helpers>tslib": { + "tslib": { "globals": { "SuppressedError": true, "define": true @@ -5412,7 +5445,7 @@ "packages": { "react-focus-lock>use-sidecar>detect-node-es": true, "react": true, - "@swc/helpers>tslib": true + "tslib": true } }, "readable-stream>util-deprecate": { diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 5658498ad3a7..b5f88a04662a 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -550,7 +550,7 @@ "console": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/messaging": { @@ -584,7 +584,7 @@ "@metamask/notification-services-controller>firebase>@firebase/installations": true, "@metamask/notification-services-controller>firebase>@firebase/util": true, "@metamask/notification-services-controller>firebase>@firebase/app>idb": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/notification-services-controller>firebase>@firebase/util": { @@ -634,7 +634,7 @@ "ethereumjs-util>ethereum-cryptography>bs58check": true, "buffer": true, "browserify>buffer": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@keystonehq/metamask-airgapped-keyring": { @@ -907,6 +907,12 @@ "@metamask/eth-snap-keyring>@metamask/eth-sig-util>@metamask/abi-utils>@metamask/utils": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": { + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/abi-utils": { "packages": { "@metamask/utils>@metamask/superstruct": true, @@ -1213,6 +1219,17 @@ "@metamask/eth-sig-util>tweetnacl": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": { + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/abi-utils": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "@ethereumjs/tx>ethereum-cryptography": true, + "@metamask/eth-sig-util>tweetnacl": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, @@ -1283,6 +1300,7 @@ "packages": { "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": true, "@trezor/connect-web": true, "browserify>buffer": true, @@ -2153,6 +2171,21 @@ "semver": true } }, + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "@metamask/utils>@metamask/superstruct": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "browserify>buffer": true, + "nock>debug": true, + "@metamask/utils>pony-cause": true, + "semver": true + } + }, "@metamask/keyring-controller>@metamask/eth-sig-util>@metamask/utils": { "globals": { "TextDecoder": true, @@ -2776,13 +2809,13 @@ "packages": { "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/eth-trezor-keyring>@trezor/connect-plugin-ethereum": { "packages": { - "@metamask/eth-sig-util": true, - "@swc/helpers>tslib": true + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "tslib": true } }, "@trezor/connect-web": { @@ -2813,7 +2846,7 @@ "@trezor/connect-web>@trezor/connect": true, "@trezor/connect-web>@trezor/utils": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect": { @@ -2822,7 +2855,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "@trezor/connect-web>@trezor/connect>@trezor/transport": true, "@trezor/connect-web>@trezor/utils": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils": { @@ -2839,7 +2872,7 @@ }, "packages": { "process": true, - "@swc/helpers>tslib": true, + "tslib": true, "@trezor/connect-web>@trezor/connect-common>@trezor/env-utils>ua-parser-js": true } }, @@ -2848,7 +2881,7 @@ "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": true, "browserify>buffer": true, "@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@trezor/connect-web>@trezor/connect>@trezor/schema-utils": { @@ -2878,7 +2911,7 @@ "@trezor/connect-web>@trezor/utils>bignumber.js": true, "browserify>buffer": true, "webpack>events": true, - "@swc/helpers>tslib": true + "tslib": true } }, "@welldone-software/why-did-you-render": { @@ -2991,7 +3024,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "@metamask/transaction-controller>@metamask/nonce-tracker>async-mutex": { @@ -3000,7 +3033,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "string.prototype.matchall>es-abstract>available-typed-arrays": { @@ -3902,7 +3935,7 @@ "setTimeout": true }, "packages": { - "@swc/helpers>tslib": true + "tslib": true } }, "browserify>util>which-typed-array>for-each": { @@ -5416,7 +5449,7 @@ "document.getSelection": true } }, - "@swc/helpers>tslib": { + "tslib": { "globals": { "SuppressedError": true, "define": true @@ -5504,7 +5537,7 @@ "packages": { "react-focus-lock>use-sidecar>detect-node-es": true, "react": true, - "@swc/helpers>tslib": true + "tslib": true } }, "readable-stream>util-deprecate": { diff --git a/package.json b/package.json index 40d8f4845635..77b94c6e74a4 100644 --- a/package.json +++ b/package.json @@ -249,7 +249,15 @@ "secp256k1@npm:^4.0.0": "4.0.4", "secp256k1@npm:^4.0.1": "4.0.4", "secp256k1@npm:4.0.2": "4.0.4", - "secp256k1@npm:4.0.3": "4.0.4" + "secp256k1@npm:4.0.3": "4.0.4", + "tslib@npm:^2.0.0": "~2.6.0", + "tslib@npm:^2.0.1": "~2.6.0", + "tslib@npm:^2.0.3": "~2.6.0", + "tslib@npm:^2.1.0": "~2.6.0", + "tslib@npm:^2.3.0": "~2.6.0", + "tslib@npm:^2.3.1": "~2.6.0", + "tslib@npm:^2.4.0": "~2.6.0", + "tslib@npm:^2.6.2": "~2.6.0" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.25.9#~/.yarn/patches/@babel-runtime-npm-7.25.9-fe8c62510a.patch", @@ -301,7 +309,7 @@ "@metamask/eth-sig-util": "^7.0.1", "@metamask/eth-snap-keyring": "^7.0.0", "@metamask/eth-token-tracker": "^9.0.0", - "@metamask/eth-trezor-keyring": "^3.1.3", + "@metamask/eth-trezor-keyring": "^6.0.0", "@metamask/etherscan-link": "^3.0.0", "@metamask/ethjs": "^0.6.0", "@metamask/ethjs-contract": "^0.4.1", @@ -432,6 +440,7 @@ "simple-git": "^3.20.0", "single-call-balance-checker-abi": "^1.0.0", "ts-mixer": "patch:ts-mixer@npm%3A6.0.4#~/.yarn/patches/ts-mixer-npm-6.0.4-5d9747bdf5.patch", + "tslib": "~2.6.0", "unicode-confusables": "^0.1.1", "uri-js": "^4.4.1", "uuid": "^8.3.2", diff --git a/yarn.lock b/yarn.lock index 7f988937d1bb..e98bb06943fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5459,17 +5459,18 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-trezor-keyring@npm:^3.1.3": - version: 3.1.3 - resolution: "@metamask/eth-trezor-keyring@npm:3.1.3" +"@metamask/eth-trezor-keyring@npm:^6.0.0": + version: 6.0.0 + resolution: "@metamask/eth-trezor-keyring@npm:6.0.0" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@ethereumjs/util": "npm:^8.1.0" - "@metamask/eth-sig-util": "npm:^7.0.3" + "@metamask/eth-sig-util": "npm:^8.0.0" "@trezor/connect-plugin-ethereum": "npm:^9.0.3" "@trezor/connect-web": "npm:^9.1.11" hdkey: "npm:^2.1.0" - checksum: 10/d32a687bcaab4593e6208a1bb59cbdd2b111eff357fd30e707787454ef571abfb4e6162422504f730f3ab2fe576b555d68114de0406ae5cdad252dab1b635cce + tslib: "npm:^2.6.2" + checksum: 10/d5d799c60eeab963ef3e5533de472044b08b6f72652ecefbf26cec99784829bbcd706df57f6450ddb019c7dff7c41b0e0dad244aad62b7d03b51fc97755e2c4c languageName: node linkType: hard @@ -6231,9 +6232,9 @@ __metadata: linkType: hard "@metamask/slip44@npm:^4.0.0": - version: 4.1.0 - resolution: "@metamask/slip44@npm:4.1.0" - checksum: 10/4265254a1800a24915bd1de15f86f196737132f9af2a084c2efc885decfc5dd87ad8f0687269d90b35e2ec64d3ea4fbff0caa793bcea6e585b1f3a290952b750 + version: 4.0.0 + resolution: "@metamask/slip44@npm:4.0.0" + checksum: 10/3e47e8834b0fbdabe1f126fd78665767847ddc1f9ccc8defb23007dd71fcd2e4899c8ca04857491be3630668a3765bad1e40fdfca9a61ef33236d8d08e51535e languageName: node linkType: hard @@ -26631,7 +26632,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^7.0.1" "@metamask/eth-snap-keyring": "npm:^7.0.0" "@metamask/eth-token-tracker": "npm:^9.0.0" - "@metamask/eth-trezor-keyring": "npm:^3.1.3" + "@metamask/eth-trezor-keyring": "npm:^6.0.0" "@metamask/etherscan-link": "npm:^3.0.0" "@metamask/ethjs": "npm:^0.6.0" "@metamask/ethjs-contract": "npm:^0.4.1" @@ -26959,6 +26960,7 @@ __metadata: through2: "npm:^4.0.2" ts-mixer: "patch:ts-mixer@npm%3A6.0.4#~/.yarn/patches/ts-mixer-npm-6.0.4-5d9747bdf5.patch" ts-node: "npm:^10.9.2" + tslib: "npm:~2.6.0" tsx: "npm:^4.7.1" ttest: "npm:^2.1.1" typescript: "npm:~5.4.5" @@ -35721,10 +35723,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca +"tslib@npm:~2.6.0": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c languageName: node linkType: hard From 93b1e13410a19bf3659e6dbb12f9b614852472da Mon Sep 17 00:00:00 2001 From: Priya Date: Thu, 9 Jan 2025 14:08:28 +0100 Subject: [PATCH 55/71] chore: fix flaky e2e for nft token send (#29476) 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/29476?quickstart=1) ## **Related issues** Fixes: [#29382](https://github.com/MetaMask/metamask-extension/issues/29382) ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- test/e2e/helpers.js | 2 +- .../confirmations/transactions/nft-token-send-redesign.spec.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 40e7258ea4a1..7d675788f5bd 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -633,9 +633,9 @@ async function unlockWallet( await driver.navigate(); } + await driver.waitForSelector('#password', { state: 'enabled' }); await driver.fill('#password', password); await driver.press('#password', driver.Key.ENTER); - if (waitLoginSuccess) { await driver.assertElementNotPresent('[data-testid="unlock-page"]'); } diff --git a/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts index 09bd9d4b32a2..9b712ebb7a65 100644 --- a/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts +++ b/test/e2e/tests/confirmations/transactions/nft-token-send-redesign.spec.ts @@ -196,6 +196,7 @@ async function createERC721WalletInitiatedTransactionAndAssertDetails( const testDapp = new TestDapp(driver); await testDapp.openTestDappPage({ contractAddress, url: DAPP_URL }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await testDapp.clickERC721MintButton(); From c16460e7078a0510bd018768df4ad4f2ba5dfd8e Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:22:41 +0100 Subject: [PATCH 56/71] test: [POM] Migrate connections e2e tests to TS and Page Object Model (#29384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Migrate connections e2e tests to TS and Page Object Model ``` test/e2e/tests/connections/edit-account-flow.spec.ts test/e2e/tests/connections/edit-networks-flow.spec.ts ``` - Remove the spec `test/e2e/tests/connections/connect-with-metamask.spec.js` as it's already completely tested in another spec. We don't want to repeat testing. - create more class methods for permission page class [![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/29440 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../modules/bridge-utils/bridge.util.test.ts | 4 +- .../pages/permission/site-permission-page.ts | 93 +++++++++++++++++- .../connections/connect-with-metamask.spec.js | 75 -------------- .../connections/edit-account-flow.spec.js | 97 ------------------- .../edit-account-permissions.spec.ts | 74 ++++++++++++++ .../connections/edit-networks-flow.spec.js | 73 -------------- .../edit-networks-permissions.spec.ts | 49 ++++++++++ 7 files changed, 216 insertions(+), 249 deletions(-) delete mode 100644 test/e2e/tests/connections/connect-with-metamask.spec.js delete mode 100644 test/e2e/tests/connections/edit-account-flow.spec.js create mode 100644 test/e2e/tests/connections/edit-account-permissions.spec.ts delete mode 100644 test/e2e/tests/connections/edit-networks-flow.spec.js create mode 100644 test/e2e/tests/connections/edit-networks-permissions.spec.ts diff --git a/shared/modules/bridge-utils/bridge.util.test.ts b/shared/modules/bridge-utils/bridge.util.test.ts index 555de4fc2516..b9fea3db1ea8 100644 --- a/shared/modules/bridge-utils/bridge.util.test.ts +++ b/shared/modules/bridge-utils/bridge.util.test.ts @@ -149,7 +149,7 @@ describe('Bridge utils', () => { (fetchWithCache as jest.Mock).mockRejectedValue(mockError); - await expect(fetchBridgeFeatureFlags()).rejects.toThrowError(mockError); + await expect(fetchBridgeFeatureFlags()).rejects.toThrow(mockError); }); }); @@ -223,7 +223,7 @@ describe('Bridge utils', () => { (fetchWithCache as jest.Mock).mockRejectedValue(mockError); - await expect(fetchBridgeTokens('0xa')).rejects.toThrowError(mockError); + await expect(fetchBridgeTokens('0xa')).rejects.toThrow(mockError); }); }); diff --git a/test/e2e/page-objects/pages/permission/site-permission-page.ts b/test/e2e/page-objects/pages/permission/site-permission-page.ts index bc5eee61c781..9f7ae5d778ff 100644 --- a/test/e2e/page-objects/pages/permission/site-permission-page.ts +++ b/test/e2e/page-objects/pages/permission/site-permission-page.ts @@ -7,7 +7,33 @@ import { Driver } from '../../../webdriver/driver'; class SitePermissionPage { private driver: Driver; - private readonly permissionPage = '[data-testid ="connections-page"]'; + private readonly confirmEditAccountsButton = + '[data-testid="connect-more-accounts-button"]'; + + private readonly confirmEditNetworksButton = + '[data-testid="connect-more-chains-button"]'; + + private readonly connectedAccountsInfo = { + text: 'See your accounts and suggest transactions', + tag: 'p', + }; + + private readonly editAccountsModalTitle = { + text: 'Edit accounts', + tag: 'h4', + }; + + private readonly editButton = '[data-testid="edit"]'; + + private readonly editNetworksModalTitle = { + text: 'Edit networks', + tag: 'h4', + }; + + private readonly enabledNetworksInfo = { + text: 'Use your enabled networks', + tag: 'p', + }; constructor(driver: Driver) { this.driver = driver; @@ -20,7 +46,8 @@ class SitePermissionPage { */ async check_pageIsLoaded(site: string): Promise { try { - await this.driver.waitForSelector(this.permissionPage); + await this.driver.waitForSelector(this.connectedAccountsInfo); + await this.driver.waitForSelector(this.enabledNetworksInfo); await this.driver.waitForSelector({ text: site, tag: 'span' }); } catch (e) { console.log( @@ -31,6 +58,68 @@ class SitePermissionPage { } console.log('Site permission page is loaded'); } + + /** + * Edit permissions for accounts on site permission page + * + * @param accountLabels - Account labels to edit + */ + async editPermissionsForAccount(accountLabels: string[]): Promise { + console.log(`Edit permissions for accounts: ${accountLabels}`); + const editButtons = await this.driver.findElements(this.editButton); + await editButtons[0].click(); + await this.driver.waitForSelector(this.editAccountsModalTitle); + for (const accountLabel of accountLabels) { + await this.driver.clickElement({ text: accountLabel, tag: 'button' }); + } + await this.driver.clickElementAndWaitToDisappear( + this.confirmEditAccountsButton, + ); + } + + /** + * Edit permissions for networks on site permission page + * + * @param networkNames - Network names to edit + */ + async editPermissionsForNetwork(networkNames: string[]): Promise { + console.log(`Edit permissions for networks: ${networkNames}`); + const editButtons = await this.driver.findElements(this.editButton); + await editButtons[1].click(); + await this.driver.waitForSelector(this.editNetworksModalTitle); + for (const networkName of networkNames) { + await this.driver.clickElement({ text: networkName, tag: 'p' }); + } + await this.driver.clickElementAndWaitToDisappear( + this.confirmEditNetworksButton, + ); + } + + /** + * Check if the number of connected accounts is correct + * + * @param number - Expected number of connected accounts + */ + async check_connectedAccountsNumber(number: number): Promise { + console.log(`Check that the number of connected accounts is: ${number}`); + await this.driver.waitForSelector({ + text: `${number} accounts connected`, + tag: 'span', + }); + } + + /** + * Check if the number of connected networks is correct + * + * @param number - Expected number of connected networks + */ + async check_connectedNetworksNumber(number: number): Promise { + console.log(`Check that the number of connected networks is: ${number}`); + await this.driver.waitForSelector({ + text: `${number} networks connected`, + tag: 'span', + }); + } } export default SitePermissionPage; diff --git a/test/e2e/tests/connections/connect-with-metamask.spec.js b/test/e2e/tests/connections/connect-with-metamask.spec.js deleted file mode 100644 index b46fc8730d84..000000000000 --- a/test/e2e/tests/connections/connect-with-metamask.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -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.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 deleted file mode 100644 index 1c4899ed8328..000000000000 --- a/test/e2e/tests/connections/edit-account-flow.spec.js +++ /dev/null @@ -1,97 +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('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.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-account-permissions.spec.ts b/test/e2e/tests/connections/edit-account-permissions.spec.ts new file mode 100644 index 000000000000..bd1eebdf9ffd --- /dev/null +++ b/test/e2e/tests/connections/edit-account-permissions.spec.ts @@ -0,0 +1,74 @@ +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { + ACCOUNT_TYPE, + DEFAULT_FIXTURE_ACCOUNT, + DAPP_HOST_ADDRESS, +} from '../../constants'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import Homepage from '../../page-objects/pages/home/homepage'; +import PermissionListPage from '../../page-objects/pages/permission/permission-list-page'; +import SitePermissionPage from '../../page-objects/pages/permission/site-permission-page'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +const accountLabel2 = '2nd custom name'; +const accountLabel3 = '3rd custom name'; +describe('Edit Accounts Permissions', function () { + it('should be able to edit accounts', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithBalanceValidation(driver); + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await testDapp.check_pageIsLoaded(); + await testDapp.connectAccount({ + publicAddress: DEFAULT_FIXTURE_ACCOUNT, + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new Homepage(driver).check_pageIsLoaded(); + new HeaderNavbar(driver).openAccountMenu(); + + // create second account with custom label + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: accountLabel2, + }); + const homepage = new Homepage(driver); + await homepage.check_expectedBalanceIsDisplayed(); + + // create third account with custom label + await homepage.headerNavbar.openAccountMenu(); + await accountListPage.check_pageIsLoaded(); + await accountListPage.addAccount({ + accountType: ACCOUNT_TYPE.Ethereum, + accountName: accountLabel3, + }); + await homepage.check_expectedBalanceIsDisplayed(); + + // go to connections permissions page + await homepage.headerNavbar.openPermissionsPage(); + const permissionListPage = new PermissionListPage(driver); + await permissionListPage.check_pageIsLoaded(); + await permissionListPage.openPermissionPageForSite(DAPP_HOST_ADDRESS); + const sitePermissionPage = new SitePermissionPage(driver); + await sitePermissionPage.check_pageIsLoaded(DAPP_HOST_ADDRESS); + await sitePermissionPage.editPermissionsForAccount([ + accountLabel2, + accountLabel3, + ]); + await sitePermissionPage.check_connectedAccountsNumber(3); + }, + ); + }); +}); diff --git a/test/e2e/tests/connections/edit-networks-flow.spec.js b/test/e2e/tests/connections/edit-networks-flow.spec.js deleted file mode 100644 index 95a091f7e504..000000000000 --- a/test/e2e/tests/connections/edit-networks-flow.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -const { strict: assert } = require('assert'); -const { - withFixtures, - WINDOW_TITLES, - connectToDapp, - logInWithBalanceValidation, - locateAccountBalanceDOM, - defaultGanacheOptions, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); - -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"]', - ); - await locateAccountBalanceDOM(driver); - await driver.clickElement( - '[data-testid ="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'All Permissions', tag: 'div' }); - 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/edit-networks-permissions.spec.ts b/test/e2e/tests/connections/edit-networks-permissions.spec.ts new file mode 100644 index 000000000000..55b7cdd2caeb --- /dev/null +++ b/test/e2e/tests/connections/edit-networks-permissions.spec.ts @@ -0,0 +1,49 @@ +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import { DEFAULT_FIXTURE_ACCOUNT, DAPP_HOST_ADDRESS } from '../../constants'; +import FixtureBuilder from '../../fixture-builder'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import Homepage from '../../page-objects/pages/home/homepage'; +import TestDapp from '../../page-objects/pages/test-dapp'; +import PermissionListPage from '../../page-objects/pages/permission/permission-list-page'; +import SitePermissionPage from '../../page-objects/pages/permission/site-permission-page'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Edit Networks Permissions', function () { + it('should be able to edit networks', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await loginWithBalanceValidation(driver); + const testDapp = new TestDapp(driver); + await testDapp.openTestDappPage(); + await testDapp.check_pageIsLoaded(); + + await testDapp.connectAccount({ + publicAddress: DEFAULT_FIXTURE_ACCOUNT, + }); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await new Homepage(driver).check_pageIsLoaded(); + + // Open permission page for dapp + new HeaderNavbar(driver).openPermissionsPage(); + const permissionListPage = new PermissionListPage(driver); + await permissionListPage.check_pageIsLoaded(); + await permissionListPage.openPermissionPageForSite(DAPP_HOST_ADDRESS); + const sitePermissionPage = new SitePermissionPage(driver); + await sitePermissionPage.check_pageIsLoaded(DAPP_HOST_ADDRESS); + + // Disconnect Mainnet + await sitePermissionPage.editPermissionsForNetwork([ + 'Ethereum Mainnet', + ]); + await sitePermissionPage.check_connectedNetworksNumber(2); + }, + ); + }); +}); From 3154019858c172231111698f519b19577fc1e2f6 Mon Sep 17 00:00:00 2001 From: Guillaume Roux Date: Thu, 9 Jan 2025 14:25:56 +0100 Subject: [PATCH 57/71] fix(snaps): Scrollbar being partially hidden behind footer (#29435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This fixes the scrollbar in Snap dialogs being partially hidden behind the footer. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29435?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/66cf18d5-4e0c-49b4-ad44-353384404be8) ![image](https://github.com/user-attachments/assets/81df4231-7b23-4377-b6fe-e18e829d50dd) ### **After** ![image](https://github.com/user-attachments/assets/6390d11c-223e-4e73-8f8e-3f361e0c069f) ![image](https://github.com/user-attachments/assets/269ac240-a7e0-4716-844d-680f2389166f) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/snaps/snap-ui-renderer/components/container.ts | 6 ------ ui/components/app/snaps/snap-ui-renderer/index.scss | 4 ---- .../app/snaps/snap-ui-renderer/snap-ui-renderer.js | 4 ++++ .../__snapshots__/snaps-section.test.tsx.snap | 8 ++++---- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/ui/components/app/snaps/snap-ui-renderer/components/container.ts b/ui/components/app/snaps/snap-ui-renderer/components/container.ts index cac6788f8c48..b3f717aecbdd 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/container.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/container.ts @@ -2,7 +2,6 @@ import { BoxElement, JSXElement } from '@metamask/snaps-sdk/jsx'; import { getJsxChildren } from '@metamask/snaps-utils'; import { mapToTemplate } from '../utils'; import { - BlockSize, Display, FlexDirection, } from '../../../../../helpers/constants/design-system'; @@ -78,12 +77,7 @@ export const container: UIComponentFactory = ({ props: { display: Display.Flex, flexDirection: FlexDirection.Column, - height: BlockSize.Full, className: 'snap-ui-renderer__container', - style: { - overflowY: 'auto', - paddingBottom: useFooter ? '80px' : 'initial', - }, }, }; }; diff --git a/ui/components/app/snaps/snap-ui-renderer/index.scss b/ui/components/app/snaps/snap-ui-renderer/index.scss index cbd4258e1d9d..c840a76b670c 100644 --- a/ui/components/app/snaps/snap-ui-renderer/index.scss +++ b/ui/components/app/snaps/snap-ui-renderer/index.scss @@ -5,10 +5,6 @@ $width-screen-md-min: 80vw; $width-screen-lg-min: 62vw; - &__content { - margin-bottom: 0 !important; - } - &__container { & > *:first-child { gap: 16px; diff --git a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js index 332fab99308d..481a51a0a202 100644 --- a/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js +++ b/ui/components/app/snaps/snap-ui-renderer/snap-ui-renderer.js @@ -131,6 +131,10 @@ const SnapUIRendererComponent = ({ className="snap-ui-renderer__content" height={BlockSize.Full} backgroundColor={contentBackgroundColor} + style={{ + overflowY: 'auto', + marginBottom: useFooter ? '80px' : '0', + }} > diff --git a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap index 27b21ab7ab3c..558011e68629 100644 --- a/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/snaps/snaps-section/__snapshots__/snaps-section.test.tsx.snap @@ -40,10 +40,10 @@ exports[`SnapsSection renders section for typed sign request 1`] = ` >

Date: Thu, 9 Jan 2025 21:40:23 +0800 Subject: [PATCH 58/71] fix: show localized snap name in snap tag (#29049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes the issue where the localized snap name is not being used when in the account tag. It also removes tech debt which is the `mergeAccount` function that isn't needed any more. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29049?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/769 ## **Manual testing steps** 1. Install the BTC snap 2. Create a BTC account 3. Click on the account menu and see that the name is correct. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../lib/accounts/BalancesController.test.ts | 2 + test/data/mock-send-state.json | 2 +- test/data/mock-state.json | 35 +++++++++- test/jest/mocks.ts | 8 ++- .../account-list-item/account-list-item.js | 22 +++--- .../account-list-item.stories.js | 2 +- .../account-list-item.test.js | 68 +++++++++++++------ .../account-list-menu.test.tsx | 9 ++- .../account-list-menu/account-list-menu.tsx | 33 --------- .../account-list-menu/hidden-account-list.js | 6 +- .../pages/connections/connections.tsx | 23 +++---- .../review-permissions-page.tsx | 13 ++-- .../__snapshots__/your-accounts.test.tsx.snap | 2 +- .../pages/send/components/your-accounts.tsx | 14 ++-- .../multichain/pages/send/send.test.js | 1 + ui/helpers/utils/accounts.js | 12 +++- ui/helpers/utils/accounts.test.js | 13 ++-- .../connect-page/connect-page.tsx | 14 ++-- .../remove-snap-account/snap-account-card.tsx | 11 +-- ui/selectors/selectors.test.js | 3 +- 20 files changed, 161 insertions(+), 132 deletions(-) diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index e18fbc1e7be8..3dec8c565c73 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -1,6 +1,7 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { Balance, BtcAccountType, CaipAssetType } from '@metamask/keyring-api'; import { InternalAccount } from '@metamask/keyring-internal-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { @@ -25,6 +26,7 @@ const mockBtcAccount = createMockInternalAccount({ options: { scope: MultichainNetworks.BITCOIN_TESTNET, }, + keyringType: KeyringTypes.snap, }); const mockBalanceResult = { diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 0b7cc237547f..0270926566d6 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -133,7 +133,7 @@ } } }, - "snaps": [{}], + "snaps": {}, "preferences": { "hideZeroBalanceTokens": false, "showFiatInTestnets": false, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 9ea2e674e7a3..b85b62ae80d7 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -370,6 +370,39 @@ "version": "5.1.2" } ] + }, + "local:snap-id": { + "id": "local:snap-id", + "origin": "local:snap-id", + "version": "5.1.2", + "iconUrl": null, + "initialPermissions": {}, + "manifest": { + "description": "mock snap description", + "proposedName": "mock snap name", + "repository": { + "type": "git", + "url": "https://127.0.0.1" + }, + "source": { + "location": { + "npm": { + "filePath": "dist/bundle.js", + "packageName": "@metamask/test-snap-dialog", + "registry": "https://registry.npmjs.org" + } + }, + "shasum": "L1k+dT9Q+y3KfIqzaH09MpDZVPS9ZowEh9w01ZMTWMU=" + }, + "version": "5.1.2" + }, + "versionHistory": [ + { + "date": 1680686075921, + "origin": "https://metamask.github.io", + "version": "5.1.2" + } + ] } }, "preferences": { @@ -541,7 +574,7 @@ }, "snap": { "enabled": true, - "id": "snap-id", + "id": "local:snap-id", "name": "snap-name" } }, diff --git a/test/jest/mocks.ts b/test/jest/mocks.ts index 9fdc6538d9d6..b165385d8f4b 100644 --- a/test/jest/mocks.ts +++ b/test/jest/mocks.ts @@ -186,7 +186,11 @@ export function createMockInternalAccount({ type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, lastSelected = 0, - snapOptions = undefined, + snapOptions = { + enabled: true, + id: 'npm:snap-id', + name: 'snap-name', + }, options = undefined, }: { name?: string; @@ -236,7 +240,7 @@ export function createMockInternalAccount({ keyring: { type: keyringType, }, - snap: snapOptions, + snap: keyringType === KeyringTypes.snap ? snapOptions : undefined, lastSelected, }, options: options ?? {}, 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 2c080c940d8a..affda29f6881 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -4,7 +4,7 @@ import classnames from 'classnames'; import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { shortenAddress } from '../../../helpers/utils/util'; +import { getSnapName, shortenAddress } from '../../../helpers/utils/util'; import { AccountListItemMenu } from '../account-list-item-menu'; import { AvatarGroup } from '../avatar-group'; @@ -53,6 +53,7 @@ import { getIsTokenNetworkFilterEqualCurrentNetwork, getShowFiatInTestnets, getChainIdsToPoll, + getSnapsMetadata, } from '../../../selectors'; import { getMultichainIsTestnet, @@ -73,6 +74,7 @@ import { normalizeSafeAddress } from '../../../../app/scripts/lib/multichain/add import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { useGetFormattedTokensPerChain } from '../../../hooks/useGetFormattedTokensPerChain'; import { useAccountTotalCrossChainFiatBalance } from '../../../hooks/useAccountTotalCrossChainFiatBalance'; +import { getAccountLabel } from '../../../helpers/utils/accounts'; import { AccountListItemMenuTypes } from './account-list-item.types'; const MAXIMUM_CURRENCY_DECIMALS = 3; @@ -99,6 +101,14 @@ const AccountListItem = ({ const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); const [accountListItemMenuElement, setAccountListItemMenuElement] = useState(); + const snapMetadata = useSelector(getSnapsMetadata); + const accountLabel = getAccountLabel( + account.metadata.keyring.type, + account, + account.metadata.keyring.type === KeyringType.snap + ? getSnapName(snapMetadata)(account.metadata?.snap.id) + : null, + ); const useBlockie = useSelector(getUseBlockie); const { isEvmNetwork } = useMultichainSelector(getMultichainNetwork, account); @@ -402,9 +412,9 @@ const AccountListItem = ({ )} - {account.label ? ( + {accountLabel ? ( setAccountOptionsMenuOpen(false)} isOpen={accountOptionsMenuOpen} - isRemovable={account.keyring.type !== KeyringType.hdKeyTree} + isRemovable={account.metadata.keyring.type !== KeyringType.hdKeyTree} closeMenu={closeMenu} isPinned={isPinned} isHidden={isHidden} @@ -487,10 +497,6 @@ AccountListItem.propTypes = { type: PropTypes.string.isRequired, }).isRequired, }).isRequired, - keyring: PropTypes.shape({ - type: PropTypes.string.isRequired, - }).isRequired, - label: PropTypes.string, }).isRequired, /** * Represents if this account is currently selected diff --git a/ui/components/multichain/account-list-item/account-list-item.stories.js b/ui/components/multichain/account-list-item/account-list-item.stories.js index 913879b5a71a..6de7e61e8ea5 100644 --- a/ui/components/multichain/account-list-item/account-list-item.stories.js +++ b/ui/components/multichain/account-list-item/account-list-item.stories.js @@ -64,7 +64,7 @@ const SNAP_ACCOUNT = { }, snap: { name: 'Test Snap Name', - id: 'snap-id', + id: 'npm:snap-id', enabled: true, }, }, diff --git a/ui/components/multichain/account-list-item/account-list-item.test.js b/ui/components/multichain/account-list-item/account-list-item.test.js index 94619d2ae389..39705dac4e8b 100644 --- a/ui/components/multichain/account-list-item/account-list-item.test.js +++ b/ui/components/multichain/account-list-item/account-list-item.test.js @@ -19,9 +19,6 @@ const mockAccount = { 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3' ], balance: '0x152387ad22c3f0', - keyring: { - type: 'HD Key Tree', - }, }; const mockNonEvmAccount = { @@ -31,6 +28,40 @@ const mockNonEvmAccount = { type: 'bip122:p2wpkh', }; +const mockSnap = { + id: 'local:mock-snap', + origin: 'local:mock-snap', + version: '1.3.7', + iconUrl: null, + initialPermissions: {}, + manifest: { + description: 'mock-description', + proposedName: 'mock-snap-name', + repository: { + type: 'git', + url: 'https://127.0.0.1', + }, + source: { + location: { + npm: { + filePath: 'dist/bundle.js', + packageName: 'local:mock-snap', + }, + }, + shasum: 'L1k+dT9Q+y3KfIqzaH09MpDZVPS9ZowEh9w01ZMTWMU=', + locales: ['en'], + }, + version: '1.3.7', + }, + versionHistory: [ + { + date: 1680686075921, + origin: 'https://metamask.github.io', + version: '1.3.7', + }, + ], +}; + const DEFAULT_PROPS = { account: mockAccount, selected: false, @@ -64,6 +95,10 @@ const render = (props = {}, state = {}) => { conversionRate: '100000', }, }, + snaps: { + ...mockState.metamask.snaps, + [mockSnap.id]: mockSnap, + }, }, activeTab: { id: 113, @@ -172,30 +207,25 @@ describe('AccountListItem', () => { }); ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - it('renders the snap label for unnamed snap accounts', () => { - const { container } = render({ - account: { - ...mockAccount, - balance: '0x0', - keyring: 'Snap Keyring', - label: 'Snaps (Beta)', - }, - }); - const tag = container.querySelector('.mm-tag'); - expect(tag.textContent).toBe('Snaps (Beta)'); - }); - it('renders the snap name for named snap accounts', () => { const { container } = render({ account: { ...mockAccount, + metadata: { + ...mockAccount.metadata, + snap: { + id: mockSnap.id, + }, + keyring: { + type: 'Snap Keyring', + }, + }, + balance: '0x0', - keyring: 'Snap Keyring', - label: 'Test Snap Name (Beta)', }, }); const tag = container.querySelector('.mm-tag'); - expect(tag.textContent).toBe('Test Snap Name (Beta)'); + expect(tag.textContent).toBe(`${mockSnap.manifest.proposedName} (Beta)`); }); ///: END:ONLY_INCLUDE_IF diff --git a/ui/components/multichain/account-list-menu/account-list-menu.test.tsx b/ui/components/multichain/account-list-menu/account-list-menu.test.tsx index 539899d0eb09..0d7884fcd666 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.test.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.test.tsx @@ -479,6 +479,9 @@ describe('AccountListMenu', () => { keyring: { type: 'Snap Keyring', }, + snap: { + id: 'local:snap-id', + }, }, }, }, @@ -491,7 +494,7 @@ describe('AccountListMenu', () => { '.multichain-account-list-item', ); const tag = listItems[0].querySelector('.mm-tag') as Element; - expect(tag.textContent).toBe('Snaps (Beta)'); + expect(tag.textContent).toBe('mock snap name (Beta)'); }); it('displays the correct label for named snap accounts', () => { @@ -511,7 +514,7 @@ describe('AccountListMenu', () => { }, snap: { name: 'Test Snap Name', - id: 'test-snap-id', + id: 'local:snap-id', }, }, }, @@ -524,7 +527,7 @@ describe('AccountListMenu', () => { '.multichain-account-list-item', ); const tag = listItems[0].querySelector('.mm-tag') as Element; - expect(tag.textContent).toBe('Test Snap Name (Beta)'); + expect(tag.textContent).toBe('mock snap name (Beta)'); }); ///: END:ONLY_INCLUDE_IF 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 ca9852af1474..ea79ad84f879 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -23,7 +23,6 @@ import { ///: END:ONLY_INCLUDE_IF } from '@metamask/keyring-api'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -import { InternalAccount } from '@metamask/keyring-internal-api'; import { BITCOIN_WALLET_NAME, BITCOIN_WALLET_SNAP_ID, @@ -96,7 +95,6 @@ import { // eslint-disable-next-line import/no-restricted-paths import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; -import { getAccountLabel } from '../../../helpers/utils/accounts'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) import { ACCOUNT_WATCHER_NAME, @@ -185,36 +183,6 @@ export const getActionTitle = ( } }; -/** - * Merges ordered accounts with balances with each corresponding account data from internal accounts - * - * @param accountsWithBalances - ordered accounts with balances - * @param internalAccounts - internal accounts - * @returns merged accounts list with balances and internal account data - */ -export const mergeAccounts = ( - accountsWithBalances: MergedInternalAccount[], - internalAccounts: InternalAccount[], -) => { - return accountsWithBalances.map((account) => { - const internalAccount = internalAccounts.find( - (intAccount) => intAccount.address === account.address, - ); - if (internalAccount) { - return { - ...account, - ...internalAccount, - keyring: internalAccount.metadata.keyring, - label: getAccountLabel( - internalAccount.metadata.keyring.type, - internalAccount, - ), - }; - } - return account; - }); -}; - type AccountListMenuProps = { onClose: () => void; privacyMode?: boolean; @@ -345,7 +313,6 @@ export const AccountListMenu = ({ fuse.setCollection(filteredAccounts); searchResults = fuse.search(searchQuery); } - searchResults = mergeAccounts(searchResults, filteredAccounts); const title = useMemo( () => getActionTitle(t as (text: string) => string, actionMode), diff --git a/ui/components/multichain/account-list-menu/hidden-account-list.js b/ui/components/multichain/account-list-menu/hidden-account-list.js index 6ae3a32da20b..1fad2cd0a56b 100644 --- a/ui/components/multichain/account-list-menu/hidden-account-list.js +++ b/ui/components/multichain/account-list-menu/hidden-account-list.js @@ -19,7 +19,6 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getConnectedSubjectsForAllAddresses, getHiddenAccountsList, - getInternalAccounts, getMetaMaskAccountsOrdered, getOriginOfCurrentTab, getSelectedAccount, @@ -38,7 +37,6 @@ import { AccountListItem, AccountListItemMenuTypes, } from '../account-list-item'; -import { mergeAccounts } from './account-list-menu'; export const HiddenAccountList = ({ onClose }) => { const t = useI18nContext(); @@ -46,12 +44,10 @@ export const HiddenAccountList = ({ onClose }) => { const dispatch = useDispatch(); const hiddenAddresses = useSelector(getHiddenAccountsList); const accounts = useSelector(getMetaMaskAccountsOrdered); - const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts = mergeAccounts(accounts, internalAccounts); const selectedAccount = useSelector(getSelectedAccount); const connectedSites = useSelector(getConnectedSubjectsForAllAddresses); const currentTabOrigin = useSelector(getOriginOfCurrentTab); - const filteredHiddenAccounts = mergedAccounts.filter((account) => + const filteredHiddenAccounts = accounts.filter((account) => hiddenAddresses.includes(account.address), ); const [showListItem, setShowListItem] = useState(false); diff --git a/ui/components/multichain/pages/connections/connections.tsx b/ui/components/multichain/pages/connections/connections.tsx index 4a8b188b86b6..0592f5de84ee 100644 --- a/ui/components/multichain/pages/connections/connections.tsx +++ b/ui/components/multichain/pages/connections/connections.tsx @@ -18,7 +18,6 @@ import { getURLHost } from '../../../../helpers/utils/util'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { getConnectedSitesList, - getInternalAccounts, getOrderedConnectedAccountsForConnectedDapp, getPermissionSubjects, getPermittedAccountsByOrigin, @@ -43,7 +42,6 @@ import { IconSize, Text, } from '../../../component-library'; -import { mergeAccounts } from '../../account-list-menu/account-list-menu'; import { AccountListItem, AccountListItemMenuTypes, @@ -109,11 +107,6 @@ export const Connections = () => { getOrderedConnectedAccountsForConnectedDapp(state, activeTabOrigin), ); const selectedAccount = useSelector(getSelectedAccount); - const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts = mergeAccounts( - connectedAccounts, - internalAccounts, - ) as AccountType[]; const permittedAccountsByOrigin = useSelector( getPermittedAccountsByOrigin, @@ -168,14 +161,14 @@ export const Connections = () => { } }; - // In the mergeAccounts, we need the lastSelected value to determine which connectedAccount was last selected. - const latestSelected = mergedAccounts.findIndex( + // In the connectedAccounts, we need the lastSelected value to determine which connectedAccount was last selected. + const latestSelected = connectedAccounts.findIndex( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any (_account: any, index: any) => { return ( index === - mergedAccounts.reduce( + connectedAccounts.reduce( ( indexOfAccountWIthHighestLastSelected: number, currentAccountToCompare: AccountType, @@ -185,7 +178,7 @@ export const Connections = () => { ) => { const currentLastSelected = currentAccountToCompare.metadata.lastSelected ?? 0; - const accountAtIndexLastSelected = mergedAccounts[ + const accountAtIndexLastSelected = connectedAccounts[ indexOfAccountWIthHighestLastSelected ].metadata.lastSelected ? i @@ -251,11 +244,11 @@ export const Connections = () => { - {permittedAccounts.length > 0 && mergeAccounts.length > 0 ? ( + {permittedAccounts.length > 0 && connectedAccounts.length > 0 ? ( {/* TODO: Replace `any` with type */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {mergedAccounts.map((account: AccountType, index: any) => { + {connectedAccounts.map((account: AccountType, index: any) => { const connectedSites: ConnectedSites = {}; const connectedSite = connectedSites[account.address]?.find( ({ origin }) => origin === activeTabOrigin, @@ -271,7 +264,7 @@ export const Connections = () => { { /> ) : null} - {permittedAccounts.length > 0 && mergeAccounts.length > 0 ? ( + {permittedAccounts.length > 0 && connectedAccounts.length > 0 ? ( { }; const accounts = useSelector(getUpdatedAndSortedAccounts); - const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts: MergedInternalAccount[] = useMemo(() => { - return mergeAccounts(accounts, internalAccounts).filter( - (account: InternalAccount) => isEvmAccountType(account.type), + const evmAccounts: MergedInternalAccount[] = useMemo(() => { + return accounts.filter((account: InternalAccount) => + isEvmAccountType(account.type), ); - }, [accounts, internalAccounts]); + }, [accounts]); const connectedAccountAddresses = useSelector((state) => getPermittedAccountsForSelectedTab(state, activeTabOrigin), @@ -196,7 +193,7 @@ export const ReviewPermissions = () => { - snap-name (Beta) + mock snap name (Beta)

diff --git a/ui/components/multichain/pages/send/components/your-accounts.tsx b/ui/components/multichain/pages/send/components/your-accounts.tsx index a0d431919ce6..6123bb1cac78 100644 --- a/ui/components/multichain/pages/send/components/your-accounts.tsx +++ b/ui/components/multichain/pages/send/components/your-accounts.tsx @@ -4,7 +4,6 @@ import { EthAccountType, KeyringAccountType } from '@metamask/keyring-api'; import { InternalAccount } from '@metamask/keyring-internal-api'; import { getUpdatedAndSortedAccounts, - getInternalAccounts, getSelectedInternalAccount, } from '../../../../../selectors'; import { AccountListItem } from '../../..'; @@ -13,13 +12,11 @@ import { updateRecipient, updateRecipientUserInput, } from '../../../../../ducks/send'; -import { mergeAccounts } from '../../../account-list-menu/account-list-menu'; import { MetaMetricsContext } from '../../../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../../../shared/constants/metametrics'; -import { MergedInternalAccount } from '../../../../../selectors/selectors.types'; import { SendPageRow } from '.'; type SendPageYourAccountsProps = { @@ -36,19 +33,18 @@ export const SendPageYourAccounts = ({ // Your Accounts const accounts = useSelector(getUpdatedAndSortedAccounts); - const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts: MergedInternalAccount[] = useMemo(() => { - return mergeAccounts(accounts, internalAccounts).filter( - (account: InternalAccount) => allowedAccountTypes.includes(account.type), + const filteredAccounts = useMemo(() => { + return accounts.filter((account: InternalAccount) => + allowedAccountTypes.includes(account.type), ); - }, [accounts, internalAccounts]); + }, [accounts]); const selectedAccount = useSelector(getSelectedInternalAccount); return ( {/* TODO: Replace `any` with type */} {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} - {mergedAccounts.map((account: any) => ( + {filteredAccounts.map((account: any) => ( { }); describe('Snap Account Label', () => { + const mockSnapName = 'Test Snap Name'; const mockSnapAccountWithName = { ...mockAccount, metadata: { ...mockAccount.metadata, type: KeyringType.snap, - snap: { name: 'Test Snap Name' }, + snap: { name: mockSnapName }, }, }; const mockSnapAccountWithoutName = { @@ -179,9 +180,13 @@ describe('Accounts', () => { }; it('should return snap name with beta tag if snap name is provided', () => { - expect(getAccountLabel(KeyringType.snap, mockSnapAccountWithName)).toBe( - 'Test Snap Name (Beta)', - ); + expect( + getAccountLabel( + KeyringType.snap, + mockSnapAccountWithName, + mockSnapName, + ), + ).toBe('Test Snap Name (Beta)'); }); it('should return generic snap label with beta tag if snap name is not provided', () => { diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx index 741629b3d4d7..0baa9f393b77 100644 --- a/ui/pages/permissions-connect/connect-page/connect-page.tsx +++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx @@ -5,7 +5,6 @@ import { isEvmAccountType } from '@metamask/keyring-api'; import { NetworkConfiguration } from '@metamask/network-controller'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { - getInternalAccounts, getSelectedInternalAccount, getUpdatedAndSortedAccounts, } from '../../../selectors'; @@ -31,8 +30,6 @@ import { 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'; import { @@ -117,12 +114,11 @@ export const ConnectPage: React.FC = ({ ); const accounts = useSelector(getUpdatedAndSortedAccounts); - const internalAccounts = useSelector(getInternalAccounts); - const mergedAccounts: MergedInternalAccount[] = useMemo(() => { - return mergeAccounts(accounts, internalAccounts).filter( - (account: InternalAccount) => isEvmAccountType(account.type), + const evmAccounts = useMemo(() => { + return accounts.filter((account: InternalAccount) => + isEvmAccountType(account.type), ); - }, [accounts, internalAccounts]); + }, [accounts]); const currentAccount = useSelector(getSelectedInternalAccount); const currentAccountAddress = isEvmAccountType(currentAccount.type) @@ -157,7 +153,7 @@ export const ConnectPage: React.FC = ({ { const accounts = useSelector(getMetaMaskAccountsOrdered); - const internalAccounts = useSelector(getInternalAccounts); - // We should stop using mergeAccounts and write a new selector instead - const mergedAccounts = mergeAccounts(accounts, internalAccounts); - const account = mergedAccounts.find( + const account = accounts.find( (internalAccount: { address: string }) => internalAccount.address === address, ) as MergedInternalAccount; diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index aa6ae125bd4b..fce129745561 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1297,6 +1297,7 @@ describe('Selectors', () => { 'npm:@metamask/test-snap-networkAccess': false, 'npm:@metamask/test-snap-notify': false, 'npm:@metamask/test-snap-wasm': false, + 'local:snap-id': false, }); }); @@ -1520,7 +1521,7 @@ describe('Selectors', () => { name: 'Snap Account 1', snap: { enabled: true, - id: 'snap-id', + id: 'local:snap-id', name: 'snap-name', }, }, From 6e05923cb64e689628697ac05cbee8c86d7b12e2 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 9 Jan 2025 16:04:20 +0000 Subject: [PATCH 59/71] fix: Catch errors from the assets controller (#29439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses a group of errors reported on Sentry that occur when `getTokenStandardAndDetails` in `assetsContractController` returns "Unable to determine contract standard". [Sentry Issue Link](https://metamask.sentry.io/issues/5660074561/?project=273505&query=is%3Aunresolved%20getTokenStandardAndDetails&referrer=issue-stream&sort=date&statsPeriod=7d&stream_index=0). The error happens fairly often because only contracts that strictly adhere to the standard ABIs for ERC20, ERC721, and ERC1155 are correctly parsed. When a contract does not conform to these standards, the function fails and throws an error. This PR introduces error handling to catch and silence the error when assetsContractController.getTokenStandardAndDetails fails to determine the contract standard. The control flow of the function is adjusted to ensure it continues to execute normally even when the error is caught. ### Changes - Added a try...catch block around the assetsContractController.getTokenStandardAndDetails call to catch and log the error. - Ensured that the function continues to execute and return appropriate details even when the error is caught. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29439?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/25212 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 63 +++++++++++++++++------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 49517145bcd7..b869cd704633 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4521,39 +4521,46 @@ export default class MetamaskController extends EventEmitter { // or if it is true but the `fetchTokenBalance`` call failed. In either case, we should // attempt to retrieve details from `assetsContractController.getTokenStandardAndDetails` if (details === undefined) { - details = await this.assetsContractController.getTokenStandardAndDetails( - address, - userAddress, - tokenId, - ); + try { + details = + await this.assetsContractController.getTokenStandardAndDetails( + address, + userAddress, + tokenId, + ); + } catch (e) { + log.warn(`Failed to get token standard and details. Error: ${e}`); + } } - const tokenDetailsStandardIsERC1155 = isEqualCaseInsensitive( - details.standard, - TokenStandard.ERC1155, - ); + if (details) { + const tokenDetailsStandardIsERC1155 = isEqualCaseInsensitive( + details.standard, + TokenStandard.ERC1155, + ); - if (tokenDetailsStandardIsERC1155) { - try { - const balance = await fetchERC1155Balance( - address, - userAddress, - tokenId, - this.provider, - ); + if (tokenDetailsStandardIsERC1155) { + try { + const balance = await fetchERC1155Balance( + address, + userAddress, + tokenId, + this.provider, + ); - const balanceToUse = balance?._hex - ? parseInt(balance._hex, 16).toString() - : null; + const balanceToUse = balance?._hex + ? parseInt(balance._hex, 16).toString() + : null; - details = { - ...details, - balance: balanceToUse, - }; - } catch (e) { - // If the `fetchTokenBalance` call failed, `details` remains undefined, and we - // fall back to the below `assetsContractController.getTokenStandardAndDetails` call - log.warn('Failed to get token balance. Error:', e); + details = { + ...details, + balance: balanceToUse, + }; + } catch (e) { + // If the `fetchTokenBalance` call failed, `details` remains undefined, and we + // fall back to the below `assetsContractController.getTokenStandardAndDetails` call + log.warn('Failed to get token balance. Error:', e); + } } } From 4cbce35280445237862f064dcb4f07d2c12637f9 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 9 Jan 2025 10:18:59 -0600 Subject: [PATCH 60/71] chore: Remove toggle to turn on/off Per Dapp Selected Network Feature (#29301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The "Select networks for each site" preference toggle on the experimental settings page has been live for many releases now since the toggle has been turned on by default. We meant to remove it a while ago. Screenshot 2024-11-19 at 10 58 04 AM This PR removes this toggle ~and integrates new versions of the QueuedRequestController and SelectedNetworkController which remove the backend logic it operated.~ - We have delayed the updates to the controllers side because of some mobile side requirements. See [this PR](https://github.com/MetaMask/core/pull/5065#issue-2736965186) for more context: > We are not yet ready to release per-dapp selected network functionality on the mobile client and with this change there is no clean way to https://github.com/MetaMask/metamask-mobile/issues/12434#issuecomment-2537358920 in the mobile client without having the Domains state starting to populate and possibly become corrupt since its not being consumed by/updated by the frontend in the expected way and may need to be migrated away when its time to actually start using the controller. > Without this revert the @MetaMask/wallet-framework team is blocked from completing their goal to get both clients up to the latest versions of all controllers. Beyond the fact that this removal is overdue, another reason we should remove this now is that having this setting when turned off is [causing a bug](https://github.com/MetaMask/metamask-extension/issues/28441) with `wallet_switchEthereumChain` and the interaction with the new chain permissions feature. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/2844 ## **Manual testing steps** 1. Go to experimental tab of settings 2. See that there is no longer a toggleable preference called "Selected Networks for each site" 3. See that Per Dapp Selected Network Functionality is still on by default ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/_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/fr/messages.json | 12 - app/_locales/hi/messages.json | 12 - app/_locales/id/messages.json | 12 - app/_locales/ja/messages.json | 12 - app/_locales/ko/messages.json | 12 - app/_locales/pt/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/scripts/background.js | 6 +- app/scripts/constants/sentry-state.ts | 1 - .../preferences-controller.test.ts | 13 - .../controllers/preferences-controller.ts | 26 -- app/scripts/fixtures/with-preferences.js | 1 - app/scripts/lib/backup.test.js | 1 - app/scripts/metamask-controller.js | 66 +--- app/scripts/migrations/136.test.ts | 56 +++ app/scripts/migrations/136.ts | 37 ++ app/scripts/migrations/index.js | 1 + test/e2e/default-fixture.js | 1 - test/e2e/fixture-builder.js | 11 +- test/e2e/json-rpc/switchEthereumChain.spec.js | 121 ------ .../pages/settings/experimental-settings.ts | 8 - test/e2e/restore/MetaMaskUserData.json | 1 - .../alerts/queued-confirmations.spec.ts | 8 +- .../review-switch-permission-page.spec.js | 2 +- ...rs-after-init-opt-in-background-state.json | 1 - .../errors-after-init-opt-in-ui-state.json | 1 - ...s-before-init-opt-in-background-state.json | 1 - .../errors-before-init-opt-in-ui-state.json | 1 - .../multichain/all-permissions-page.spec.ts | 1 - .../batch-txs-per-dapp-diff-network.spec.js | 2 - .../batch-txs-per-dapp-extra-tx.spec.js | 2 - .../batch-txs-per-dapp-same-network.spec.js | 2 - .../request-queuing/chainid-check.spec.js | 369 +++++------------- .../dapp1-send-dapp2-signTypedData.spec.js | 2 - .../dapp1-subscribe-network-switch.spec.ts | 1 - ...-switch-dapp2-eth-request-accounts.spec.js | 3 - .../dapp1-switch-dapp2-send.spec.js | 4 - .../request-queuing/enable-queuing.spec.js | 47 --- ...multi-dapp-sendTx-revokePermission.spec.js | 2 - .../multiple-networks-dapps-txs.spec.js | 2 - .../sendTx-revokePermissions.spec.ts | 1 - .../sendTx-switchChain-sendTx.spec.js | 2 +- .../request-queuing/switch-network.spec.js | 2 - .../switchChain-sendTx.spec.js | 2 +- .../switchChain-watchAsset.spec.js | 2 +- test/e2e/tests/request-queuing/ui.spec.js | 25 +- .../watchAsset-switchChain-watchAsset.spec.js | 2 +- .../data/integration-init-state.json | 1 - .../data/onboarding-completion-route.json | 1 - .../nfts/nfts-items/nfts-items.stories.tsx | 1 - .../network-list-menu.test.js | 1 - .../network-list-menu/network-list-menu.tsx | 8 +- ui/index.js | 6 +- ui/pages/routes/routes.component.js | 3 - ui/pages/routes/routes.container.js | 2 - .../experimental-tab.component.tsx | 18 - .../experimental-tab.container.ts | 4 - .../experimental-tab/experimental-tab.test.js | 2 +- ui/selectors/selectors.js | 12 - ui/selectors/selectors.test.js | 1 - ui/store/actions.ts | 8 - 70 files changed, 241 insertions(+), 856 deletions(-) create mode 100644 app/scripts/migrations/136.test.ts create mode 100644 app/scripts/migrations/136.ts delete mode 100644 test/e2e/tests/request-queuing/enable-queuing.spec.js diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 5a499ce7dc6d..c466b7273b95 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -5972,18 +5972,6 @@ "message": "An: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Dadurch können Sie ein Netzwerk für jede Website auswählen, anstatt ein einziges Netzwerk für alle Websites auszuwählen. Diese Funktion verhindert, dass Sie manuell zwischen den Netzwerken wechseln müssen, was die Benutzerfreundlichkeit auf bestimmten Websites beeinträchtigen könnte." - }, - "toggleRequestQueueField": { - "message": "Wählen Sie Netzwerke für jede Website" - }, - "toggleRequestQueueOff": { - "message": "Aus" - }, - "toggleRequestQueueOn": { - "message": "An" - }, "token": { "message": "Token" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index d930bafaa3d7..c4cce0299949 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -5972,18 +5972,6 @@ "message": "Προς: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Αυτό σας επιτρέπει να επιλέξετε ένα δίκτυο για κάθε ιστότοπο αντί για ένα μόνο επιλεγμένο δίκτυο για όλους τους ιστότοπους. Αυτή η λειτουργία θα σας αποτρέψει από το να αλλάζετε δίκτυα χειροκίνητα, το οποίο μπορεί να διαταράξει την εμπειρία του χρήστη σε ορισμένους ιστότοπους." - }, - "toggleRequestQueueField": { - "message": "Επιλογή δικτύων για κάθε ιστότοπο" - }, - "toggleRequestQueueOff": { - "message": "Απενεργοποίηση" - }, - "toggleRequestQueueOn": { - "message": "Ενεργοποίηση" - }, "token": { "message": "Token" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a45457e8b4c1..d16ea6af6580 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6274,18 +6274,6 @@ "toggleDecodeDescription": { "message": "We use 4byte.directory and Sourcify services to decode and display more readable transaction data. This helps you understand the outcome of pending and past transactions, but can result in your IP address being shared." }, - "toggleRequestQueueDescription": { - "message": "This allows you to select a network for each site instead of a single selected network for all sites. This feature will prevent you from switching networks manually, which may break your user experience on certain sites." - }, - "toggleRequestQueueField": { - "message": "Select networks for each site" - }, - "toggleRequestQueueOff": { - "message": "Off" - }, - "toggleRequestQueueOn": { - "message": "On" - }, "token": { "message": "Token" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index f66111064a90..a915059f262f 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -5744,18 +5744,6 @@ "toggleEthSignOn": { "message": "ON (Not recommended)" }, - "toggleRequestQueueDescription": { - "message": "This allows you to select a network for each site instead of a single selected network for all sites. This feature will prevent you from switching networks manually, which may break your user experience on certain sites." - }, - "toggleRequestQueueField": { - "message": "Select networks for each site" - }, - "toggleRequestQueueOff": { - "message": "Off" - }, - "toggleRequestQueueOn": { - "message": "On" - }, "token": { "message": "Token" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index bfb53061f73b..7e8253d37d46 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -5972,18 +5972,6 @@ "message": "Para: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Esto le permite seleccionar una red para cada sitio en lugar de una única red seleccionada para todos los sitios. Esta función evitará que cambie de red manualmente, lo que puede afectar su experiencia de usuario en ciertos sitios." - }, - "toggleRequestQueueField": { - "message": "Seleccionar redes para cada sitio" - }, - "toggleRequestQueueOff": { - "message": "Desactivado" - }, - "toggleRequestQueueOn": { - "message": "Activado" - }, "token": { "message": "Token" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index a86d465b786d..1d4777e5aaea 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -5972,18 +5972,6 @@ "message": "Vers : $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Cette fonction vous permet de sélectionner un réseau pour chaque site au lieu d’un seul réseau pour tous les sites. Vous n’aurez donc pas à changer manuellement de réseau, ce qui pourrait nuire à l’expérience utilisateur sur certains sites." - }, - "toggleRequestQueueField": { - "message": "Sélectionnez les réseaux pour chaque site" - }, - "toggleRequestQueueOff": { - "message": "Désactiver" - }, - "toggleRequestQueueOn": { - "message": "Activer" - }, "token": { "message": "Jeton" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 144db40e41a3..39bd7241e6cc 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -5972,18 +5972,6 @@ "message": "प्रति: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "ऐसा करके, आप सभी साइटों के लिए कोई सिंगल नेटवर्क चुनने के बजाय हरेक साइट के लिए एक नेटवर्क चुन सकते हैं। यह फीचर आपको मैन्युअल तरीके से नेटवर्क स्विच करने से रोकता है, इस वजह से कुछ साइटों पर आपका यूज़र अनुभव ख़राब हो सकता है।" - }, - "toggleRequestQueueField": { - "message": "हरेक साइट के लिए नेटवर्क चुनें" - }, - "toggleRequestQueueOff": { - "message": "बंद करें" - }, - "toggleRequestQueueOn": { - "message": "चालू करें" - }, "token": { "message": "टोकन" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 44b2bf804727..4faf653d1419 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -5972,18 +5972,6 @@ "message": "Ke: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Hal ini memungkinkan Anda memilih jaringan untuk setiap situs, daripada satu jaringan yang dipilih untuk semua situs. Fitur ini akan mencegah Anda berpindah jaringan secara manual, yang dapat merusak pengalaman pengguna di situs tertentu." - }, - "toggleRequestQueueField": { - "message": "Pilih jaringan untuk setiap situs" - }, - "toggleRequestQueueOff": { - "message": "Nonaktif" - }, - "toggleRequestQueueOn": { - "message": "Aktif" - }, "token": { "message": "Token" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 70d6da31301e..96d3a404faa4 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -5972,18 +5972,6 @@ "message": "移動先: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "これにより、選択した単一のネットワークをすべてのサイトで使用するのではなく、サイトごとにネットワークを選択できます。この機能により、特定のサイトでのユーザーエクスペリエンスの妨げとなる、ネットワークの手動切り替えが不要になります。" - }, - "toggleRequestQueueField": { - "message": "サイトごとにネットワークを選択する" - }, - "toggleRequestQueueOff": { - "message": "オフ" - }, - "toggleRequestQueueOn": { - "message": "オン" - }, "token": { "message": "トークン" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6978164f82f4..ae3942931205 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -5972,18 +5972,6 @@ "message": "수신: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "이 기능을 이용하면 모든 사이트에 한 가지 네트워크를 선택하여 사용하는 대신, 사이트별로 네트워크를 다르게 선택할 수 있습니다. 이 기능을 사용하면 수동으로 네트워크를 전환하지 않아도 되므로 특정 사이트에서 사용자 경험이 저해되지 않습니다." - }, - "toggleRequestQueueField": { - "message": "각 사이트별 네트워크 선택" - }, - "toggleRequestQueueOff": { - "message": "끄기" - }, - "toggleRequestQueueOn": { - "message": "켜기" - }, "token": { "message": "토큰" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 7819be595e94..7f9f0cb72311 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -5972,18 +5972,6 @@ "message": "Para: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Isso permite que você selecione uma rede para cada site em vez de uma única rede selecionada para todos eles. O recurso evitará que você alterne manualmente entre redes, o que pode atrapalhar sua experiência de usuário em determinados sites." - }, - "toggleRequestQueueField": { - "message": "Selecionar redes para cada site" - }, - "toggleRequestQueueOff": { - "message": "Não" - }, - "toggleRequestQueueOn": { - "message": "Sim" - }, "token": { "message": "Token" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 5e281f34ac05..e785dfeb2d67 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -5972,18 +5972,6 @@ "message": "Адресат: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Это позволяет вам выбрать сеть для каждого сайта вместо одной выбранной сети для всех сайтов. Эта функция не позволит вам переключать сети вручную, что может снизить удобство использования некоторых сайтов." - }, - "toggleRequestQueueField": { - "message": "Выберите сети для каждого сайта" - }, - "toggleRequestQueueOff": { - "message": "Выкл." - }, - "toggleRequestQueueOn": { - "message": "Вкл." - }, "token": { "message": "Токен" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 5a6bce602970..c854917ae676 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -5972,18 +5972,6 @@ "message": "Para kay/sa: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Pinahihintulutan ka nitong pumili ng network para sa bawat site sa halip na iisang piniling network para sa lahat ng site. Pipigilan ka ng feature na ito na magpalit ng network nang mano-mano, na maaaring makasira sa iyong karanasan bilang user sa ilang partikular na site." - }, - "toggleRequestQueueField": { - "message": "Piliin ang mga network para sa bawat site" - }, - "toggleRequestQueueOff": { - "message": "Naka-off" - }, - "toggleRequestQueueOn": { - "message": "Naka-on" - }, "token": { "message": "Token" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 046905392710..dc26bb137e19 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -5972,18 +5972,6 @@ "message": "Alıcı: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Bu, tüm siteler için tek bir seçili ağ yerine her bir site için bir ağ seçebilmenize olanak sağlar. Bu özellik, manuel olarak ağ değiştirmenizi önleyebilir ve bu da belirli sitelerde kullanıcı deneyiminizi bozabilir." - }, - "toggleRequestQueueField": { - "message": "Her site için ağ seçin" - }, - "toggleRequestQueueOff": { - "message": "Kapalı" - }, - "toggleRequestQueueOn": { - "message": "Açık" - }, "token": { "message": "Token" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index a6baaff586aa..5f372083000f 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -5972,18 +5972,6 @@ "message": "Đến: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "Tính năng này cho phép bạn chọn mạng cho từng trang web thay vì một mạng duy nhất được chọn cho tất cả các trang web. Tính năng này sẽ ngăn bạn chuyển đổi mạng theo cách thủ công, điều này có thể ảnh hưởng đến trải nghiệm người dùng của bạn trên một số trang web." - }, - "toggleRequestQueueField": { - "message": "Chọn mạng cho từng trang web" - }, - "toggleRequestQueueOff": { - "message": "Tắt" - }, - "toggleRequestQueueOn": { - "message": "Bật" - }, "token": { "message": "Token" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 5472a92a5660..5ea2340440a5 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -5972,18 +5972,6 @@ "message": "至:$1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleRequestQueueDescription": { - "message": "这使您可以为每个网站选择网络,而不是为所有网站选择同一个网络。此功能将阻止您手动切换网络,这可能会破坏您在某些网站上的用户体验。" - }, - "toggleRequestQueueField": { - "message": "为每个网站选择网络" - }, - "toggleRequestQueueOff": { - "message": "关" - }, - "toggleRequestQueueOn": { - "message": "开" - }, "token": { "message": "代币" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index c6a3bccd46aa..029adf026841 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -1153,10 +1153,8 @@ export function setupController( controller.appStateController.waitingForUnlock.length + controller.approvalController.getTotalApprovalCount(); - if (controller.preferencesController.getUseRequestQueue()) { - pendingApprovalCount += - controller.queuedRequestController.state.queuedRequestCount; - } + pendingApprovalCount += + controller.queuedRequestController.state.queuedRequestCount; return pendingApprovalCount; } catch (error) { console.error('Failed to get pending approval count:', error); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 705b33f450ba..23e4c2d45af9 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -275,7 +275,6 @@ export const SENTRY_BACKGROUND_STATE = { useNonceField: true, usePhishDetect: true, useTokenDetection: true, - useRequestQueue: true, useTransactionSimulations: true, enableMV3TimestampSave: true, }, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index f30e938e635e..eac95430ec3a 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -640,19 +640,6 @@ describe('preferences controller', () => { }); }); - 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({}); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index 217fe43b022d..8a09539ea917 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -143,7 +143,6 @@ export type PreferencesControllerState = Omit< useMultiAccountBalanceChecker: boolean; use4ByteResolution: boolean; useCurrencyRateCheck: boolean; - useRequestQueue: boolean; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; ///: END:ONLY_INCLUDE_IF @@ -190,7 +189,6 @@ export const getDefaultPreferencesControllerState = useNftDetection: true, use4ByteResolution: true, useCurrencyRateCheck: true, - useRequestQueue: true, openSeaEnabled: true, securityAlertsEnabled: true, watchEthereumAccountEnabled: false, @@ -342,10 +340,6 @@ const controllerMetadata = { persist: true, anonymous: true, }, - useRequestQueue: { - persist: true, - anonymous: true, - }, openSeaEnabled: { persist: true, anonymous: true, @@ -642,17 +636,6 @@ export class PreferencesController extends BaseController< }); } - /** - * Setter for the `useRequestQueue` property - * - * @param val - Whether or not the user wants to have requests queued if network change is required. - */ - setUseRequestQueue(val: boolean): void { - this.update((state) => { - state.useRequestQueue = val; - }); - } - /** * Setter for the `openSeaEnabled` property * @@ -865,15 +848,6 @@ export class PreferencesController extends BaseController< return selectedAccount.address; } - /** - * Getter for the `useRequestQueue` property - * - * @returns whether this option is on or off. - */ - getUseRequestQueue(): boolean { - return this.state.useRequestQueue; - } - /** * Sets a custom label for an account * diff --git a/app/scripts/fixtures/with-preferences.js b/app/scripts/fixtures/with-preferences.js index c3a482ef8f94..8d1845f728bb 100644 --- a/app/scripts/fixtures/with-preferences.js +++ b/app/scripts/fixtures/with-preferences.js @@ -31,7 +31,6 @@ export const FIXTURES_PREFERENCES = { useTokenDetection: true, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - useRequestQueue: true, theme: 'light', useExternalNameSources: true, useTransactionSimulations: true, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 826aa04018d9..242b9174add4 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -175,7 +175,6 @@ const jsonData = JSON.stringify({ theme: 'light', customNetworkListEnabled: false, textDirection: 'auto', - useRequestQueue: true, }, internalAccounts: { accounts: { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b869cd704633..38ec6023b264 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1334,13 +1334,13 @@ export default class MetamaskController extends EventEmitter { ], }), state: initState.SelectedNetworkController, - useRequestQueuePreference: - this.preferencesController.state.useRequestQueue, - onPreferencesStateChange: (listener) => { - preferencesMessenger.subscribe( - 'PreferencesController:stateChange', - listener, - ); + useRequestQueuePreference: true, + onPreferencesStateChange: () => { + // noop + // we have removed the ability to toggle the useRequestQueue preference + // both useRequestQueue and onPreferencesStateChange will be removed + // once mobile supports per dapp network selection + // see https://github.com/MetaMask/core/pull/5065#issue-2736965186 }, domainProxyMap: new WeakRefObjectMap(), }); @@ -3360,9 +3360,7 @@ export default class MetamaskController extends EventEmitter { * @returns {Promise<{ isUnlocked: boolean, networkVersion: string, chainId: string, accounts: string[] }>} An object with relevant state properties. */ async getProviderState(origin) { - const providerNetworkState = await this.getProviderNetworkState( - this.preferencesController.getUseRequestQueue() ? origin : undefined, - ); + const providerNetworkState = await this.getProviderNetworkState(origin); return { isUnlocked: this.isUnlocked(), @@ -3517,9 +3515,6 @@ export default class MetamaskController extends EventEmitter { setOpenSeaEnabled: preferencesController.setOpenSeaEnabled.bind( preferencesController, ), - getUseRequestQueue: this.preferencesController.getUseRequestQueue.bind( - this.preferencesController, - ), getProviderConfig: () => getProviderConfig({ metamask: this.networkController.state, @@ -3569,7 +3564,6 @@ export default class MetamaskController extends EventEmitter { preferencesController.setUseTransactionSimulations.bind( preferencesController, ), - setUseRequestQueue: this.setUseRequestQueue.bind(this), setIpfsGateway: preferencesController.setIpfsGateway.bind( preferencesController, ), @@ -5473,14 +5467,6 @@ export default class MetamaskController extends EventEmitter { this.sendUpdate(); } - //============================================================================= - // REQUEST QUEUE - //============================================================================= - - setUseRequestQueue(value) { - this.preferencesController.setUseRequestQueue(value); - } - //============================================================================= // SETUP //============================================================================= @@ -5973,12 +5959,12 @@ export default class MetamaskController extends EventEmitter { enqueueRequest: this.queuedRequestController.enqueueRequest.bind( this.queuedRequestController, ), - useRequestQueue: this.preferencesController.getUseRequestQueue.bind( - this.preferencesController, - ), shouldEnqueueRequest: (request) => { return methodsThatShouldBeEnqueued.includes(request.method); }, + // This will be removed once we can actually remove useRequestQueue state + // i.e. unrevert https://github.com/MetaMask/core/pull/5065 + useRequestQueue: () => true, }); engine.push(requestQueueMiddleware); @@ -7210,31 +7196,17 @@ export default class MetamaskController extends EventEmitter { } async _notifyChainChange() { - if (this.preferencesController.getUseRequestQueue()) { - this.notifyAllConnections(async (origin) => ({ - method: NOTIFICATION_NAMES.chainChanged, - params: await this.getProviderNetworkState(origin), - })); - } else { - this.notifyAllConnections({ - method: NOTIFICATION_NAMES.chainChanged, - params: await this.getProviderNetworkState(), - }); - } + this.notifyAllConnections(async (origin) => ({ + method: NOTIFICATION_NAMES.chainChanged, + params: await this.getProviderNetworkState(origin), + })); } async _notifyChainChangeForConnection(connection, origin) { - if (this.preferencesController.getUseRequestQueue()) { - this.notifyConnection(connection, { - method: NOTIFICATION_NAMES.chainChanged, - params: await this.getProviderNetworkState(origin), - }); - } else { - this.notifyConnection(connection, { - method: NOTIFICATION_NAMES.chainChanged, - params: await this.getProviderNetworkState(), - }); - } + this.notifyConnection(connection, { + method: NOTIFICATION_NAMES.chainChanged, + params: await this.getProviderNetworkState(origin), + }); } async _onFinishedTransaction(transactionMeta) { diff --git a/app/scripts/migrations/136.test.ts b/app/scripts/migrations/136.test.ts new file mode 100644 index 000000000000..01fc9d49d69f --- /dev/null +++ b/app/scripts/migrations/136.test.ts @@ -0,0 +1,56 @@ +import { migrate, version } from './136'; + +const oldVersion = 135; + +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('removes the useRequestQueue preference', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + useRequestQueue: true, + otherPreference: true, + }, + }, + }; + const expectedData = { + PreferencesController: { + otherPreference: true, + }, + }; + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + + it('does nothing to other PreferencesController state if there is a useRequestQueue preference', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + existingPreference: true, + }, + }, + }; + + const expectedData = { + PreferencesController: { + existingPreference: true, + }, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual(expectedData); + }); + }); +}); diff --git a/app/scripts/migrations/136.ts b/app/scripts/migrations/136.ts new file mode 100644 index 000000000000..3f6bfc661292 --- /dev/null +++ b/app/scripts/migrations/136.ts @@ -0,0 +1,37 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; +export const version = 136; +/** + * This migration removes the useRequestQueue preference from 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 { + 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) && + hasProperty(state.PreferencesController, 'useRequestQueue') + ) { + delete state.PreferencesController.useRequestQueue; + } + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 35b72816b388..797b06d85cb5 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -159,6 +159,7 @@ const migrations = [ require('./133.2'), require('./134'), require('./135'), + require('./136'), ]; export default migrations; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 5bb2a24d0c5c..51338fd3eea8 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -244,7 +244,6 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - useRequestQueue: true, isMultiAccountBalancesEnabled: true, showIncomingTransactions: { [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 020c4db1c64b..6acf95bef637 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -95,7 +95,6 @@ function onboardingFixture() { useTokenDetection: false, useCurrencyRateCheck: true, useMultiAccountBalanceChecker: true, - useRequestQueue: true, isMultiAccountBalancesEnabled: true, showIncomingTransactions: { [ETHERSCAN_SUPPORTED_CHAIN_IDS.MAINNET]: true, @@ -845,15 +844,7 @@ class FixtureBuilder { [DAPP_ONE_URL]: '76e9cd59-d8e2-47e7-b369-9c205ccb602c', }, }), - this.withPreferencesControllerUseRequestQueueEnabled(), - ); - } - - withPreferencesControllerUseRequestQueueEnabled() { - return merge( - this.withPreferencesController({ - useRequestQueue: true, - }), + this, ); } diff --git a/test/e2e/json-rpc/switchEthereumChain.spec.js b/test/e2e/json-rpc/switchEthereumChain.spec.js index e55dfc622865..8395a3ef9603 100644 --- a/test/e2e/json-rpc/switchEthereumChain.spec.js +++ b/test/e2e/json-rpc/switchEthereumChain.spec.js @@ -34,30 +34,6 @@ describe('Switch Ethereum Chain for two dapps', function () { await tempToggleSettingRedesignedTransactionConfirmations(driver); - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const experimentalTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(experimentalTabRawLocator); - - // Toggle off request queue setting (on by default now) - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - // open two dapps const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); @@ -184,31 +160,6 @@ describe('Switch Ethereum Chain for two dapps', function () { }, async ({ driver }) => { await unlockWallet(driver); - - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const experimentalTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(experimentalTabRawLocator); - - // Toggle off request queue setting (on by default now) - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - // open two dapps const dappOne = await openDapp(driver, undefined, DAPP_URL); const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); @@ -274,30 +225,6 @@ describe('Switch Ethereum Chain for two dapps', function () { async ({ driver }) => { await unlockWallet(driver); - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const experimentalTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(experimentalTabRawLocator); - - // Toggle off request queue setting (on by default now) - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - // open two dapps await openDapp(driver, undefined, DAPP_URL); await openDapp(driver, undefined, DAPP_ONE_URL); @@ -410,30 +337,6 @@ describe('Switch Ethereum Chain for two dapps', function () { async ({ driver }) => { await unlockWallet(driver); - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const experimentalTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(experimentalTabRawLocator); - - // Toggle off request queue setting (on by default now) - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - // open two dapps const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); @@ -544,30 +447,6 @@ describe('Switch Ethereum Chain for two dapps', function () { async ({ driver }) => { await unlockWallet(driver); - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const experimentalTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(experimentalTabRawLocator); - - // Toggle off request queue setting (on by default now) - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - // open two dapps const dappTwo = await openDapp(driver, undefined, DAPP_ONE_URL); const dappOne = await openDapp(driver, undefined, DAPP_URL); diff --git a/test/e2e/page-objects/pages/settings/experimental-settings.ts b/test/e2e/page-objects/pages/settings/experimental-settings.ts index 69df0525093d..f3755db72947 100644 --- a/test/e2e/page-objects/pages/settings/experimental-settings.ts +++ b/test/e2e/page-objects/pages/settings/experimental-settings.ts @@ -19,9 +19,6 @@ class ExperimentalSettings { private readonly redesignedSignatureToggle = '[data-testid="toggle-redesigned-confirmations-container"]'; - private readonly requestQueueToggle = - '[data-testid="experimental-setting-toggle-request-queue"] label'; - private readonly watchAccountToggleState = '[data-testid="watch-account-toggle"]'; @@ -73,11 +70,6 @@ class ExperimentalSettings { await this.driver.clickElement(this.redesignedSignatureToggle); } - async toggleRequestQueue(): Promise { - console.log('Toggle Request Queue on experimental setting page'); - await this.driver.clickElement(this.requestQueueToggle); - } - async toggleWatchAccount(): Promise { console.log('Toggle Watch Account on experimental setting page'); await this.driver.clickElement(this.watchAccountToggle); diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 7a687ec254c0..6f08305db357 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -40,7 +40,6 @@ }, "theme": "light", "useBlockie": false, - "useRequestQueue": true, "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, diff --git a/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts b/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts index 8ecd7e908a30..97b6beb81096 100644 --- a/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts +++ b/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts @@ -41,7 +41,7 @@ describe('Queued Confirmations', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -86,7 +86,7 @@ describe('Queued Confirmations', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -141,7 +141,6 @@ describe('Queued Confirmations', function () { .withPermissionControllerConnectedToTestDapp() .withPreferencesController({ preferences: { redesignedConfirmationsEnabled: true }, - useRequestQueue: true, }) .withSelectedNetworkControllerPerDomain() .build(), @@ -193,7 +192,7 @@ describe('Queued Confirmations', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() .withMetaMetricsController({ metaMetricsId: 'fake-metrics-id', @@ -281,7 +280,6 @@ describe('Queued Confirmations', function () { .withPermissionControllerConnectedToTestDapp() .withPreferencesController({ preferences: { redesignedConfirmationsEnabled: true }, - useRequestQueue: true, }) .withSelectedNetworkControllerPerDomain() .withMetaMetricsController({ diff --git a/test/e2e/tests/connections/review-switch-permission-page.spec.js b/test/e2e/tests/connections/review-switch-permission-page.spec.js index 5fe3d6d19526..121c3b413833 100644 --- a/test/e2e/tests/connections/review-switch-permission-page.spec.js +++ b/test/e2e/tests/connections/review-switch-permission-page.spec.js @@ -21,7 +21,7 @@ describe('Permissions Page when Dapp Switch to an enabled and non permissioned n dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { 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 c4d176348946..cab47452ea3f 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,7 +211,6 @@ "useNftDetection": false, "use4ByteResolution": true, "useCurrencyRateCheck": true, - "useRequestQueue": true, "openSeaEnabled": false, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "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 76cc18653595..e2b805831ea4 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 @@ -121,7 +121,6 @@ "useTokenDetection": true, "useNftDetection": false, "useCurrencyRateCheck": true, - "useRequestQueue": true, "openSeaEnabled": false, "securityAlertsEnabled": "boolean", "watchEthereumAccountEnabled": "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 c443b0fe84ec..c85bf92f4334 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 @@ -125,7 +125,6 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true, "isMultiAccountBalancesEnabled": "boolean", "showIncomingTransactions": "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 012ccabe6f2f..f86a5278e054 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 @@ -125,7 +125,6 @@ "useTokenDetection": false, "useCurrencyRateCheck": true, "useMultiAccountBalanceChecker": true, - "useRequestQueue": true, "isMultiAccountBalancesEnabled": "boolean", "showIncomingTransactions": "object" }, diff --git a/test/e2e/tests/multichain/all-permissions-page.spec.ts b/test/e2e/tests/multichain/all-permissions-page.spec.ts index eca4052fdbf7..4c481520ce81 100644 --- a/test/e2e/tests/multichain/all-permissions-page.spec.ts +++ b/test/e2e/tests/multichain/all-permissions-page.spec.ts @@ -62,7 +62,6 @@ describe('Permissions Page', function () { const experimentalSettings = new ExperimentalSettings(driver); await experimentalSettings.check_pageIsLoaded(); - await experimentalSettings.toggleRequestQueue(); await settingsPage.closeSettingsPage(); // go to homepage and check site permissions 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 958c1351d8b3..e23db3855609 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 @@ -22,7 +22,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { @@ -145,7 +144,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { 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 066acacab23a..898fe4cf3d90 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 @@ -22,7 +22,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { @@ -185,7 +184,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks', fun dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { 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 d3241c95c9d5..c85631d32800 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 @@ -23,7 +23,6 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 3 }, ganacheOptions: { @@ -180,7 +179,6 @@ describe('Request Queuing for Multiple Dapps and Txs on same networks', function dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 3 }, ganacheOptions: { diff --git a/test/e2e/tests/request-queuing/chainid-check.spec.js b/test/e2e/tests/request-queuing/chainid-check.spec.js index 1579a8ae5aa4..95ed3ca6bd8c 100644 --- a/test/e2e/tests/request-queuing/chainid-check.spec.js +++ b/test/e2e/tests/request-queuing/chainid-check.spec.js @@ -13,304 +13,145 @@ const { const { PAGES } = require('../../webdriver/driver'); describe('Request Queueing chainId proxy sync', function () { - describe('request queue is on', function () { - it('should preserve per dapp network selections after connecting and switching without refresh calls @no-mmi', 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(), + it('should preserve per dapp network selections after connecting and switching without refresh calls @no-mmi', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerDoubleGanache() + + .withSelectedNetworkControllerPerDomain() + .build(), + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + ], }, - async ({ driver }) => { - await unlockWallet(driver); + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); - // Navigate to extension home screen - await driver.navigate(PAGES.HOME); + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); - // Open Dapp One - await openDapp(driver, undefined, DAPP_URL); + // Open Dapp One + await openDapp(driver, undefined, DAPP_URL); - await driver.delay(regularDelayMs); + await driver.delay(regularDelayMs); - const chainIdRequest = JSON.stringify({ - method: 'eth_chainId', - }); + 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 chainIdBeforeConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - const chainIdBeforeConnectAfterManualSwitch = - await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + assert.equal(chainIdBeforeConnect, '0x539'); // 1337 - // before connecting the chainId should change with the wallet - assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); - // 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})`, - ); + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); - // should still be on the same chainId as the wallet after connecting - assert.equal(chainIdAfterConnect, '0x1'); + // Switch to second network + await driver.clickElement({ + text: 'Ethereum Mainnet', + css: 'p', + }); - const switchEthereumChainRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x539' }], - }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + const chainIdBeforeConnectAfterManualSwitch = 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})`, - ); - // after connecting the dapp should now have its own selected network - // independent from the globally selected and therefore should not have changed when - // the globally selected network was manually changed via the wallet UI - assert.equal(chainIdAfterManualSwitch, '0x539'); // 1337 - }, - ); - }); - }); - - describe('request queue is off', function () { - it('should always follow the globally selected network after connecting and switching without refresh calls @no-mmi', async function () { - const port = 8546; - const chainId = 1338; - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() - .build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [ - { - port, - chainId, - ganacheOptions2: defaultGanacheOptions, - }, - ], - }, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'Settings', tag: 'div' }); - await driver.clickElement({ text: 'Experimental', tag: 'div' }); - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - - // 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"]'); + // before connecting the chainId should change with the wallet + assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); - // Switch to second network - await driver.clickElement({ - text: 'Ethereum Mainnet', - css: 'p', - }); + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.delay(regularDelayMs); - const chainIdBeforeConnectAfterManualSwitch = - await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + await switchToNotificationWindow(driver); - // before connecting the chainId should change with the wallet - assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); - // Connect to dapp - await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const chainIdAfterConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Connect', - tag: 'button', - }); + // should still be on the same chainId as the wallet after connecting + assert.equal(chainIdAfterConnect, '0x1'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - const chainIdAfterConnect = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); - // should still be on the same chainId as the wallet after connecting - assert.equal(chainIdAfterConnect, '0x1'); - await driver.waitForSelector({ - css: '[id="chainId"]', - text: '0x1', - }); + await switchToNotificationWindow(driver); + await driver.findClickableElements({ + text: 'Confirm', + tag: 'button', + }); - const switchEthereumChainRequest = JSON.stringify({ - jsonrpc: '2.0', - method: 'wallet_switchEthereumChain', - params: [{ chainId: '0x539' }], - }); + await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.executeScript( - `window.ethereum.request(${switchEthereumChainRequest})`, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + const chainIdAfterDappSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - await driver.clickElementAndWaitForWindowToClose({ - text: 'Confirm', - tag: 'button', - }); + // should be on the new chainId that was requested + assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); - const chainIdAfterDappSwitch = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); - // should be on the new chainId that was requested - assert.equal(chainIdAfterDappSwitch, '0x539'); // 1337 + // Switch network + await driver.clickElement({ + text: 'Localhost 8546', + css: 'p', + }); - await driver.waitForSelector({ - css: '[id="chainId"]', - text: '0x539', - }); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - // TODO: check that the wallet network has changed too - - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); - - // Switch network - await driver.clickElement({ - text: 'Ethereum Mainnet', - css: 'p', - }); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - const chainIdAfterManualSwitch = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); - // after connecting the dapp should still be following the - // globally selected network and therefore should have changed when - // the globally selected network was manually changed via the wallet UI - assert.equal(chainIdAfterManualSwitch, '0x1'); - }, - ); - }); + const chainIdAfterManualSwitch = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + // after connecting the dapp should now have its own selected network + // independent from the globally selected and therefore should not have changed when + // the globally selected network was manually changed via the wallet UI + assert.equal(chainIdAfterManualSwitch, '0x539'); // 1337 + }, + ); }); }); 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 28b19efbeb04..9d7fb364f21c 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 @@ -24,7 +24,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -173,7 +172,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, diff --git a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts index ec607fc34936..6b87a7142fbe 100644 --- a/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts +++ b/test/e2e/tests/request-queuing/dapp1-subscribe-network-switch.spec.ts @@ -18,7 +18,6 @@ describe('Request Queueing', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, 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 67efbfe6fee9..bd61fe8d00b9 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 @@ -22,7 +22,6 @@ describe('Request Queuing Dapp 1 Send Tx -> Dapp 2 Request Accounts Tx', functio dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withPermissionControllerConnectedToTestDapp() .build(), dappOptions: { numberOfDapps: 2 }, @@ -116,7 +115,6 @@ describe('Request Queuing Dapp 1 Send Tx -> Dapp 2 Request Accounts Tx', functio dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withPermissionControllerConnectedToTestDapp() .build(), dappOptions: { numberOfDapps: 2 }, @@ -207,7 +205,6 @@ describe('Request Queuing Dapp 1 Send Tx -> Dapp 2 Request Accounts Tx', functio dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withPermissionControllerConnectedToTwoTestDapps() .build(), dappOptions: { numberOfDapps: 2 }, 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 24c09ee18d09..e2b5bb0ca9b1 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 @@ -20,7 +20,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -177,7 +176,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -328,7 +326,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -483,7 +480,6 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, diff --git a/test/e2e/tests/request-queuing/enable-queuing.spec.js b/test/e2e/tests/request-queuing/enable-queuing.spec.js deleted file mode 100644 index ccc5f7cec2c7..000000000000 --- a/test/e2e/tests/request-queuing/enable-queuing.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -const { - withFixtures, - defaultGanacheOptions, - unlockWallet, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); - -describe('Toggle Request Queuing Setting', function () { - it('enables the request queuing experimental setting', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - failOnConsoleError: false, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // Open account menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = - '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Experimental tab - const securityAndPrivacyTabRawLocator = { - text: 'Experimental', - tag: 'div', - }; - await driver.clickElement(securityAndPrivacyTabRawLocator); - - // Toggle request queue setting - await driver.clickElement( - '[data-testid="experimental-setting-toggle-request-queue"]', - ); - }, - ); - }); -}); 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 3a413f147e06..66e1b8963731 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 @@ -20,7 +20,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { @@ -141,7 +140,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks revok dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), dappOptions: { numberOfDapps: 2 }, ganacheOptions: { 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 f831ff1ff38d..fec528a84c60 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 @@ -21,7 +21,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, @@ -159,7 +158,6 @@ describe('Request Queuing for Multiple Dapps and Txs on different networks.', fu dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), dappOptions: { numberOfDapps: 2 }, diff --git a/test/e2e/tests/request-queuing/sendTx-revokePermissions.spec.ts b/test/e2e/tests/request-queuing/sendTx-revokePermissions.spec.ts index 9f5454404806..620735761c69 100644 --- a/test/e2e/tests/request-queuing/sendTx-revokePermissions.spec.ts +++ b/test/e2e/tests/request-queuing/sendTx-revokePermissions.spec.ts @@ -21,7 +21,6 @@ describe('Request Queuing', function () { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() - .withPreferencesControllerUseRequestQueueEnabled() .withSelectedNetworkControllerPerDomain() .build(), ganacheOptions: { diff --git a/test/e2e/tests/request-queuing/sendTx-switchChain-sendTx.spec.js b/test/e2e/tests/request-queuing/sendTx-switchChain-sendTx.spec.js index 5b0c4dca2f07..13d34ecbbb66 100644 --- a/test/e2e/tests/request-queuing/sendTx-switchChain-sendTx.spec.js +++ b/test/e2e/tests/request-queuing/sendTx-switchChain-sendTx.spec.js @@ -21,7 +21,7 @@ describe('Request Queuing Send Tx -> SwitchChain -> SendTx', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPermissionControllerConnectedToTestDapp() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, diff --git a/test/e2e/tests/request-queuing/switch-network.spec.js b/test/e2e/tests/request-queuing/switch-network.spec.js index 913bdf459a26..d0a72be430ca 100644 --- a/test/e2e/tests/request-queuing/switch-network.spec.js +++ b/test/e2e/tests/request-queuing/switch-network.spec.js @@ -22,7 +22,6 @@ describe('Request Queuing Switch Network on Dapp Send Tx while on different netw fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPermissionControllerConnectedToTestDapp() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -109,7 +108,6 @@ describe('Request Queuing Switch Network on Dapp Send Tx while on different netw fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPermissionControllerConnectedToTestDapp() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, diff --git a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js index df33600413e1..dd34215fe007 100644 --- a/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-sendTx.spec.js @@ -16,7 +16,7 @@ describe('Request Queuing SwitchChain -> SendTx', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, diff --git a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js index 5767bd26def5..080d9ef38903 100644 --- a/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/switchChain-watchAsset.spec.js @@ -20,7 +20,7 @@ describe('Request Queue SwitchChain -> WatchAsset', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, diff --git a/test/e2e/tests/request-queuing/ui.spec.js b/test/e2e/tests/request-queuing/ui.spec.js index 707c252396b7..dd8fa26122bb 100644 --- a/test/e2e/tests/request-queuing/ui.spec.js +++ b/test/e2e/tests/request-queuing/ui.spec.js @@ -239,7 +239,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -303,7 +302,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -429,7 +427,6 @@ describe('Request-queue UI changes', function () { .withPreferencesController({ preferences: { showTestNetworks: true }, }) - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -505,7 +502,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -578,7 +574,7 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -650,7 +646,7 @@ describe('Request-queue UI changes', function () { driverOptions: { timeOut: 30000 }, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -718,7 +714,6 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -780,7 +775,7 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerTripleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -904,7 +899,7 @@ describe('Request-queue UI changes', function () { .withPreferencesController({ preferences: { showTestNetworks: true }, }) - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -973,9 +968,7 @@ describe('Request-queue UI changes', function () { await withFixtures( { dapp: true, - fixtures: new FixtureBuilder() - .withPreferencesControllerUseRequestQueueEnabled() - .build(), + fixtures: new FixtureBuilder().build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), driverOptions: { constrainWindowSize: true }, @@ -1019,7 +1012,7 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -1071,7 +1064,7 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -1140,7 +1133,7 @@ describe('Request-queue UI changes', function () { dapp: true, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, @@ -1206,7 +1199,7 @@ describe('Request-queue UI changes', function () { driverOptions: { timeOut: 30000 }, fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, 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 64ac781a20e0..c60d1d0a9278 100644 --- a/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js +++ b/test/e2e/tests/request-queuing/watchAsset-switchChain-watchAsset.spec.js @@ -22,7 +22,7 @@ describe('Request Queue WatchAsset -> SwitchChain -> WatchAsset', function () { fixtures: new FixtureBuilder() .withNetworkControllerDoubleGanache() .withPermissionControllerConnectedToTestDapp() - .withPreferencesControllerUseRequestQueueEnabled() + .build(), ganacheOptions: { ...defaultGanacheOptions, diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 3d3580dc12ca..cbf065ea2aba 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -2049,7 +2049,6 @@ "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, - "useRequestQueue": false, "useSafeChainsListValidation": true, "useTokenDetection": false, "useTransactionSimulations": true, diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index 9d3df88dbeae..479c9041b3a2 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -462,7 +462,6 @@ "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, - "useRequestQueue": true, "useSafeChainsListValidation": true, "useTokenDetection": true, "useTransactionSimulations": true, diff --git a/ui/components/app/assets/nfts/nfts-items/nfts-items.stories.tsx b/ui/components/app/assets/nfts/nfts-items/nfts-items.stories.tsx index f13af8c13ea4..7a7f4c8ccaa9 100644 --- a/ui/components/app/assets/nfts/nfts-items/nfts-items.stories.tsx +++ b/ui/components/app/assets/nfts/nfts-items/nfts-items.stories.tsx @@ -77,7 +77,6 @@ const createMockState = () => ({ nftContracts: [], nfts: [], ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), - useRequestQueue: true, }, appState: { isLoading: false, 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 ec4539aa55da..90ed2fca6809 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 @@ -143,7 +143,6 @@ const render = ({ [CHAIN_IDS.LINEA_MAINNET]: true, }, }, - useRequestQueue: true, domains: { ...(selectedTabOriginInDomainsState ? { [origin]: selectedNetworkClientId } 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 481ddfe5142f..491ae289b19c 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -46,7 +46,6 @@ import { getOnboardedInThisUISession, getShowNetworkBanner, getOriginOfCurrentTab, - getUseRequestQueue, getEditedNetwork, getOrderedNetworksList, getIsAddingNewNetwork, @@ -120,7 +119,6 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const showTestNetworks = useSelector(getShowTestNetworks); const currentChainId = useSelector(getCurrentChainId); const selectedTabOrigin = useSelector(getOriginOfCurrentTab); - const useRequestQueue = useSelector(getUseRequestQueue); const isUnlocked = useSelector(getIsUnlocked); const domains = useSelector(getAllDomains); const orderedNetworksList = useSelector(getOrderedNetworksList); @@ -309,11 +307,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { // If presently on a dapp, communicate a change to // the dapp via silent switchEthereumChain that the // network has changed due to user action - if ( - useRequestQueue && - selectedTabOrigin && - domains[selectedTabOrigin] - ) { + if (selectedTabOrigin && domains[selectedTabOrigin]) { setNetworkClientIdForDomain(selectedTabOrigin, networkClientId); } diff --git a/ui/index.js b/ui/index.js index a63b2acc86fa..54d952eb85f4 100644 --- a/ui/index.js +++ b/ui/index.js @@ -29,7 +29,6 @@ import { getUnapprovedTransactions, getNetworkToAutomaticallySwitchTo, getSwitchedNetworkDetails, - getUseRequestQueue, } from './selectors'; import { ALERT_STATE } from './ducks/alerts'; import { @@ -243,10 +242,7 @@ async function runInitialActions(store) { // Register this window as the current popup // and set in background state - if ( - getUseRequestQueue(state) && - getEnvironmentType() === ENVIRONMENT_TYPE_POPUP - ) { + if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) { const thisPopupId = Date.now(); global.metamask.id = thisPopupId; await store.dispatch(actions.setCurrentExtensionPopupId(thisPopupId)); diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index 6212ed4258f3..74f15a5f99c6 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -193,7 +193,6 @@ export default class Routes extends Component { automaticallySwitchNetwork: PropTypes.func.isRequired, totalUnapprovedConfirmationCount: PropTypes.number.isRequired, currentExtensionPopupId: PropTypes.number, - useRequestQueue: PropTypes.bool, clearEditedNetwork: PropTypes.func.isRequired, oldestPendingApproval: PropTypes.object.isRequired, pendingApprovals: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -217,7 +216,6 @@ export default class Routes extends Component { activeTabOrigin, totalUnapprovedConfirmationCount, isUnlocked, - useRequestQueue, currentExtensionPopupId, } = this.props; if (theme !== prevProps.theme) { @@ -243,7 +241,6 @@ export default class Routes extends Component { // Terminate the popup when another popup is opened // if the user is using RPC queueing if ( - useRequestQueue && currentExtensionPopupId !== undefined && global.metamask.id !== undefined && currentExtensionPopupId !== global.metamask.id diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index 0342fc2e15d1..feb1be09408e 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -21,7 +21,6 @@ import { getSwitchedNetworkDetails, getNetworkToAutomaticallySwitchTo, getNumberOfAllUnapprovedTransactionsAndMessages, - getUseRequestQueue, getCurrentNetwork, getSelectedInternalAccount, oldestPendingConfirmationSelector, @@ -118,7 +117,6 @@ function mapStateToProps(state) { switchedNetworkNeverShowMessage: selectSwitchedNetworkNeverShowMessage(state), currentExtensionPopupId: state.metamask.currentExtensionPopupId, - useRequestQueue: getUseRequestQueue(state), oldestPendingApproval, pendingApprovals, transactionsMetadata, diff --git a/ui/pages/settings/experimental-tab/experimental-tab.component.tsx b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx index 5bea76d80dbe..dd985eb469e7 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.component.tsx +++ b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx @@ -48,8 +48,6 @@ type ExperimentalTabProps = { addSnapAccountEnabled: boolean; setAddSnapAccountEnabled: (value: boolean) => void; ///: END:ONLY_INCLUDE_IF - useRequestQueue: boolean; - setUseRequestQueue: (value: boolean) => void; petnamesEnabled: boolean; setPetnamesEnabled: (value: boolean) => void; featureNotificationsEnabled: boolean; @@ -237,21 +235,6 @@ export default class ExperimentalTab extends PureComponent } ///: END:ONLY_INCLUDE_IF - renderToggleRequestQueue() { - const { t } = this.context; - const { useRequestQueue, setUseRequestQueue } = this.props; - return this.renderToggleSection({ - title: t('toggleRequestQueueField'), - description: t('toggleRequestQueueDescription'), - toggleValue: useRequestQueue || false, - toggleCallback: (value) => setUseRequestQueue(!value), - toggleContainerDataTestId: 'experimental-setting-toggle-request-queue', - toggleDataTestId: 'experimental-setting-toggle-request-queue', - toggleOffLabel: t('toggleRequestQueueOff'), - toggleOnLabel: t('toggleRequestQueueOn'), - }); - } - renderNotificationsToggle() { const { t } = this.context; const { featureNotificationsEnabled, setFeatureNotificationsEnabled } = @@ -423,7 +406,6 @@ export default class ExperimentalTab extends PureComponent {this.renderToggleRedesignedSignatures()} {this.renderToggleRedesignedTransactions()} {process.env.NOTIFICATIONS ? this.renderNotificationsToggle() : null} - {this.renderToggleRequestQueue()} {/* Section: Account Management Snaps */} { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/ui/pages/settings/experimental-tab/experimental-tab.container.ts b/ui/pages/settings/experimental-tab/experimental-tab.container.ts index 6de36b9de5c0..e656b035dbbf 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.container.ts +++ b/ui/pages/settings/experimental-tab/experimental-tab.container.ts @@ -7,7 +7,6 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setAddSnapAccountEnabled, ///: END:ONLY_INCLUDE_IF - setUseRequestQueue, setPetnamesEnabled, setFeatureNotificationsEnabled, setRedesignedConfirmationsEnabled, @@ -26,7 +25,6 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) getIsAddSnapAccountEnabled, ///: END:ONLY_INCLUDE_IF - getUseRequestQueue, getPetnamesEnabled, getFeatureNotificationsEnabled, getRedesignedConfirmationsEnabled, @@ -52,7 +50,6 @@ const mapStateToProps = (state: MetaMaskReduxState) => { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) addSnapAccountEnabled: getIsAddSnapAccountEnabled(state), ///: END:ONLY_INCLUDE_IF - useRequestQueue: getUseRequestQueue(state), petnamesEnabled, featureNotificationsEnabled, redesignedConfirmationsEnabled: getRedesignedConfirmationsEnabled(state), @@ -75,7 +72,6 @@ const mapDispatchToProps = (dispatch: MetaMaskReduxDispatch) => { setAddSnapAccountEnabled: (value: boolean) => setAddSnapAccountEnabled(value), ///: END:ONLY_INCLUDE_IF - setUseRequestQueue: (value: boolean) => setUseRequestQueue(value), setPetnamesEnabled: (value: boolean) => { return dispatch(setPetnamesEnabled(value)); }, diff --git a/ui/pages/settings/experimental-tab/experimental-tab.test.js b/ui/pages/settings/experimental-tab/experimental-tab.test.js index 08d02bf94215..cda6b95c0a5d 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.test.js +++ b/ui/pages/settings/experimental-tab/experimental-tab.test.js @@ -30,7 +30,7 @@ describe('ExperimentalTab', () => { const { getAllByRole } = render(); const toggle = getAllByRole('checkbox'); - expect(toggle).toHaveLength(9); + expect(toggle).toHaveLength(8); }); it('enables add account snap', async () => { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index ce117af76262..43671bc77a6b 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2200,11 +2200,9 @@ export function getNetworkToAutomaticallySwitchTo(state) { // This allows the user to be connected on one chain // for one dapp, and automatically change for another const selectedTabOrigin = getOriginOfCurrentTab(state); - const useRequestQueue = getUseRequestQueue(state); if ( getEnvironmentType() === ENVIRONMENT_TYPE_POPUP && getIsUnlocked(state) && - useRequestQueue && selectedTabOrigin && numberOfUnapprovedTx === 0 ) { @@ -2699,16 +2697,6 @@ export function getIstokenDetectionInactiveOnNonMainnetSupportedNetwork(state) { return isDynamicTokenListAvailable && !useTokenDetection && !isMainnet; } -/** - * To get the `useRequestQueue` value which determines whether we use a request queue infront of provider api calls. This will have the effect of implementing per-dapp network switching. - * - * @param {*} state - * @returns Boolean - */ -export function getUseRequestQueue(state) { - return state.metamask.useRequestQueue; -} - /** * To get the `getIsSecurityAlertsEnabled` value which determines whether security check is enabled * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index fce129745561..b4afbcadcf38 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -305,7 +305,6 @@ describe('Selectors', () => { }, metamask: { isUnlocked: true, - useRequestQueue: true, selectedTabOrigin: SELECTED_ORIGIN, unapprovedDecryptMsgs: [], unapprovedPersonalMsgs: [], diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 75de201c76b7..b6dc0bc0a7c2 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -5248,14 +5248,6 @@ export async function getSnapAccountsById(snapId: string): Promise { } ///: END:ONLY_INCLUDE_IF -export function setUseRequestQueue(val: boolean): void { - try { - submitRequestToBackground('setUseRequestQueue', [val]); - } catch (error) { - logErrorWithMessage(error); - } -} - export function setUseExternalNameSources(val: boolean): void { try { submitRequestToBackground('setUseExternalNameSources', [val]); From 2d443ffef31e826afa7bed96766fc9f084655365 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang <7315988+dawnseeker8@users.noreply.github.com> Date: Fri, 10 Jan 2025 00:41:13 +0800 Subject: [PATCH 61/71] feat: Enable Ledger clear signing feature (#28909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR enable the clear signing feature in metamask mobile. Please refer to this feature requests for detail: https://github.com/MetaMask/accounts-planning/issues/544 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28909?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/544 ## **Manual testing steps** - Test the clear signing using this dapp provided by ledger team: https://clear-signing-tester.vercel.app/ - EIP712 sign message (Polygon mainnet or Ethereum mainnet). - Sign transaction (Linea testnet). - Swap in a layer 2 chain like Linea or Polygon. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Sébastien Van Eyck Co-authored-by: MetaMask Bot --- .../offscreen-bridge/ledger-offscreen-bridge.ts | 14 ++++++++------ offscreen/scripts/ledger.ts | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/scripts/lib/offscreen-bridge/ledger-offscreen-bridge.ts b/app/scripts/lib/offscreen-bridge/ledger-offscreen-bridge.ts index 608d1ee8156b..1ff85111f54a 100644 --- a/app/scripts/lib/offscreen-bridge/ledger-offscreen-bridge.ts +++ b/app/scripts/lib/offscreen-bridge/ledger-offscreen-bridge.ts @@ -1,4 +1,8 @@ -import { LedgerBridge } from '@metamask/eth-ledger-bridge-keyring'; +import { + LedgerBridge, + LedgerSignTypedDataParams, + LedgerSignTypedDataResponse, +} from '@metamask/eth-ledger-bridge-keyring'; import { LedgerAction, OffscreenCommunicationEvents, @@ -161,11 +165,9 @@ export class LedgerOffscreenBridge }); } - deviceSignTypedData(params: { - hdPath: string; - domainSeparatorHex: string; - hashStructMessageHex: string; - }) { + deviceSignTypedData( + params: LedgerSignTypedDataParams, + ): Promise { return new Promise<{ v: number; s: string; diff --git a/offscreen/scripts/ledger.ts b/offscreen/scripts/ledger.ts index c08fdc6ac85a..4cff2f3a6748 100644 --- a/offscreen/scripts/ledger.ts +++ b/offscreen/scripts/ledger.ts @@ -96,7 +96,7 @@ function setupMessageListeners(iframe: HTMLIFrameElement) { export default async function init() { return new Promise((resolve) => { const iframe = document.createElement('iframe'); - iframe.src = 'https://metamask.github.io/eth-ledger-bridge-keyring'; + iframe.src = 'https://metamask.github.io/ledger-iframe-bridge/8.0.0/'; iframe.allow = 'hid'; iframe.onload = () => { setupMessageListeners(iframe); From 87c524e4a34749cff8c94cc43c1cee8d83ff107c Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 9 Jan 2025 14:19:56 -0330 Subject: [PATCH 62/71] ci: Migrate metamaskbot PR comment (#29373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Migrate the `metamaskbot` PR comment from CircleCI to GitHub Actions. CircleCI is still used for artifact storage for linked artifacts. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29373?quickstart=1) ## **Related issues** Relates to #28572 These changes were extracted from #29256 ## **Manual testing steps** * Test that all links in the `metamaskbot` comment work correctly, except those that are already broken. Links already broken on `main` include: * Some build links (fixed in #29403 ): - Beta builds - Firefox test build, and Firefox flask-test build * MV3 performance stats reports (fixed in #29408 ): * mv3: Background Module Init Stats (link works, page is broken) * mv3: UI Init Stats (link works, page is broken) * mv3: Module Load Stats (link works, page is broken) * Coverage report (fixed by #29410) * mv3 bundle size stats (fixed by #29486) ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 8 --- .github/workflows/main.yml | 9 +++ .github/workflows/publish-prerelease.yml | 72 ++++++++++++++++++++ development/highlights/index.js | 2 +- development/metamaskbot-build-announce.js | 81 +++++++++++++---------- 5 files changed, 128 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/publish-prerelease.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 38ccd80d7059..5243d8ef4043 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1331,14 +1331,6 @@ jobs: - store_artifacts: path: development/ts-migration-dashboard/build/final destination: ts-migration-dashboard - - run: - name: Set branch parent commit env var - command: | - echo "export PARENT_COMMIT=$(git merge-base origin/HEAD HEAD)" >> $BASH_ENV - source $BASH_ENV - - run: - name: build:announce - command: ./development/metamaskbot-build-announce.js job-publish-release: executor: node-browsers-small diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e5a5121f336..25cb1e016868 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,6 +76,15 @@ jobs: name: Wait for CircleCI workflow status uses: ./.github/workflows/wait-for-circleci-workflow-status.yml + publish-prerelease: + name: Publish prerelease + if: ${{ github.event_name == 'pull_request' }} + needs: + - wait-for-circleci-workflow-status + uses: ./.github/workflows/publish-prerelease.yml + secrets: + PR_COMMENT_TOKEN: ${{ secrets.PR_COMMENT_TOKEN }} + all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml new file mode 100644 index 000000000000..9b4e460d546a --- /dev/null +++ b/.github/workflows/publish-prerelease.yml @@ -0,0 +1,72 @@ +name: Publish prerelease + +on: + workflow_call: + secrets: + PR_COMMENT_TOKEN: + required: true + +jobs: + publish-prerelease: + name: Publish prerelease + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # This is needed to get merge base to calculate bundle size diff + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: Get merge base commit hash + id: get-merge-base + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + merge_base="$(git merge-base "origin/${BASE_REF}" HEAD)" + echo "Merge base is '${merge_base}'" + echo "MERGE_BASE=${merge_base}" >> "$GITHUB_OUTPUT" + + - name: Get CircleCI job details + id: get-circleci-job-details + env: + REPOSITORY: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha }} + run: | + pipeline_id=$(curl --silent "https://circleci.com/api/v2/project/gh/$OWNER/$REPOSITORY/pipeline?branch=$BRANCH" | jq -r ".items | map(select(.vcs.revision == \"${HEAD_COMMIT_HASH}\" )) | first | .id") + workflow_id=$(curl --silent "https://circleci.com/api/v2/pipeline/$pipeline_id/workflow" | jq -r ".items[0].id") + job_details=$(curl --silent "https://circleci.com/api/v2/workflow/$workflow_id/job" | jq -r '.items[] | select(.name == "job-publish-prerelease")') + build_num=$(echo "$job_details" | jq -r '.job_number') + + echo 'CIRCLE_BUILD_NUM='"$build_num" >> "$GITHUB_OUTPUT" + job_id=$(echo "$job_details" | jq -r '.id') + echo 'CIRCLE_WORKFLOW_JOB_ID='"$job_id" >> "$GITHUB_OUTPUT" + + echo "Getting artifacts from pipeline '${pipeline_id}', workflow '${workflow_id}', build number '${build_num}', job ID '${job_id}'" + + - name: Get CircleCI job artifacts + env: + CIRCLE_WORKFLOW_JOB_ID: ${{ steps.get-circleci-job-details.outputs.CIRCLE_WORKFLOW_JOB_ID }} + run: | + mkdir -p "test-artifacts/chrome/benchmark" + curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/test-artifacts/chrome/benchmark/pageload.json" > "test-artifacts/chrome/benchmark/pageload.json" + + bundle_size=$(curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/test-artifacts/chrome/bundle_size.json") + mkdir -p "test-artifacts/chrome" + echo "${bundle_size}" > "test-artifacts/chrome/bundle_size.json" + + stories=$(curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/storybook/stories.json") + mkdir "storybook-build" + echo "${stories}" > "storybook-build/stories.json" + + - name: Publish prerelease + env: + PR_COMMENT_TOKEN: ${{ secrets.PR_COMMENT_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha }} + MERGE_BASE_COMMIT_HASH: ${{ steps.get-merge-base.outputs.MERGE_BASE }} + CIRCLE_BUILD_NUM: ${{ steps.get-circleci-job-details.outputs.CIRCLE_BUILD_NUM }} + CIRCLE_WORKFLOW_JOB_ID: ${{ steps.get-circleci-job-details.outputs.CIRCLE_WORKFLOW_JOB_ID }} + run: ./development/metamaskbot-build-announce.js diff --git a/development/highlights/index.js b/development/highlights/index.js index 2616d602633e..9efd3073d031 100644 --- a/development/highlights/index.js +++ b/development/highlights/index.js @@ -9,7 +9,7 @@ async function getHighlights({ artifactBase }) { // here we assume the PR base branch ("target") is `main` in lieu of doing // a query against the github api which requires an access token // see https://discuss.circleci.com/t/how-to-retrieve-a-pull-requests-base-branch-name-github/36911 - const changedFiles = await getChangedFiles({ target: 'main' }); + const changedFiles = await getChangedFiles({ target: 'origin/main' }); console.log(`detected changed files vs main:`); for (const filename of changedFiles) { console.log(` ${filename}`); diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index dc8c2ece4be6..e463da31aaec 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -5,7 +5,6 @@ const path = require('path'); // Fetch is part of node js in future versions, thus triggering no-shadow // eslint-disable-next-line no-shadow const fetch = require('node-fetch'); -const glob = require('fast-glob'); const VERSION = require('../package.json').version; const { getHighlights } = require('./highlights'); @@ -38,29 +37,41 @@ function getPercentageChange(from, to) { return parseFloat(((to - from) / Math.abs(from)) * 100).toFixed(2); } +/** + * Check whether an artifact exists, + * + * @param {string} url - The URL of the artifact to check. + * @returns True if the artifact exists, false if it doesn't + */ +async function artifactExists(url) { + // Using a regular GET request here rather than HEAD because for some reason CircleCI always + // returns 404 for HEAD requests. + const response = await fetch(url); + return response.ok; +} + async function start() { const { - GITHUB_COMMENT_TOKEN, - CIRCLE_PULL_REQUEST, - CIRCLE_SHA1, + PR_COMMENT_TOKEN, + PR_NUMBER, + HEAD_COMMIT_HASH, + MERGE_BASE_COMMIT_HASH, CIRCLE_BUILD_NUM, CIRCLE_WORKFLOW_JOB_ID, - PARENT_COMMIT, } = process.env; - console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST); - console.log('CIRCLE_SHA1', CIRCLE_SHA1); + console.log('PR_NUMBER', PR_NUMBER); + console.log('HEAD_COMMIT_HASH', HEAD_COMMIT_HASH); + console.log('MERGE_BASE_COMMIT_HASH', MERGE_BASE_COMMIT_HASH); console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM); console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID); - console.log('PARENT_COMMIT', PARENT_COMMIT); - if (!CIRCLE_PULL_REQUEST) { - console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`); + if (!PR_NUMBER) { + console.warn(`No pull request detected for commit "${HEAD_COMMIT_HASH}"`); return; } - const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop(); - const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7); + const SHORT_SHA1 = HEAD_COMMIT_HASH.slice(0, 7); const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`; // build the github comment content @@ -96,30 +107,30 @@ async function start() { // links to bundle browser builds const bundles = {}; - const fileType = '.html'; const sourceMapRoot = '/build-artifacts/source-map-explorer/'; - const bundleFiles = await glob(`.${sourceMapRoot}*${fileType}`); - - bundleFiles.forEach((bundleFile) => { - const fileName = bundleFile.split(sourceMapRoot)[1]; - const bundleName = fileName.split(fileType)[0]; - const url = `${BUILD_LINK_BASE}${sourceMapRoot}${fileName}`; - let fileRoot = bundleName; - let fileIndex = bundleName.match(/-[0-9]{1,}$/u)?.index; - - if (fileIndex) { - fileRoot = bundleName.slice(0, fileIndex); - fileIndex = bundleName.slice(fileIndex + 1, bundleName.length); - } - - const link = `${fileIndex || fileRoot}`; + const fileRoots = [ + 'background', + 'common', + 'ui', + 'content-script', + 'offscreen', + ]; - if (fileRoot in bundles) { + for (const fileRoot of fileRoots) { + bundles[fileRoot] = []; + let fileIndex = 0; + let url = `${BUILD_LINK_BASE}${sourceMapRoot}${fileRoot}-${fileIndex}.html`; + console.log(`Verifying ${url}`); + while (await artifactExists(url)) { + const link = `${fileIndex}`; bundles[fileRoot].push(link); - } else { - bundles[fileRoot] = [link]; + + fileIndex += 1; + url = `${BUILD_LINK_BASE}${sourceMapRoot}${fileRoot}-${fileIndex}.html`; + console.log(`Verifying ${url}`); } - }); + console.log(`Not found: ${url}`); + } const bundleMarkup = `
    ${Object.keys(bundles) .map((key) => `
  • ${key}: ${bundles[key].join(', ')}
  • `) @@ -295,7 +306,7 @@ async function start() { }; const devSizes = Object.keys(prSizes).reduce((sizes, part) => { - sizes[part] = devBundleSizeStats[PARENT_COMMIT][part] || 0; + sizes[part] = devBundleSizeStats[MERGE_BASE_COMMIT_HASH][part] || 0; return sizes; }, {}); @@ -346,7 +357,7 @@ async function start() { } const JSON_PAYLOAD = JSON.stringify({ body: commentBody }); - const POST_COMMENT_URI = `https://api.github.com/repos/metamask/metamask-extension/issues/${CIRCLE_PR_NUMBER}/comments`; + const POST_COMMENT_URI = `https://api.github.com/repos/metamask/metamask-extension/issues/${PR_NUMBER}/comments`; console.log(`Announcement:\n${commentBody}`); console.log(`Posting to: ${POST_COMMENT_URI}`); @@ -355,7 +366,7 @@ async function start() { body: JSON_PAYLOAD, headers: { 'User-Agent': 'metamaskbot', - Authorization: `token ${GITHUB_COMMENT_TOKEN}`, + Authorization: `token ${PR_COMMENT_TOKEN}`, }, }); if (!response.ok) { From edaae773872e404ddedd460c6ff423eaea35378b Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Thu, 9 Jan 2025 19:05:04 -0330 Subject: [PATCH 63/71] fix: Fix bundle size tracking (#29486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The bundle size tracking was accidentally broken in #29408. This restores the bundle size tracking. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29486?quickstart=1) ## **Related issues** Fixes #29485 Resolves bug introduced by #29408 ## **Manual testing steps** 1. Check the "mv3: Bundle Size Stats" link in the `metamaskbot` comment 2. Once this is merged, check the bundle size tracker to ensure it's working again: https://github.com/MetaMask/extension_bundlesize_stats * Unfortunately I am not sure how to easily test this on the PR. The tracker is only updated when commits are made to `main`. ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .circleci/config.yml | 34 ++++++ package.json | 1 + test/e2e/mv3-perf-stats/bundle-size.js | 140 +++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100755 test/e2e/mv3-perf-stats/bundle-size.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 5243d8ef4043..4a2b1a30a530 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -277,6 +277,9 @@ workflows: - user-actions-benchmark: requires: - prep-build-test + - bundle-size: + requires: + - prep-build-test - job-publish-prerelease: requires: - prep-deps @@ -294,6 +297,7 @@ workflows: - prep-build-ts-migration-dashboard - benchmark - user-actions-benchmark + - bundle-size - all-tests-pass - job-publish-release: filters: @@ -1270,6 +1274,36 @@ jobs: paths: - test-artifacts + bundle-size: + executor: node-browsers-small + steps: + - run: *shallow-git-clone-and-enable-vnc + - run: sudo corepack enable + - attach_workspace: + at: . + - run: + name: Move test build to dist + command: mv ./dist-test ./dist + - run: + name: Move test zips to builds + command: mv ./builds-test ./builds + - run: + name: Measure bundle size + command: yarn bundle-size --out test-artifacts/chrome + - run: + name: Install jq + command: sudo apt install jq -y + - run: + name: Record bundle size at commit + command: ./.circleci/scripts/bundle-stats-commit.sh + - store_artifacts: + path: test-artifacts + destination: test-artifacts + - persist_to_workspace: + root: . + paths: + - test-artifacts + job-publish-prerelease: executor: node-browsers-medium steps: diff --git a/package.json b/package.json index 77b94c6e74a4..5d8ac90663c9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "start:test:mv2:flask": "ENABLE_MV3=false yarn start:test:flask --apply-lavamoat=false --snow=false", "start:test:mv2": "ENABLE_MV3=false BLOCKAID_FILE_CDN=static.cx.metamask.io/api/v1/confirmations/ppom yarn start:test --apply-lavamoat=false --snow=false", "benchmark:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/benchmark.js", + "bundle-size": "SELENIUM_BROWSER=chrome ts-node test/e2e/mv3-perf-stats/bundle-size.js", "user-actions-benchmark:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/user-actions-benchmark.js", "benchmark:firefox": "SELENIUM_BROWSER=firefox ts-node test/e2e/benchmark.js", "build:test": "yarn env:e2e build test", diff --git a/test/e2e/mv3-perf-stats/bundle-size.js b/test/e2e/mv3-perf-stats/bundle-size.js new file mode 100755 index 000000000000..b6580d86538c --- /dev/null +++ b/test/e2e/mv3-perf-stats/bundle-size.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +/* eslint-disable node/shebang */ +const path = require('path'); +const { promises: fs } = require('fs'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const { + isWritable, + getFirstParentDirectoryThatExists, +} = require('../../helpers/file'); + +const { exitWithError } = require('../../../development/lib/exit-with-error'); + +/** + * The e2e test case is used to capture bundle time statistics for extension. + */ + +const backgroundFiles = [ + 'scripts/runtime-lavamoat.js', + 'scripts/lockdown-more.js', + 'scripts/sentry-install.js', + 'scripts/policy-load.js', +]; + +const uiFiles = [ + 'scripts/sentry-install.js', + 'scripts/runtime-lavamoat.js', + 'scripts/lockdown-more.js', + 'scripts/policy-load.js', +]; + +const BackgroundFileRegex = /background-[0-9]*.js/u; +const CommonFileRegex = /common-[0-9]*.js/u; +const UIFileRegex = /ui-[0-9]*.js/u; + +async function main() { + const { argv } = yargs(hideBin(process.argv)).usage( + '$0 [options]', + 'Capture bundle size stats', + (_yargs) => + _yargs.option('out', { + description: + 'Output filename. Output printed to STDOUT of this is omitted.', + type: 'string', + normalize: true, + }), + ); + const { out } = argv; + + const distFolder = 'dist/chrome'; + const backgroundFileList = []; + const uiFileList = []; + const commonFileList = []; + + const files = await fs.readdir(distFolder); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (CommonFileRegex.test(file)) { + const stats = await fs.stat(`${distFolder}/${file}`); + commonFileList.push({ name: file, size: stats.size }); + } else if ( + backgroundFiles.includes(file) || + BackgroundFileRegex.test(file) + ) { + const stats = await fs.stat(`${distFolder}/${file}`); + backgroundFileList.push({ name: file, size: stats.size }); + } else if (uiFiles.includes(file) || UIFileRegex.test(file)) { + const stats = await fs.stat(`${distFolder}/${file}`); + uiFileList.push({ name: file, size: stats.size }); + } + } + + const backgroundBundleSize = backgroundFileList.reduce( + (result, file) => result + file.size, + 0, + ); + + const uiBundleSize = uiFileList.reduce( + (result, file) => result + file.size, + 0, + ); + + const commonBundleSize = commonFileList.reduce( + (result, file) => result + file.size, + 0, + ); + + const result = { + background: { + name: 'background', + size: backgroundBundleSize, + fileList: backgroundFileList, + }, + ui: { + name: 'ui', + size: uiBundleSize, + fileList: uiFileList, + }, + common: { + name: 'common', + size: commonBundleSize, + fileList: commonFileList, + }, + }; + + if (out) { + const outPath = `${out}/bundle_size.json`; + const outputDirectory = path.dirname(outPath); + const existingParentDirectory = await getFirstParentDirectoryThatExists( + outputDirectory, + ); + if (!(await isWritable(existingParentDirectory))) { + throw new Error('Specified output file directory is not writable'); + } + if (outputDirectory !== existingParentDirectory) { + await fs.mkdir(outputDirectory, { recursive: true }); + } + await fs.writeFile(outPath, JSON.stringify(result, null, 2)); + await fs.writeFile( + `${out}/bundle_size_stats.json`, + JSON.stringify( + { + background: backgroundBundleSize, + ui: uiBundleSize, + common: commonBundleSize, + timestamp: new Date().getTime(), + }, + null, + 2, + ), + ); + } else { + console.log(JSON.stringify(result, null, 2)); + } +} + +main().catch((error) => { + exitWithError(error); +}); From 46bf1cfc4332d0cdda1091e4d69ccacfc3c2d596 Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 9 Jan 2025 15:11:18 -0800 Subject: [PATCH 64/71] test: Remove obsolete permitted chains feature flag tests (#29618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes tests that are no longer applicable pertaining to permitted chains. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29618?quickstart=1) ## **Related issues** See: https://github.com/MetaMask/metamask-extension/pull/27847#discussion_r1908944940 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../handlers/add-ethereum-chain.test.js | 444 ++++-------------- .../handlers/switch-ethereum-chain.test.js | 300 +++--------- 2 files changed, 156 insertions(+), 588 deletions(-) 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 ee0c9d3f732b..eb35e27a1c2b 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 @@ -84,92 +84,62 @@ describe('addEthereumChainHandler', () => { jest.clearAllMocks(); }); - 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(); - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: CHAIN_IDS.OPTIMISM, - chainName: 'Optimism Mainnet', - rpcUrls: ['https://optimism.llamarpc.com'], - nativeCurrency: { - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrls: ['https://optimistic.etherscan.io'], - iconUrls: ['https://optimism.icon.com'], - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); + it('creates a new network configuration for the given chainid, requests `endowment:permitted-chains` permission and switches to it if no networkConfigurations with the same chainId exist', async () => { + const nonInfuraConfiguration = createMockNonInfuraConfiguration(); - expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1); - expect(mocks.addNetwork).toHaveBeenCalledTimes(1); - expect(mocks.addNetwork).toHaveBeenCalledWith({ - blockExplorerUrls: ['https://optimistic.etherscan.io'], - defaultBlockExplorerUrlIndex: 0, - chainId: '0xa', - defaultRpcEndpointIndex: 0, - name: 'Optimism Mainnet', - nativeCurrency: 'ETH', - rpcEndpoints: [ + const mocks = makeMocks({ + permissionedChainIds: [], + overrides: { + getCurrentChainIdForDomain: jest + .fn() + .mockReturnValue(CHAIN_IDS.MAINNET), + }, + }); + await addEthereumChainHandler( + { + origin: 'example.com', + params: [ { - name: 'Optimism Mainnet', - url: 'https://optimism.llamarpc.com', - type: 'custom', + chainId: nonInfuraConfiguration.chainId, + chainName: nonInfuraConfiguration.name, + rpcUrls: nonInfuraConfiguration.rpcEndpoints.map((rpc) => rpc.url), + nativeCurrency: { + symbol: nonInfuraConfiguration.nativeCurrency, + decimals: 18, + }, + blockExplorerUrls: nonInfuraConfiguration.blockExplorerUrls, }, ], - }); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); - }); + }, + {}, + jest.fn(), + jest.fn(), + mocks, + ); - it('creates a new networkConfiguration when called without "blockExplorerUrls" property', async () => { - const mocks = makeMocks(); - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: CHAIN_IDS.OPTIMISM, - chainName: 'Optimism Mainnet', - rpcUrls: ['https://optimism.llamarpc.com'], - nativeCurrency: { - symbol: 'ETH', - decimals: 18, - }, - iconUrls: ['https://optimism.icon.com'], - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - expect(mocks.addNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - }); + expect(mocks.addNetwork).toHaveBeenCalledWith(nonInfuraConfiguration); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledWith([createMockNonInfuraConfiguration().chainId]); + expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); + expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); + }); - 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 () => { + describe('if a networkConfiguration for the given chainId already exists', () => { + describe('if the proposed networkConfiguration has a different rpcUrl from the one already in state', () => { + 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], overrides: { - getNetworkConfigurationByChainId: jest + getCurrentChainIdForDomain: jest .fn() - // Start with just infura endpoint - .mockReturnValue(createMockMainnetConfiguration()), + .mockReturnValue(CHAIN_IDS.SEPOLIA), }, }); - // Add a custom endpoint await addEthereumChainHandler( { origin: 'example.com', @@ -192,131 +162,38 @@ describe('addEthereumChainHandler', () => { mocks, ); - expect(mocks.updateNetwork).toHaveBeenCalledTimes(1); - expect(mocks.updateNetwork).toHaveBeenCalledWith( - '0x1', - { - chainId: '0x1', - name: 'Ethereum Mainnet', - // Expect both endpoints - rpcEndpoints: [ - { - networkClientId: 'mainnet', - url: 'https://mainnet.infura.io/v3/', - type: 'infura', - }, - { - name: 'Ethereum Mainnet', - url: 'https://eth.llamarpc.com', - type: 'custom', - }, - ], - // and the new one is the default - defaultRpcEndpointIndex: 1, - nativeCurrency: 'ETH', - blockExplorerUrls: ['https://etherscan.io'], - defaultBlockExplorerUrlIndex: 0, - }, - undefined, - ); + expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1); + expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); + expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); + expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); }); - it('makes the rpc url the default if it already exists', async () => { - const existingNetwork = { - chainId: '0x1', - name: 'Ethereum Mainnet', - // Start with infura + custom endpoint - rpcEndpoints: [ - { - networkClientId: 'mainnet', - url: 'https://mainnet.infura.io/v3/', - type: 'infura', - }, - { - name: 'Ethereum Mainnet', - url: 'https://eth.llamarpc.com', - type: 'custom', - }, - ], - // Infura is the default - defaultRpcEndpointIndex: 0, - nativeCurrency: 'ETH', - blockExplorerUrls: ['https://etherscan.io'], - defaultBlockExplorerUrlIndex: 0, - }; - + 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({ + permissionedChainIds: [], overrides: { getNetworkConfigurationByChainId: jest .fn() - .mockReturnValue(existingNetwork), - }, - }); - - // Add the same custom endpoint - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: CHAIN_IDS.MAINNET, - chainName: 'Ethereum Mainnet', - rpcUrls: ['https://eth.llamarpc.com'], - nativeCurrency: { - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrls: ['https://etherscan.io'], - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.updateNetwork).toHaveBeenCalledTimes(1); - expect(mocks.updateNetwork).toHaveBeenCalledWith( - '0x1', - { - ...existingNetwork, - // Verify the custom endpoint becomes the default - defaultRpcEndpointIndex: 1, - }, - undefined, - ); - }); - - it('switches to the network if its not already the currently selected chain id', async () => { - const existingNetwork = createMockMainnetConfiguration(); - - const mocks = makeMocks({ - overrides: { - // Start on sepolia + .mockReturnValue(createMockNonInfuraConfiguration()), getCurrentChainIdForDomain: jest .fn() - .mockReturnValue(CHAIN_IDS.SEPOLIA), - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(existingNetwork), + .mockReturnValue(CHAIN_IDS.MAINNET), }, }); - // Add with rpc + block explorers that already exist await addEthereumChainHandler( { origin: 'example.com', params: [ { - chainId: CHAIN_IDS.MAINNET, - chainName: 'Ethereum Mainnet', - rpcUrls: [existingNetwork.rpcEndpoints[0].url], + chainId: NON_INFURA_CHAIN_ID, + chainName: 'Custom Network', + rpcUrls: ['https://new-custom.network'], nativeCurrency: { - symbol: 'ETH', + symbol: 'CUST', decimals: 18, }, - blockExplorerUrls: ['https://etherscan.io'], + blockExplorerUrls: ['https://custom.blockexplorer'], }, ], }, @@ -326,65 +203,49 @@ describe('addEthereumChainHandler', () => { mocks, ); - // No updates, network already had all the info - expect(mocks.updateNetwork).toHaveBeenCalledTimes(0); - - // User should be prompted to switch chains + expect(mocks.updateNetwork).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledTimes(1); + expect( + mocks.grantPermittedChainsPermissionIncremental, + ).toHaveBeenCalledWith([NON_INFURA_CHAIN_ID]); expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith('mainnet'); - }); - - it('should return error for invalid chainId', async () => { - const mocks = makeMocks(); - const mockEnd = jest.fn(); - - await addEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: 'invalid_chain_id' }], - }, - {}, - jest.fn(), - mockEnd, - mocks, - ); - - expect(mockEnd).toHaveBeenCalledWith( - rpcErrors.invalidParams({ - message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\ninvalid_chain_id`, - }), - ); }); }); - }); - - describe('with `endowment:permitted-chains` permissioning active', () => { - it('creates a new network configuration for the given chainid, requests `endowment:permitted-chains` permission and switches to it if no networkConfigurations with the same chainId exist', async () => { - const nonInfuraConfiguration = createMockNonInfuraConfiguration(); + it('should switch to the existing networkConfiguration if one already exsits for the given chain id', async () => { const mocks = makeMocks({ - permissionedChainIds: [], + permissionedChainIds: [ + createMockOptimismConfiguration().chainId, + CHAIN_IDS.MAINNET, + ], overrides: { getCurrentChainIdForDomain: jest .fn() .mockReturnValue(CHAIN_IDS.MAINNET), + getNetworkConfigurationByChainId: jest + .fn() + .mockReturnValue(createMockOptimismConfiguration()), }, }); + await addEthereumChainHandler( { origin: 'example.com', params: [ { - chainId: nonInfuraConfiguration.chainId, - chainName: nonInfuraConfiguration.name, - rpcUrls: nonInfuraConfiguration.rpcEndpoints.map( + chainId: createMockOptimismConfiguration().chainId, + chainName: createMockOptimismConfiguration().name, + rpcUrls: createMockOptimismConfiguration().rpcEndpoints.map( (rpc) => rpc.url, ), nativeCurrency: { - symbol: nonInfuraConfiguration.nativeCurrency, + symbol: createMockOptimismConfiguration().nativeCurrency, decimals: 18, }, - blockExplorerUrls: nonInfuraConfiguration.blockExplorerUrls, + blockExplorerUrls: + createMockOptimismConfiguration().blockExplorerUrls, }, ], }, @@ -394,150 +255,11 @@ describe('addEthereumChainHandler', () => { mocks, ); - expect(mocks.addNetwork).toHaveBeenCalledWith(nonInfuraConfiguration); - expect( - mocks.grantPermittedChainsPermissionIncremental, - ).toHaveBeenCalledTimes(1); - expect( - mocks.grantPermittedChainsPermissionIncremental, - ).toHaveBeenCalledWith([createMockNonInfuraConfiguration().chainId]); + expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); - }); - - describe('if a networkConfiguration for the given chainId already exists', () => { - describe('if the proposed networkConfiguration has a different rpcUrl from the one already in state', () => { - 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], - overrides: { - getCurrentChainIdForDomain: jest - .fn() - .mockReturnValue(CHAIN_IDS.SEPOLIA), - }, - }); - - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: CHAIN_IDS.MAINNET, - chainName: 'Ethereum Mainnet', - rpcUrls: ['https://eth.llamarpc.com'], - nativeCurrency: { - symbol: 'ETH', - decimals: 18, - }, - blockExplorerUrls: ['https://etherscan.io'], - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.requestUserApproval).toHaveBeenCalledTimes(1); - expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith(123); - }); - - 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({ - permissionedChainIds: [], - overrides: { - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(createMockNonInfuraConfiguration()), - getCurrentChainIdForDomain: jest - .fn() - .mockReturnValue(CHAIN_IDS.MAINNET), - }, - }); - - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: NON_INFURA_CHAIN_ID, - chainName: 'Custom Network', - rpcUrls: ['https://new-custom.network'], - nativeCurrency: { - symbol: 'CUST', - decimals: 18, - }, - blockExplorerUrls: ['https://custom.blockexplorer'], - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.updateNetwork).toHaveBeenCalledTimes(1); - expect( - mocks.grantPermittedChainsPermissionIncremental, - ).toHaveBeenCalledTimes(1); - expect( - mocks.grantPermittedChainsPermissionIncremental, - ).toHaveBeenCalledWith([NON_INFURA_CHAIN_ID]); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - }); - }); - - it('should switch to the existing networkConfiguration if one already exsits for the given chain id', async () => { - const mocks = makeMocks({ - permissionedChainIds: [ - createMockOptimismConfiguration().chainId, - CHAIN_IDS.MAINNET, - ], - overrides: { - getCurrentChainIdForDomain: jest - .fn() - .mockReturnValue(CHAIN_IDS.MAINNET), - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(createMockOptimismConfiguration()), - }, - }); - - await addEthereumChainHandler( - { - origin: 'example.com', - params: [ - { - chainId: createMockOptimismConfiguration().chainId, - chainName: createMockOptimismConfiguration().name, - rpcUrls: createMockOptimismConfiguration().rpcEndpoints.map( - (rpc) => rpc.url, - ), - nativeCurrency: { - symbol: createMockOptimismConfiguration().nativeCurrency, - decimals: 18, - }, - blockExplorerUrls: - createMockOptimismConfiguration().blockExplorerUrls, - }, - ], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockOptimismConfiguration().rpcEndpoints[0].networkClientId, - ); - }); + expect(mocks.setActiveNetwork).toHaveBeenCalledWith( + createMockOptimismConfiguration().rpcEndpoints[0].networkClientId, + ); }); }); 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 be612fbc7d8e..abd0c0eb9ff7 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 @@ -16,16 +16,6 @@ const createMockMainnetConfiguration = () => ({ ], }); -const createMockLineaMainnetConfiguration = () => ({ - chainId: CHAIN_IDS.LINEA_MAINNET, - defaultRpcEndpointIndex: 0, - rpcEndpoints: [ - { - networkClientId: NETWORK_TYPES.LINEA_MAINNET, - }, - ], -}); - describe('switchEthereumChainHandler', () => { const makeMocks = ({ permissionedChainIds = [], @@ -55,228 +45,84 @@ describe('switchEthereumChainHandler', () => { jest.clearAllMocks(); }); - describe('with permittedChains permissioning inactive', () => { - it('should call setActiveNetwork when switching to a built-in infura network', async () => { - const mocks = makeMocks({ - overrides: { - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(createMockMainnetConfiguration()), - }, - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.MAINNET }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); - }); - - it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is lower case', async () => { - const mocks = makeMocks({ - overrides: { - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(createMockLineaMainnetConfiguration()), - }, - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toLowerCase() }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockLineaMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); - }); - - it('should call setActiveNetwork when switching to a built-in infura network, when chainId from request is upper case', async () => { - const mocks = makeMocks({ - overrides: { - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(createMockLineaMainnetConfiguration()), - }, - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.LINEA_MAINNET.toUpperCase() }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockLineaMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); - }); - - it('should call setActiveNetwork when switching to a custom network', async () => { - const mocks = makeMocks({ - overrides: { - getCurrentChainIdForDomain: jest - .fn() - .mockReturnValue(CHAIN_IDS.MAINNET), - }, - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: NON_INFURA_CHAIN_ID }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); - }); - - it('should handle missing networkConfiguration', async () => { - // Mock a network configuration that has an undefined or missing rpcEndpoints - const mockNetworkConfiguration = undefined; - - const mocks = makeMocks({ - overrides: { - getNetworkConfigurationByChainId: jest - .fn() - .mockReturnValue(mockNetworkConfiguration), - }, - }); - - const switchEthereumChainHandler = switchEthereumChain.implementation; - - const mockEnd = jest.fn(); - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.MAINNET }], - }, - {}, - jest.fn(), - mockEnd, - mocks, - ); - - // Check that the function handled the missing rpcEndpoints and did not attempt to call setActiveNetwork - expect(mockEnd).toHaveBeenCalledWith( - expect.objectContaining({ - code: 4902, - message: expect.stringContaining('Unrecognized chain ID'), - }), - ); - expect(mocks.setActiveNetwork).not.toHaveBeenCalled(); + it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in `endowment:permitted-chains`', async () => { + const mockrequestPermittedChainsPermission = jest.fn().mockResolvedValue(); + const mocks = makeMocks({ + overrides: { + requestPermittedChainsPermission: mockrequestPermittedChainsPermission, + }, }); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.MAINNET }], + }, + {}, + jest.fn(), + jest.fn(), + mocks, + ); + + expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); + expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledWith([ + CHAIN_IDS.MAINNET, + ]); + expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); + expect(mocks.setActiveNetwork).toHaveBeenCalledWith( + createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, + ); }); - describe('with permittedChains permissioning active', () => { - it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in `endowment:permitted-chains`', async () => { - const mockrequestPermittedChainsPermission = jest - .fn() - .mockResolvedValue(); - const mocks = makeMocks({ - overrides: { - requestPermittedChainsPermission: - mockrequestPermittedChainsPermission, - }, - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.MAINNET }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledWith([ - CHAIN_IDS.MAINNET, - ]); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); + it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in `endowment:permitted-chains`', async () => { + const mocks = makeMocks({ + permissionedChainIds: [CHAIN_IDS.MAINNET], }); + const switchEthereumChainHandler = switchEthereumChain.implementation; + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.MAINNET }], + }, + {}, + jest.fn(), + jest.fn(), + mocks, + ); + + expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); + expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); + expect(mocks.setActiveNetwork).toHaveBeenCalledWith( + createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, + ); + }); - it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in `endowment:permitted-chains`', async () => { - const mocks = makeMocks({ - permissionedChainIds: [CHAIN_IDS.MAINNET], - }); - const switchEthereumChainHandler = switchEthereumChain.implementation; - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.MAINNET }], - }, - {}, - jest.fn(), - jest.fn(), - mocks, - ); - - expect(mocks.requestPermittedChainsPermission).not.toHaveBeenCalled(); - expect(mocks.setActiveNetwork).toHaveBeenCalledTimes(1); - expect(mocks.setActiveNetwork).toHaveBeenCalledWith( - createMockMainnetConfiguration().rpcEndpoints[0].networkClientId, - ); - }); - - it('should handle errors during the switch network permission request', async () => { - const mockError = new Error('Permission request failed'); - const mockrequestPermittedChainsPermission = jest - .fn() - .mockRejectedValue(mockError); - const mocks = makeMocks({ - overrides: { - requestPermittedChainsPermission: - mockrequestPermittedChainsPermission, - }, - }); - const mockEnd = jest.fn(); - const switchEthereumChainHandler = switchEthereumChain.implementation; - - await switchEthereumChainHandler( - { - origin: 'example.com', - params: [{ chainId: CHAIN_IDS.MAINNET }], - }, - {}, - jest.fn(), - mockEnd, - mocks, - ); - - expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); - expect(mockEnd).toHaveBeenCalledWith(mockError); - expect(mocks.setActiveNetwork).not.toHaveBeenCalled(); + it('should handle errors during the switch network permission request', async () => { + const mockError = new Error('Permission request failed'); + const mockrequestPermittedChainsPermission = jest + .fn() + .mockRejectedValue(mockError); + const mocks = makeMocks({ + overrides: { + requestPermittedChainsPermission: mockrequestPermittedChainsPermission, + }, }); + const mockEnd = jest.fn(); + const switchEthereumChainHandler = switchEthereumChain.implementation; + + await switchEthereumChainHandler( + { + origin: 'example.com', + params: [{ chainId: CHAIN_IDS.MAINNET }], + }, + {}, + jest.fn(), + mockEnd, + mocks, + ); + + expect(mocks.requestPermittedChainsPermission).toHaveBeenCalledTimes(1); + expect(mockEnd).toHaveBeenCalledWith(mockError); + expect(mocks.setActiveNetwork).not.toHaveBeenCalled(); }); }); From 64400d8f15db2abb0995ce43dd5a140b58841309 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Fri, 10 Jan 2025 11:35:48 +0100 Subject: [PATCH 65/71] chore: bump `@metamask/profile-sync-controller` to `v3.2.0` (#29598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/profile-sync-controller` to `v3.2.0`. This will add better error logging related to all things authentication & profile syncing [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29598?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- .../errors-after-init-opt-in-background-state.json | 3 ++- .../errors-after-init-opt-in-ui-state.json | 1 + .../notifications&auth/data/notification-state.ts | 1 + ui/selectors/identity/profile-syncing.test.ts | 1 + yarn.lock | 12 ++++++------ 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5d8ac90663c9..1e084dbda471 100644 --- a/package.json +++ b/package.json @@ -340,7 +340,7 @@ "@metamask/post-message-stream": "^8.0.0", "@metamask/ppom-validator": "0.36.0", "@metamask/preinstalled-example-snap": "^0.3.0", - "@metamask/profile-sync-controller": "^3.1.1", + "@metamask/profile-sync-controller": "^3.2.0", "@metamask/providers": "^18.2.0", "@metamask/queued-request-controller": "^7.0.1", "@metamask/rate-limit-controller": "^6.0.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 cab47452ea3f..0480d536973d 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 @@ -347,6 +347,7 @@ "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", "hasAccountSyncingSyncedAtLeastOnce": "boolean", - "isAccountSyncingReadyToBeDispatched": "boolean" + "isAccountSyncingReadyToBeDispatched": "boolean", + "isAccountSyncingInProgress": "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 e2b805831ea4..5a0f45d29c7e 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 @@ -219,6 +219,7 @@ "isProfileSyncingUpdateLoading": "boolean", "hasAccountSyncingSyncedAtLeastOnce": "boolean", "isAccountSyncingReadyToBeDispatched": "boolean", + "isAccountSyncingInProgress": "boolean", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", diff --git a/test/integration/notifications&auth/data/notification-state.ts b/test/integration/notifications&auth/data/notification-state.ts index 61d74d161671..d91c1e7f0d2e 100644 --- a/test/integration/notifications&auth/data/notification-state.ts +++ b/test/integration/notifications&auth/data/notification-state.ts @@ -40,6 +40,7 @@ export const getMockedNotificationsState = () => { isProfileSyncingUpdateLoading: false, hasAccountSyncingSyncedAtLeastOnce: false, isAccountSyncingReadyToBeDispatched: false, + isAccountSyncingInProgress: false, isMetamaskNotificationsFeatureSeen: true, isNotificationServicesEnabled: true, isFeatureAnnouncementsEnabled: true, diff --git a/ui/selectors/identity/profile-syncing.test.ts b/ui/selectors/identity/profile-syncing.test.ts index d05512e59523..74d4a31984f9 100644 --- a/ui/selectors/identity/profile-syncing.test.ts +++ b/ui/selectors/identity/profile-syncing.test.ts @@ -7,6 +7,7 @@ describe('Profile Syncing Selectors', () => { isProfileSyncingUpdateLoading: false, isAccountSyncingReadyToBeDispatched: false, hasAccountSyncingSyncedAtLeastOnce: false, + isAccountSyncingInProgress: false, }, }; diff --git a/yarn.lock b/yarn.lock index e98bb06943fe..ccb5a1a0e260 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6073,11 +6073,11 @@ __metadata: languageName: node linkType: hard -"@metamask/profile-sync-controller@npm:^3.1.1": - version: 3.1.1 - resolution: "@metamask/profile-sync-controller@npm:3.1.1" +"@metamask/profile-sync-controller@npm:^3.2.0": + version: 3.2.0 + resolution: "@metamask/profile-sync-controller@npm:3.2.0" dependencies: - "@metamask/base-controller": "npm:^7.0.2" + "@metamask/base-controller": "npm:^7.1.0" "@metamask/keyring-api": "npm:^12.0.0" "@metamask/keyring-controller": "npm:^19.0.2" "@metamask/network-controller": "npm:^22.1.1" @@ -6095,7 +6095,7 @@ __metadata: "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.10.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/42dd1ea0f9595eca6bd1a179681f7d16dfc6cc5090494131c8999a15caa02866f133febe1a95fca7cc74b4f3dafa7ef740f27b08eadbbf3049ebc087a739fb1b + checksum: 10/6fabb31266dfe14e67a3fcfcf8e5826786519bc0086d109b5cf324e4fa87f1fe5916bab2e9ac92489950531dee81e67e298b00b60fdaf9821312d170435005bf languageName: node linkType: hard @@ -26665,7 +26665,7 @@ __metadata: "@metamask/ppom-validator": "npm:0.36.0" "@metamask/preferences-controller": "npm:^15.0.1" "@metamask/preinstalled-example-snap": "npm:^0.3.0" - "@metamask/profile-sync-controller": "npm:^3.1.1" + "@metamask/profile-sync-controller": "npm:^3.2.0" "@metamask/providers": "npm:^18.2.0" "@metamask/queued-request-controller": "npm:^7.0.1" "@metamask/rate-limit-controller": "npm:^6.0.0" From 5778b4a64a2b7aeba40c46e0afb295d214b36355 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 10 Jan 2025 12:18:33 +0000 Subject: [PATCH 66/71] feat: Display clickable cursor on hover on petname component (#29477) 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/29477?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2340 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/components/app/name/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/components/app/name/index.scss b/ui/components/app/name/index.scss index f82638373ecd..8037a25390e4 100644 --- a/ui/components/app/name/index.scss +++ b/ui/components/app/name/index.scss @@ -6,6 +6,7 @@ gap: 5px; font-size: 12px; max-width: 100%; + cursor: pointer; &__missing { background-color: var(--color-background-alternative); From dd26784d605c92cad0bbce04985724c9b1c088c7 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 10 Jan 2025 14:27:07 +0000 Subject: [PATCH 67/71] feat: Nonce is always editable in advanced details view (#29627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Previously, the nonce can only be edited if the option to do it is enabled in the corresponding settings toggle. This PR removes the link with that toggle in the redesigned transaction screens. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29627?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/29512 ## **Manual testing steps** 1. Go to the test dApp 2. Disable `Advanced > Customize transaction nonce` in settings 3. Create a send eth transaction 4. Toggle on advanced details 5. The edit nonce icon should be visible in the confirmation screen ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../__snapshots__/approve.test.tsx.snap | 11 ++++++++++ .../advanced-details.test.tsx.snap | 22 +++++++++++++++++++ .../advanced-details/advanced-details.tsx | 6 +---- 3 files changed, 34 insertions(+), 5 deletions(-) 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 8e13a0885858..75fdd3765851 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 @@ -634,6 +634,17 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" data-testid="advanced-details-displayed-nonce" > +

    renders component when the prop override is passed 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" data-testid="advanced-details-displayed-nonce" > +

    renders component when the state property is true 1 class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" data-testid="advanced-details-displayed-nonce" > +

    { } }, [currentConfirmation, dispatch]); - const enableCustomNonce = useSelector(getUseNonceField); const nextNonce = useSelector(getNextSuggestedNonce); const customNonceValue = useSelector(getCustomNonceValue); @@ -65,9 +63,7 @@ const NonceDetails = () => { openEditNonceModal() : undefined - } + onEditClick={() => openEditNonceModal()} editIconClassName="edit-nonce-btn" editIconDataTestId="edit-nonce-icon" /> From 3ff4aba94aacf6a46488735c7656e6a1b7c1cdc2 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 10 Jan 2025 15:35:34 +0100 Subject: [PATCH 68/71] fix: Remove unwanted empty `div` from signature confirmations (#29622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** https://github.com/MetaMask/metamask-extension/pull/28854/files introduced an unwanted empty div on top of signature content. This PR aims to relocate that into child component. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29622?quickstart=1) ## **Related issues** ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** Screenshot 2025-01-10 at 09 59 45 ### **After** Screenshot 2025-01-10 at 09 58 38 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../smart-transactions-banner-alert.tsx | 45 ++++++++++--------- .../__snapshots__/confirm.test.tsx.snap | 24 ---------- ui/pages/confirmations/confirm/confirm.tsx | 5 +-- 3 files changed, 25 insertions(+), 49 deletions(-) diff --git a/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx index e9f28a25c318..6a2739c0eeb1 100644 --- a/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx +++ b/ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.tsx @@ -5,6 +5,7 @@ import { useI18nContext } from '../../../../hooks/useI18nContext'; import { BannerAlert, ButtonLink, + Box, Text, BannerAlertSeverity, } from '../../../../components/component-library'; @@ -95,27 +96,29 @@ export const SmartTransactionsBannerAlert: React.FC - - {t('smartTransactionsEnabledTitle')} - - - - {t('smartTransactionsEnabledLink')} - - {t('smartTransactionsEnabledDescription')} - - + + + + {t('smartTransactionsEnabledTitle')} + + + + {t('smartTransactionsEnabledLink')} + + {t('smartTransactionsEnabledDescription')} + + + ); }); diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index 85c6e24b3d78..f528654a4525 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -116,9 +116,6 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = `

-
-
-
-
-
-
-
-
{ return ( @@ -55,9 +54,7 @@ const Confirm = () => (
- - - + { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) From 1df79a9306a84319121493f1f00605dc9d987713 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 10 Jan 2025 12:14:11 -0330 Subject: [PATCH 69/71] chore: Remove obsolete keys (#29372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove references to obsolete keys. * PUBNUB_*: These were used for the old mobile state sync feature, which was removed a long time ago. * ETHERSCAN_KEY: This was used for incoming transactions, but we've since switched to our own API. * OPENSEA_KEY: We've switched to Reservoir The `ETHERSCAN_KEY ` environment variable is still refrenced in the `gridplus-sdk` package, so it has been left in the `build.yml` file as `null` to indicate that it's intentionally unset. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29372?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** N/A ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/metamask-controller.js | 2 -- builds.yml | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 38ec6023b264..c98ba72c0f96 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -761,8 +761,6 @@ export default class MetamaskController extends EventEmitter { }), }); - this.nftController.setApiKey(process.env.OPENSEA_KEY); - const nftDetectionControllerMessenger = this.controllerMessenger.getRestricted({ name: 'NftDetectionController', diff --git a/builds.yml b/builds.yml index 43cf02d3c8e5..ddc4858eb973 100644 --- a/builds.yml +++ b/builds.yml @@ -214,13 +214,9 @@ env: # API keys to 3rd party services ### - - PUBNUB_PUB_KEY: null - - PUBNUB_SUB_KEY: null - SEGMENT_HOST: null - SENTRY_DSN: null - SENTRY_DSN_DEV: null - - OPENSEA_KEY: null - - ETHERSCAN_KEY: null # also INFURA_PROJECT_ID below ### @@ -318,3 +314,10 @@ env: # This should NEVER be enabled in production since it slows down react ### - ENABLE_WHY_DID_YOU_RENDER: false + + ### + # Unused environment variables referenced in dependencies + # Unset environment variables cause a build error. These are set to `null` to tell our build + # system that they are intentionally unset. + ### + - ETHERSCAN_KEY: null # Used by `gridplus-sdk/dist/util.js` From f58258b7ac25137c8e3155e60c6620b206a10f9f Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 10 Jan 2025 07:51:34 -0800 Subject: [PATCH 70/71] refactor: remove unused end param in ethereum-chain-util helpers (#29619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes unused `end` param in the ethereum-chain-util helpers * validateChainId * validateAddEthereumChainParams * validateSwitchEthereumChainParams [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29619?quickstart=1) Extending E2E timeout to get past "no timings found" error: ``` flags = { "circleci": { "timeoutMinutes": 30 } } ``` ## **Related issues** See: https://github.com/MetaMask/metamask-extension/pull/27847#discussion_r1907939905 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../rpc-method-middleware/handlers/add-ethereum-chain.js | 2 +- .../handlers/ethereum-chain-utils.js | 8 ++++---- .../handlers/switch-ethereum-chain.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) 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 afcc2e167043..66d57dd8786b 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 @@ -50,7 +50,7 @@ async function addEthereumChainHandler( ) { let validParams; try { - validParams = validateAddEthereumChainParams(req.params[0], end); + validParams = validateAddEthereumChainParams(req.params[0]); } catch (error) { return end(error); } 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 10973e052715..2f86b30885e5 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 @@ -25,7 +25,7 @@ export function validateChainId(chainId) { return _chainId; } -export function validateSwitchEthereumChainParams(req, end) { +export function validateSwitchEthereumChainParams(req) { if (!req.params?.[0] || typeof req.params[0] !== 'object') { throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( @@ -43,10 +43,10 @@ export function validateSwitchEthereumChainParams(req, end) { }); } - return validateChainId(chainId, end); + return validateChainId(chainId); } -export function validateAddEthereumChainParams(params, end) { +export function validateAddEthereumChainParams(params) { if (!params || typeof params !== 'object') { throw rpcErrors.invalidParams({ message: `Expected single, object parameter. Received:\n${JSON.stringify( @@ -75,7 +75,7 @@ export function validateAddEthereumChainParams(params, end) { }); } - const _chainId = validateChainId(chainId, end); + const _chainId = validateChainId(chainId); if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) { throw rpcErrors.invalidParams({ message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, 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 5f907bef4d4b..1fbeedbef3f5 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 @@ -36,7 +36,7 @@ async function switchEthereumChainHandler( ) { let chainId; try { - chainId = validateSwitchEthereumChainParams(req, end); + chainId = validateSwitchEthereumChainParams(req); } catch (error) { return end(error); } From b2c53144a0e67971b625b4f9d430aeba40f11c87 Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:46:34 +0400 Subject: [PATCH 71/71] fix: metamaskbot comment nits (#29636) 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/29636?quickstart=1) This PR adds minor fixes and enhancements to things that were not worth blocking the original metamaskbot PR for. Things like, comments, style, reusability should be improved by this PR. Interestingly, I also saw that the `$OWNER` environment variable was missing, but somehow the workflow still worked. I added this environment variable just to make it sane. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28572 ## **Manual testing steps** 1. Everything should still work ## **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/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .github/workflows/publish-prerelease.yml | 29 +++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index 9b4e460d546a..2674b8565eff 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -25,41 +25,44 @@ jobs: BASE_REF: ${{ github.event.pull_request.base.ref }} run: | merge_base="$(git merge-base "origin/${BASE_REF}" HEAD)" - echo "Merge base is '${merge_base}'" echo "MERGE_BASE=${merge_base}" >> "$GITHUB_OUTPUT" + echo "Merge base is '${merge_base}'" - name: Get CircleCI job details id: get-circleci-job-details env: - REPOSITORY: ${{ github.repository }} + OWNER: ${{ github.repository_owner }} + REPOSITORY: ${{ github.event.repository.name }} + # For a `pull_request` event, the branch is `github.head_ref``. BRANCH: ${{ github.head_ref }} + # For a `pull_request` event, the head commit hash is `github.event.pull_request.head.sha`. HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha }} + JOB_NAME: job-publish-prerelease run: | - pipeline_id=$(curl --silent "https://circleci.com/api/v2/project/gh/$OWNER/$REPOSITORY/pipeline?branch=$BRANCH" | jq -r ".items | map(select(.vcs.revision == \"${HEAD_COMMIT_HASH}\" )) | first | .id") + pipeline_id=$(curl --silent "https://circleci.com/api/v2/project/gh/$OWNER/$REPOSITORY/pipeline?branch=$BRANCH" | jq --arg head_commit_hash "${HEAD_COMMIT_HASH}" -r '.items | map(select(.vcs.revision == $head_commit_hash)) | first | .id') workflow_id=$(curl --silent "https://circleci.com/api/v2/pipeline/$pipeline_id/workflow" | jq -r ".items[0].id") - job_details=$(curl --silent "https://circleci.com/api/v2/workflow/$workflow_id/job" | jq -r '.items[] | select(.name == "job-publish-prerelease")') - build_num=$(echo "$job_details" | jq -r '.job_number') + job_details=$(curl --silent "https://circleci.com/api/v2/workflow/$workflow_id/job" | jq --arg job_name "${JOB_NAME}" -r '.items[] | select(.name == $job_name)') + build_num=$(echo "$job_details" | jq -r '.job_number') echo 'CIRCLE_BUILD_NUM='"$build_num" >> "$GITHUB_OUTPUT" + job_id=$(echo "$job_details" | jq -r '.id') echo 'CIRCLE_WORKFLOW_JOB_ID='"$job_id" >> "$GITHUB_OUTPUT" - echo "Getting artifacts from pipeline '${pipeline_id}', workflow '${workflow_id}', build number '${build_num}', job ID '${job_id}'" + echo "Getting artifacts from pipeline '${pipeline_id}', workflow '${workflow_id}', build number '${build_num}', job id '${job_id}'" - name: Get CircleCI job artifacts env: CIRCLE_WORKFLOW_JOB_ID: ${{ steps.get-circleci-job-details.outputs.CIRCLE_WORKFLOW_JOB_ID }} run: | - mkdir -p "test-artifacts/chrome/benchmark" + mkdir -p test-artifacts/chrome/benchmark curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/test-artifacts/chrome/benchmark/pageload.json" > "test-artifacts/chrome/benchmark/pageload.json" - bundle_size=$(curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/test-artifacts/chrome/bundle_size.json") - mkdir -p "test-artifacts/chrome" - echo "${bundle_size}" > "test-artifacts/chrome/bundle_size.json" + mkdir -p test-artifacts/chrome + curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/test-artifacts/chrome/bundle_size.json" > "test-artifacts/chrome/bundle_size.json" - stories=$(curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/storybook/stories.json") - mkdir "storybook-build" - echo "${stories}" > "storybook-build/stories.json" + mkdir storybook-build + curl --silent --location "https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/storybook/stories.json" > "storybook-build/stories.json" - name: Publish prerelease env: