From fcb426588560374faf4e1e23acf4aed8b9c90b9a Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:58:13 +0200 Subject: [PATCH 01/22] fix: flaky test screenshot for `Increase Token Allowance increases token spending cap to allow other accounts to transfer tokens ` (#25324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This test fails whenever we try to switch to the MetaMask Dialog. This PR waits until there are 3 window handles and then performs the window switch. Not sure yet which is the cause of the Dialog not appearing, as I cannot reproduce it locally. However, the benefit of this change is that in the case the dialog does not appear, now we'll be able to take a screenshot and check the artifacts for further information and debugging. See ci failure: https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/87524/workflows/aa81e40a-0aa1-40dc-9ddf-dc83b4db83f1/jobs/3205989/parallel-runs/8?filterBy=FAILED [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25324?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ![Screenshot from 2024-06-14 15-30-23](https://github.com/MetaMask/metamask-extension/assets/54408225/41fd0f92-2d50-4550-a43b-04005d7dc912) See how no screenshot was taken due to the failure on the window switch ![Screenshot from 2024-06-14 15-30-39](https://github.com/MetaMask/metamask-extension/assets/54408225/88c7b4f9-7a5f-4bea-86ee-635fb9de41ce) ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: HJetpoluru Co-authored-by: Harika Jetpoluru <153644847+hjetpoluru@users.noreply.github.com> --- test/e2e/tests/tokens/increase-token-allowance.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/tests/tokens/increase-token-allowance.spec.js b/test/e2e/tests/tokens/increase-token-allowance.spec.js index 4fb092906558..52a7d9454e38 100644 --- a/test/e2e/tests/tokens/increase-token-allowance.spec.js +++ b/test/e2e/tests/tokens/increase-token-allowance.spec.js @@ -232,6 +232,8 @@ describe('Increase Token Allowance', function () { }); await driver.delay(2000); + // Windows: MetaMask, Test Dapp and Dialog + await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); let spendingCapElement = await driver.findElement( '[data-testid="custom-spending-cap-input"]', @@ -297,6 +299,8 @@ describe('Increase Token Allowance', function () { } async function confirmTransferFromTokensSuccess(driver) { + // Windows: MetaMask, Test Dapp and Dialog + await driver.waitUntilXWindowHandles(3, 1000, 10000); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: '1.5 TST', tag: 'h1' }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); From 18ebe12d66130d6ddeb12d211fe973642b4ef7ce Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Mon, 24 Jun 2024 16:48:21 -0230 Subject: [PATCH 02/22] test: Simplify Jest config and expand coverage (#25013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Our Mocha and Jest configuration has been messy because we're in the middle of a migration from Mocha to Jest. Each file had to be explicitly included in the Jest configuration or ignored in the Mocha configuration. This was inconvenient and error-prone, and resulted in some tests not being run at all. ~Both test configurations have been updated to use a shared list of Mocha test files. There are only a few of these files left, and this list should only get shorter as we migrate more tests to Jest. No further configuration changes will be needed to add Jest tests.~ We have now finished migrating unit tests from Mocha to Jest, so this PR now only affects the Jest configuration. Note that the ESLint configuration has not been updated to use these simpler globs to determine which tests use Mocha and which use Jest. I tried doing that in this PR initially but it raised too many lint errors, so that will come in a later PR. In the meantime, we may still need to update `.eslintrc.js` when adding a new test of either type. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25013?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** Run `yarn test:unit` and verify that it runs correctly (no errors, no missing tests) ## **Screenshots/Recordings** N/A ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- jest.config.js | 51 +++----------------------------------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/jest.config.js b/jest.config.js index 1d6f86938950..f52466a9d183 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,26 +1,8 @@ module.exports = { collectCoverageFrom: [ - '/app/scripts/constants/error-utils.js', - '/app/scripts/controllers/app-metadata.ts', - '/app/scripts/controllers/permissions/**/*.js', - '/app/scripts/controllers/sign.ts', - '/app/scripts/controllers/decrypt-message.ts', - '/app/scripts/controllers/encryption-public-key.ts', - '/app/scripts/controllers/transactions/etherscan.ts', - '/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts', - '/app/scripts/controllers/transactions/IncomingTransactionHelper.ts', - '/app/scripts/controllers/preferences.js', - '/app/scripts/flask/**/*.js', - '/app/scripts/lib/**/*.(js|ts)', - '/app/scripts/metamask-controller.js', - '/app/scripts/migrations/*.js', - '/app/scripts/migrations/*.ts', - '!/app/scripts/migrations/*.test.(js|ts)', - '/app/scripts/platforms/*.js', + '/app/scripts/**/*.(js|ts|tsx)', '/shared/**/*.(js|ts|tsx)', '/ui/**/*.(js|ts|tsx)', - '/development/fitness-functions/**/*.test.(js|ts|tsx)', - '/test/e2e/helpers.test.js', ], coverageDirectory: './coverage', coveragePathIgnorePatterns: ['.stories.*', '.snap'], @@ -41,35 +23,8 @@ module.exports = { setupFiles: ['/test/setup.js', '/test/env.js'], setupFilesAfterEnv: ['/test/jest/setup.js'], testMatch: [ - '/app/scripts/constants/error-utils.test.js', - '/app/scripts/controllers/app-metadata.test.ts', - '/app/scripts/controllers/app-state.test.js', - '/app/scripts/controllers/encryption-public-key.test.ts', - '/app/scripts/controllers/transactions/etherscan.test.ts', - '/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.test.ts', - '/app/scripts/controllers/transactions/IncomingTransactionHelper.test.ts', - '/app/scripts/controllers/onboarding.test.ts', - '/app/scripts/controllers/mmi-controller.test.ts', - '/app/scripts/controllers/permissions/**/*.test.js', - '/app/scripts/controllers/preferences.test.js', - '/app/scripts/controllers/sign.test.ts', - '/app/scripts/controllers/decrypt-message.test.ts', - '/app/scripts/controllers/authentication/**/*.test.ts', - '/app/scripts/controllers/push-platform-notifications/**/*.test.ts', - '/app/scripts/controllers/user-storage/**/*.test.ts', - '/app/scripts/controllers/metamask-notifications/**/*.test.ts', - '/app/scripts/metamask-controller.actions.test.js', - '/app/scripts/detect-multiple-instances.test.js', - '/app/scripts/controllers/swaps.test.js', - '/app/scripts/controllers/metametrics.test.js', - '/app/scripts/flask/**/*.test.js', - '/app/scripts/lib/**/*.test.(js|ts)', - '/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', - '/app/scripts/metamask-controller.test.js', - '/app/scripts/migrations/*.test.(js|ts)', - '/app/scripts/platforms/*.test.js', - '/app/scripts/translate.test.ts', - '/shared/**/*.test.(js|ts)', + '/app/scripts/**/*.test.(js|ts|tsx)', + '/shared/**/*.test.(js|ts|tsx)', '/ui/**/*.test.(js|ts|tsx)', '/development/fitness-functions/**/*.test.(js|ts|tsx)', '/test/e2e/helpers.test.js', From aac197511aa28de751a1661951ba6cb559b4f067 Mon Sep 17 00:00:00 2001 From: Victor Thomas <10986371+vthomas13@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:05:08 -0400 Subject: [PATCH 03/22] fix(ci): Restrict test-e2e-firefox-flask & test-e2e-firefox-confirmation-redesign jobs to develop, master, and release branches (#25469) --- .circleci/config.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 091dbdf6189e..924193b6fea3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,6 +45,14 @@ rc_branch_only: &rc_branch_only only: - /^Version-v(\d+)[.](\d+)[.](\d+)/ +develop_master_rc_only: &develop_master_rc_only + filters: + branches: + only: + - develop + - master + - /^Version-v(\d+)[.](\d+)[.](\d+)/ + aliases: # Shallow Git Clone - &shallow-git-clone @@ -143,12 +151,14 @@ workflows: requires: - prep-deps - prep-build-confirmation-redesign-test-mv2: + <<: *develop_master_rc_only requires: - prep-deps - prep-build-test-flask: requires: - prep-deps - prep-build-test-flask-mv2: + <<: *develop_master_rc_only requires: - prep-deps - prep-build-test-mmi: @@ -184,6 +194,7 @@ workflows: requires: - prep-build-test-mv2 - test-e2e-firefox-confirmation-redesign: + <<: *develop_master_rc_only requires: - prep-build-confirmation-redesign-test-mv2 - test-e2e-chrome-rpc: @@ -196,6 +207,7 @@ workflows: requires: - prep-build-test-flask - test-e2e-firefox-flask: + <<: *develop_master_rc_only requires: - prep-build-test-flask-mv2 - test-e2e-chrome-mmi: From e1cfecfb3e6ce04b0d1a9d2e9671e744a79dc79f Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 24 Jun 2024 15:32:28 -0500 Subject: [PATCH 04/22] fix: #25189 - Display network badge for send flow asset (#25470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds the network badge for the chosen asset in the new Send flow [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25470?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/25189 ## **Manual testing steps** 1. Go to Ethereum Mainnet 2. Go to the Send flow 3. See the chosen asset with Mainnet logo 4. Change to Base network 5. Go to Send flow 6. See the chosen asset with the Base logo ## **Screenshots/Recordings** ### **Before** 338344947-c9a6eff0-74f1-4b8a-bca4-48a8325a14d8 ### **After** SCR-20240621-jquj ## **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. --- .../__snapshots__/asset-picker.test.tsx.snap | 52 ++++++++++++++----- .../asset-picker/asset-picker.tsx | 43 ++++++++++++--- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap index 4b62e6ebe220..9eedf3168b8b 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker/__snapshots__/asset-picker.test.tsx.snap @@ -13,18 +13,31 @@ exports[`AssetPicker matches snapshot 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-3 mm-box--align-items-center" >
- - undefined logo +
+ + NATIVE TICKER logo +
+
+
+ ? +
+
- ? +
+ s +
+
+
+ ? +
+
{/* This is the Modal that ask to choose token to send */} @@ -155,13 +165,32 @@ export function AssetPicker({ title={isDisabled ? t('swapTokenNotAvailable') : undefined} > - + + } + marginRight={3} + > + + + {formattedSymbol} From 096d73f73bcc984ecda97410273cc3ea0fbf9016 Mon Sep 17 00:00:00 2001 From: Nicolas Ferro Date: Mon, 24 Jun 2024 22:34:53 +0200 Subject: [PATCH 05/22] fix(swaps): fix to round gas values to integer (#25488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes rounding for max gas calculations, preventing the following issue while trying to perform a swap ![image](https://github.com/MetaMask/metamask-extension/assets/8658960/077cd50d-7832-498d-b64b-7845171a4ea3) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25488?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/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). - [ ] I confirm that 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: IF <139582705+infiniteflower@users.noreply.github.com> --- shared/lib/swaps-utils.js | 36 ++++++++++++++ shared/lib/swaps-utils.test.js | 48 ++++++++++++++++++- ui/ducks/swaps/swaps.js | 27 +++++------ .../swaps/prepare-swap-page/review-quote.js | 26 ++++------ ui/pages/swaps/view-quote/view-quote.js | 23 ++++----- 5 files changed, 114 insertions(+), 46 deletions(-) diff --git a/shared/lib/swaps-utils.js b/shared/lib/swaps-utils.js index f27f4c9e8a8f..62c71ecbaadb 100644 --- a/shared/lib/swaps-utils.js +++ b/shared/lib/swaps-utils.js @@ -18,6 +18,8 @@ import { addHexPrefix } from '../../app/scripts/lib/util'; import { decimalToHex } from '../modules/conversion.utils'; import fetchWithCache from './fetch-with-cache'; +const FALLBACK_GAS_MULTIPLIER = 1.5; + const TEST_CHAIN_IDS = [CHAIN_IDS.GOERLI, CHAIN_IDS.LOCALHOST]; const clientIdHeader = { 'X-Client-Id': SWAPS_CLIENT_ID }; @@ -325,3 +327,37 @@ export async function fetchTradesInfo( return newQuotes; } + +/** + * Given a gas estimate, gas multiplier, max gas, and custom max gas, returns the max gas limit + * to use for a transaction. + * + * @param {string} gasEstimate - The gas estimate for the transaction. + * @param {number} gasMultiplier - The gas multiplier to use. + * @param {number} maxGas - The max gas limit to use. + * @param {string} customMaxGas - The custom max gas limit to use. + * @returns {string} The max gas limit to use for the transaction. + */ + +export function calculateMaxGasLimit( + gasEstimate, + gasMultiplier = FALLBACK_GAS_MULTIPLIER, + maxGas, + customMaxGas, +) { + const gasLimitForMax = new BigNumber(gasEstimate || 0, 16) + .round(0) + .toString(16); + + const usedGasLimitWithMultiplier = new BigNumber(gasLimitForMax, 16) + .times(gasMultiplier, 10) + .round(0) + .toString(16); + + const nonCustomMaxGasLimit = gasEstimate + ? usedGasLimitWithMultiplier + : `0x${decimalToHex(maxGas || 0)}`; + const maxGasLimit = customMaxGas || nonCustomMaxGasLimit; + + return maxGasLimit; +} diff --git a/shared/lib/swaps-utils.test.js b/shared/lib/swaps-utils.test.js index 5db5fb23085b..4cbc0927e1f3 100644 --- a/shared/lib/swaps-utils.test.js +++ b/shared/lib/swaps-utils.test.js @@ -10,7 +10,11 @@ import { TOKENS, MOCK_TRADE_RESPONSE_2, } from '../../ui/pages/swaps/swaps-util-test-constants'; -import { fetchTradesInfo, shouldEnableDirectWrapping } from './swaps-utils'; +import { + fetchTradesInfo, + shouldEnableDirectWrapping, + calculateMaxGasLimit, +} from './swaps-utils'; jest.mock('./storage-helpers', () => ({ getStorageItem: jest.fn(), @@ -224,4 +228,46 @@ describe('Swaps Utils', () => { expect(shouldEnableDirectWrapping(CHAIN_IDS.MAINNET)).toBe(false); }); }); + + describe('calculateMaxGasLimit', () => { + const gasEstimate = '0x37b15'; + const maxGas = 273740; + let expectedMaxGas = '42d4c'; + let gasMultiplier = 1.2; + let customMaxGas = ''; + + it('should return the max gas limit', () => { + const result = calculateMaxGasLimit( + gasEstimate, + gasMultiplier, + maxGas, + customMaxGas, + ); + expect(result).toStrictEqual(expectedMaxGas); + }); + + it('should return the custom max gas limit', () => { + customMaxGas = '46d4c'; + const result = calculateMaxGasLimit( + gasEstimate, + gasMultiplier, + maxGas, + customMaxGas, + ); + expect(result).toStrictEqual(customMaxGas); + }); + + it('should return the max gas limit with a gas multiplier of 4.5', () => { + gasMultiplier = 4.5; + expectedMaxGas = 'fa9df'; + customMaxGas = ''; + const result = calculateMaxGasLimit( + gasEstimate, + gasMultiplier, + maxGas, + customMaxGas, + ); + expect(result).toStrictEqual(expectedMaxGas); + }); + }); }); diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 56a7273e8d5b..59fadda433d5 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -94,6 +94,7 @@ import { } from '../../../shared/lib/transactions-controller-utils'; import { EtherDenomination } from '../../../shared/constants/common'; import { Numeric } from '../../../shared/modules/Numeric'; +import { calculateMaxGasLimit } from '../../../shared/lib/swaps-utils'; export const GAS_PRICES_LOADING_STATES = { INITIAL: 'INITIAL', @@ -102,8 +103,6 @@ export const GAS_PRICES_LOADING_STATES = { COMPLETED: 'COMPLETED', }; -export const FALLBACK_GAS_MULTIPLIER = 1.5; - const initialState = { aggregatorMetadata: null, approveTxId: null, @@ -1106,20 +1105,16 @@ export const signAndSendTransactions = ( const usedQuote = getUsedQuote(state); const usedTradeTxParams = usedQuote.trade; - const estimatedGasLimit = new BigNumber( - usedQuote?.gasEstimate || 0, - 16, - ).toString(16); - - const maxGasLimit = - customSwapsGas || - (usedQuote?.gasEstimate - ? `0x${estimatedGasLimit}` - : `0x${decimalToHex( - new BigNumber(usedQuote?.maxGas) - .mul(usedQuote?.gasMultiplier || FALLBACK_GAS_MULTIPLIER) - .toString() || 0, - )}`); + const estimatedGasLimit = new BigNumber(usedQuote?.gasEstimate || 0, 16) + .round(0) + .toString(16); + + const maxGasLimit = calculateMaxGasLimit( + usedQuote?.gasEstimate, + usedQuote?.gasMultiplier, + usedQuote?.maxGas, + customSwapsGas, + ); const usedGasPrice = getUsedSwapsGasPrice(state); usedTradeTxParams.gas = maxGasLimit; diff --git a/ui/pages/swaps/prepare-swap-page/review-quote.js b/ui/pages/swaps/prepare-swap-page/review-quote.js index 011ab04bd8d9..5fb2badd8bcb 100644 --- a/ui/pages/swaps/prepare-swap-page/review-quote.js +++ b/ui/pages/swaps/prepare-swap-page/review-quote.js @@ -22,7 +22,6 @@ import { usePrevious } from '../../../hooks/usePrevious'; import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { - FALLBACK_GAS_MULTIPLIER, getQuotes, getSelectedQuote, getApproveTxParams, @@ -135,7 +134,10 @@ import { toPrecisionWithoutTrailingZeros, } from '../../../../shared/lib/transactions-controller-utils'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; +import { + calcTokenValue, + calculateMaxGasLimit, +} from '../../../../shared/lib/swaps-utils'; import { GAS_FEES_LEARN_MORE_URL } from '../../../../shared/lib/ui-utils'; import ExchangeRateDisplay from '../exchange-rate-display'; import InfoTooltip from '../../../components/ui/info-tooltip'; @@ -295,20 +297,12 @@ export default function ReviewQuote({ setReceiveToAmount }) { usedQuote?.gasEstimateWithRefund || `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - const estimatedGasLimit = new BigNumber( - usedQuote?.gasEstimate || 0, - 16, - ).toString(16); - - const nonCustomMaxGasLimit = usedQuote?.gasEstimate - ? `0x${estimatedGasLimit}` - : `0x${decimalToHex( - new BigNumber(usedQuote?.maxGas) - .mul(usedQuote?.gasMultiplier || FALLBACK_GAS_MULTIPLIER) - .toString() || 0, - )}`; - - const maxGasLimit = customMaxGas || nonCustomMaxGasLimit; + const maxGasLimit = calculateMaxGasLimit( + usedQuote?.gasEstimate, + usedQuote?.gasMultiplier, + usedQuote?.maxGas, + customMaxGas, + ); let maxFeePerGas; let maxPriorityFeePerGas; diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 3525cb457f94..f67f727317a1 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -22,7 +22,6 @@ import { useGasFeeInputs } from '../../confirmations/hooks/useGasFeeInputs'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import FeeCard from '../fee-card'; import { - FALLBACK_GAS_MULTIPLIER, getQuotes, getSelectedQuote, getApproveTxParams, @@ -109,7 +108,10 @@ import { toPrecisionWithoutTrailingZeros, } from '../../../../shared/lib/transactions-controller-utils'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; -import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; +import { + calcTokenValue, + calculateMaxGasLimit, +} from '../../../../shared/lib/swaps-utils'; import { addHexes, decGWEIToHexWEI, @@ -230,17 +232,12 @@ export default function ViewQuote() { usedQuote?.gasEstimateWithRefund || `0x${decimalToHex(usedQuote?.averageGas || 0)}`; - const gasLimitForMax = usedQuote?.gasEstimate || `0x0`; - - const usedGasLimitWithMultiplier = new BigNumber(gasLimitForMax, 16) - .times(usedQuote?.gasMultiplier || FALLBACK_GAS_MULTIPLIER, 10) - .round(0) - .toString(16); - - const nonCustomMaxGasLimit = usedQuote?.gasEstimate - ? usedGasLimitWithMultiplier - : `0x${decimalToHex(usedQuote?.maxGas || 0)}`; - const maxGasLimit = customMaxGas || nonCustomMaxGasLimit; + const maxGasLimit = calculateMaxGasLimit( + usedQuote?.gasEstimate, + usedQuote?.gasMultiplier, + usedQuote?.maxGas, + customMaxGas, + ); let maxFeePerGas; let maxPriorityFeePerGas; From f90eed6338ff979a49ad5bd411a6c049c0bcb8a0 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 24 Jun 2024 21:35:14 +0100 Subject: [PATCH 06/22] fix: 25457 firefox extension not work for trezor connect (#25487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #25457 Because firefox doesn't support usb api, which cause the `window.navigator.usb` is undefined in firefox, which break the trezor connection. This PR will check whether window.navigator.usb is undefined or not. if yes, then select-hardware will fall back to use trezor bridge to connect. ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25487?quickstart=1) ## **Related issues** Fixes: #25457 ## **Manual testing steps** 1. Open `metamask` in firefox 2. select `account drop down` in home page and then select `Add Account and hardware wallet` 3. select `trezor` in hardware select screen. 4. approve at trezor screen 5. you should be able to select `accounts` from trezor. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/MetaMask/metamask-extension/assets/7315988/980c6c78-2765-4f97-8781-8250eeec1f9b ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Dan J Miller --- .../connect-hardware/select-hardware.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/pages/create-account/connect-hardware/select-hardware.js b/ui/pages/create-account/connect-hardware/select-hardware.js index 24ea2bcbebdb..8f2e3b8569e3 100644 --- a/ui/pages/create-account/connect-hardware/select-hardware.js +++ b/ui/pages/create-account/connect-hardware/select-hardware.js @@ -56,7 +56,16 @@ export default class SelectHardware extends Component { connect = async () => { if (this.state.selectedDevice) { - if (this.state.selectedDevice === 'trezor') { + // Not all browsers have usb support. In particular, Firefox does + // not support usb. More information on that can be found here: + // https://mozilla.github.io/standards-positions/#webusb + // + // The below `&& window.navigator.usb` condition ensures that we + // only attempt to connect Trezor via usb if we are in a browser + // that supports usb. If not, the connection of the hardware wallet + // to the browser will be handled by the Trezor connect screen. In + // the case of Firefox, this will depend on the Trezor bridge software + if (this.state.selectedDevice === 'trezor' && window.navigator.usb) { this.setState({ trezorRequestDevicePending: true }); try { await window.navigator.usb.requestDevice({ From 2a0e5b23308e6287f6ffa7ed5a20fedf636f4178 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 24 Jun 2024 15:32:57 -0700 Subject: [PATCH 07/22] test: e2e request queue manually switch network (#25271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds e2e spec for testing dapp provider proxy in various scenarios when the feature flag is on and off [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25271?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2627 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Alex Co-authored-by: MetaMask Bot Co-authored-by: Mark Stacey --- .../request-queuing/chain-id-check.spec.js | 103 ----- .../request-queuing/chainid-check.spec.js | 376 +++++++++++++----- .../experimental-tab.component.js | 6 +- 3 files changed, 288 insertions(+), 197 deletions(-) delete mode 100644 test/e2e/tests/request-queuing/chain-id-check.spec.js diff --git a/test/e2e/tests/request-queuing/chain-id-check.spec.js b/test/e2e/tests/request-queuing/chain-id-check.spec.js deleted file mode 100644 index 780254295796..000000000000 --- a/test/e2e/tests/request-queuing/chain-id-check.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -const { strict: assert } = require('assert'); -const FixtureBuilder = require('../../fixture-builder'); -const { - withFixtures, - openDapp, - unlockWallet, - DAPP_URL, - regularDelayMs, - WINDOW_TITLES, - defaultGanacheOptions, - switchToNotificationWindow, -} = require('../../helpers'); -const { PAGES } = require('../../webdriver/driver'); - -describe('Request Queuing', function () { - it('should keep chain id the same with request queuing and switching mm network with a connected site.', async function () { - const port = 8546; - const chainId = 1338; - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withNetworkControllerDoubleGanache() - .withPreferencesControllerUseRequestQueueEnabled() - .withSelectedNetworkControllerPerDomain() - .build(), - ganacheOptions: { - ...defaultGanacheOptions, - concurrent: [ - { - port, - chainId, - ganacheOptions2: defaultGanacheOptions, - }, - ], - }, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - - // Navigate to extension home screen - await driver.navigate(PAGES.HOME); - - // Open Dapp One - await openDapp(driver, undefined, DAPP_URL); - - // 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: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - const request = JSON.stringify({ - method: 'eth_chainId', - }); - - const firstChainIdCall = await driver.executeScript( - `return window.ethereum.request(${request})`, - ); - - assert.equal(firstChainIdCall, '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: 'Localhost 8546', - css: 'p', - }); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - const secondChainIdCall = await driver.executeScript( - `return window.ethereum.request(${request})`, - ); - - assert.equal(secondChainIdCall, '0x539'); // 1337 - }, - ); - }); -}); diff --git a/test/e2e/tests/request-queuing/chainid-check.spec.js b/test/e2e/tests/request-queuing/chainid-check.spec.js index 664cb1117d3c..850051d39c6a 100644 --- a/test/e2e/tests/request-queuing/chainid-check.spec.js +++ b/test/e2e/tests/request-queuing/chainid-check.spec.js @@ -12,123 +12,315 @@ const { } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); -describe('Request-queue chainId proxy sync', function () { - it('should preserve per dapp network selections after connecting 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, - }, - ], +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(), }, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); + 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 chainIdCheckBeforeConnect = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + const chainIdBeforeConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - assert.equal(chainIdCheckBeforeConnect, '0x539'); // 1337 + assert.equal(chainIdBeforeConnect, '0x539'); // 1337 - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); - // Switch to second network - await driver.clickElement({ - text: 'Ethereum Mainnet', - css: 'p', - }); + // Switch to second network + await driver.clickElement({ + text: 'Ethereum Mainnet', + css: 'p', + }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const secondChainIdCheckBeforeConnect = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + const chainIdBeforeConnectAfterManualSwitch = + await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - // before connecting the chainId should change with the wallet - assert.equal(secondChainIdCheckBeforeConnect, '0x1'); + // before connecting the chainId should change with the wallet + assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); - // Connect to dapp - await driver.findClickableElement({ text: 'Connect', tag: 'button' }); - await driver.clickElement('#connectButton'); + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); - await driver.delay(regularDelayMs); + await driver.delay(regularDelayMs); - await switchToNotificationWindow(driver); + await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Next', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - css: '[data-testid="page-container-footer-next"]', - }); + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const firstChainIdCallAfterConnect = await driver.executeScript( - `return window.ethereum.request(${chainIdRequest})`, - ); + const chainIdAfterConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); - assert.equal(firstChainIdCallAfterConnect, '0x1'); + // should still be on the same chainId as the wallet after connecting + assert.equal(chainIdAfterConnect, '0x1'); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); - // Network Selector - await driver.clickElement('[data-testid="network-display"]'); + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); - // Switch to second network - await driver.clickElement({ - text: 'Localhost 8546', - css: 'p', - }); + await switchToNotificationWindow(driver); + await driver.findClickableElements({ + text: 'Switch network', + tag: 'button', + }); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement({ text: 'Switch network', tag: 'button' }); - const secondChainIdCall = 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 - assert.equal(secondChainIdCall, '0x1'); - }, - ); + 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"]'); + + // Switch to second network + await driver.clickElement({ + text: 'Ethereum Mainnet', + css: 'p', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdBeforeConnectAfterManualSwitch = + await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // before connecting the chainId should change with the wallet + assert.equal(chainIdBeforeConnectAfterManualSwitch, '0x1'); + + // Connect to dapp + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await switchToNotificationWindow(driver); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const chainIdAfterConnect = await driver.executeScript( + `return window.ethereum.request(${chainIdRequest})`, + ); + + // should still be on the same chainId as the wallet after connecting + assert.equal(chainIdAfterConnect, '0x1'); + + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x539' }], + }); + + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await switchToNotificationWindow(driver); + await driver.findClickableElements({ + text: 'Switch network', + tag: 'button', + }); + + await driver.clickElement({ text: 'Switch network', 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, + ); + + // 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'); + }, + ); + }); }); }); diff --git a/ui/pages/settings/experimental-tab/experimental-tab.component.js b/ui/pages/settings/experimental-tab/experimental-tab.component.js index 948d062dad5b..fd84fe7a1369 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.component.js +++ b/ui/pages/settings/experimental-tab/experimental-tab.component.js @@ -216,7 +216,6 @@ export default class ExperimentalTab extends PureComponent {
{t('toggleRequestQueueField')} @@ -225,7 +224,10 @@ export default class ExperimentalTab extends PureComponent {
-
+
Date: Tue, 25 Jun 2024 05:24:57 +0530 Subject: [PATCH 08/22] refactor: Replace Typography with Text component in recovery-phrase-chips.js (#25176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR migrates the Typography component to the Text component in the `recovery-phrase-chips.js` file. Devin Run Link: https://preview.devin.ai/devin/e1fe8dd068d745e4ae5e8f5208a2c8f4 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25176?quickstart=1) ## **Related issues** Partially Fixes: https://github.com/MetaMask/metamask-extension/issues/17670 ## **Manual testing steps** 1. Go to the latest build of storybook in this PR 2. Check if the `RecoveryPhraseChips` page is getting rendered correctly ## **Screenshots/Recordings** ### **Before** ![](https://api.devin.ai/attachments/ed953f0e-c174-4777-9019-9e7fed26ffd0/d3227aff-4beb-49a7-9115-670ced64e958.png) ### **After** ![](https://api.devin.ai/attachments/ed953f0e-c174-4777-9019-9e7fed26ffd0/4566ab2f-0100-48ac-8247-5e370b8391f3.png) ## **Pre-merge author checklist** - [X] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../__snapshots__/review-recovery-phrase.test.js.snap | 6 +++--- .../recovery-phrase/recovery-phrase-chips.js | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/review-recovery-phrase.test.js.snap b/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/review-recovery-phrase.test.js.snap index e95f2e73c43b..5e8cc1914b3d 100644 --- a/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/review-recovery-phrase.test.js.snap +++ b/ui/pages/onboarding-flow/recovery-phrase/__snapshots__/review-recovery-phrase.test.js.snap @@ -275,11 +275,11 @@ exports[`Review Recovery Phrase Component should match snapshot 1`] = ` class="far fa-eye" color="white" /> -
Make sure nobody is looking -
+

- {t('makeSureNoOneWatching')} - + )}
From ec5cf7ed9d377471c6fb9b061697913bfe25945f Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:59:31 -0400 Subject: [PATCH 09/22] chore: move keyring snap permissions file (#25491) This change moves the `keyring-snaps-permissions` file into the `snap-keyring` directory. --- .../lib/{ => snap-keyring}/keyring-snaps-permissions.test.ts | 0 app/scripts/lib/{ => snap-keyring}/keyring-snaps-permissions.ts | 0 app/scripts/lib/snap-keyring/utils/isBlockedUrl.ts | 2 +- app/scripts/metamask-controller.js | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename app/scripts/lib/{ => snap-keyring}/keyring-snaps-permissions.test.ts (100%) rename app/scripts/lib/{ => snap-keyring}/keyring-snaps-permissions.ts (100%) diff --git a/app/scripts/lib/keyring-snaps-permissions.test.ts b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts similarity index 100% rename from app/scripts/lib/keyring-snaps-permissions.test.ts rename to app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts diff --git a/app/scripts/lib/keyring-snaps-permissions.ts b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts similarity index 100% rename from app/scripts/lib/keyring-snaps-permissions.ts rename to app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts diff --git a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.ts b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.ts index f196faff0b96..50bc4bfa08eb 100644 --- a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.ts +++ b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.ts @@ -1,5 +1,5 @@ import { PhishingController } from '@metamask/phishing-controller'; -import { isProtocolAllowed } from '../../keyring-snaps-permissions'; +import { isProtocolAllowed } from '../keyring-snaps-permissions'; /** * Checks whether a given URL is blocked due to not using HTTPS or being diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 11463edf3430..799614e73cdb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -248,7 +248,7 @@ import { ///: END:ONLY_INCLUDE_IF import { submitSmartTransactionHook } from './lib/transaction/smart-transactions'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) -import { keyringSnapPermissionsBuilder } from './lib/keyring-snaps-permissions'; +import { keyringSnapPermissionsBuilder } from './lib/snap-keyring/keyring-snaps-permissions'; ///: END:ONLY_INCLUDE_IF import { SnapsNameProvider } from './lib/SnapsNameProvider'; From 130b9b0e2583ab2ecc40cd9c2693399426936a29 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Tue, 25 Jun 2024 08:01:35 +0800 Subject: [PATCH 10/22] feat: add new `useMultichainSelector` (#25423) This change introduces `useMultichainSelector` to pass an additional account argument when using certain multichain selectors. Co-authored-by: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> --- ui/hooks/useMultichainSelector.test.ts | 76 ++++++++++++++++++++++++++ ui/hooks/useMultichainSelector.ts | 16 ++++++ ui/selectors/accounts.test.ts | 2 +- ui/selectors/multichain.test.ts | 35 +++++++++--- ui/selectors/multichain.ts | 75 +++++++++++++++++++------ 5 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 ui/hooks/useMultichainSelector.test.ts create mode 100644 ui/hooks/useMultichainSelector.ts diff --git a/ui/hooks/useMultichainSelector.test.ts b/ui/hooks/useMultichainSelector.test.ts new file mode 100644 index 000000000000..e4469a9f05db --- /dev/null +++ b/ui/hooks/useMultichainSelector.test.ts @@ -0,0 +1,76 @@ +import { InternalAccount } from '@metamask/keyring-api'; +import { createMockInternalAccount } from '../../test/jest/mocks'; +import { renderHookWithProvider } from '../../test/lib/render-helpers'; +import { getSelectedNetworkClientId } from '../selectors'; +import { MultichainState, getMultichainIsEvm } from '../selectors/multichain'; +import { useMultichainSelector } from './useMultichainSelector'; + +const mockAccount = createMockInternalAccount(); +const mockNetworkId = '0x1'; + +const mockState = { + metamask: { + selectedNetworkClientId: mockNetworkId, + completedOnboarding: true, + internalAccounts: { + accounts: { + [mockAccount.id]: mockAccount, + }, + selectedAccount: mockAccount.id, + }, + }, +}; + +const renderUseMultichainHook = ( + selector: (state: MultichainState, account?: InternalAccount) => unknown, + account?: InternalAccount, + state?: MultichainState, +) => { + return renderHookWithProvider( + () => useMultichainSelector(selector, account ?? mockAccount), + state ?? mockState, + ); +}; + +describe('useMultichainSelector', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls useSelector with the correct selector and account', () => { + const mockSelector = jest.fn(); + renderUseMultichainHook(mockSelector, mockAccount); + + expect(mockSelector.mock.calls[0][0]).toMatchObject(mockState); + expect(mockSelector).toHaveBeenCalledWith( + expect.anything(), // already checked above + mockAccount, + ); + }); + + it('calls useSelector with the correct selector and undefined account', () => { + const mockSelector = jest.fn(); + renderUseMultichainHook(mockSelector); + + expect(mockSelector.mock.calls[0][0]).toMatchObject(mockState); + expect(mockSelector).toHaveBeenCalledWith( + expect.anything(), // already checked above + mockAccount, + ); + }); + + it('uses selectedAccount if account is not provided', () => { + const { result } = renderUseMultichainHook(getMultichainIsEvm, null); + + expect(result.current).toBe(true); + }); + + it('is compatible with selectors that do not require an account', () => { + const { result } = renderUseMultichainHook( + getSelectedNetworkClientId, + mockAccount, + ); + + expect(result.current).toBe(mockNetworkId); + }); +}); diff --git a/ui/hooks/useMultichainSelector.ts b/ui/hooks/useMultichainSelector.ts new file mode 100644 index 000000000000..326ac79bf9cd --- /dev/null +++ b/ui/hooks/useMultichainSelector.ts @@ -0,0 +1,16 @@ +import { useSelector, DefaultRootState } from 'react-redux'; +import { InternalAccount } from '@metamask/keyring-api'; +import { getSelectedInternalAccount } from '../selectors'; + +export function useMultichainSelector< + TState = DefaultRootState, + TSelected = unknown, +>( + selector: (state: TState, account?: InternalAccount) => TSelected, + account?: InternalAccount, +) { + return useSelector((state: TState) => { + // We either pass an account or fallback to the currently selected one + return selector(state, account || getSelectedInternalAccount(state)); + }); +} diff --git a/ui/selectors/accounts.test.ts b/ui/selectors/accounts.test.ts index 8f6bc4e14b17..a9dee1605b9b 100644 --- a/ui/selectors/accounts.test.ts +++ b/ui/selectors/accounts.test.ts @@ -40,7 +40,7 @@ describe('Accounts Selectors', () => { }, ); - it('returns false if none account is selected', () => { + it('returns false if no account is selected', () => { const state = MOCK_STATE; state.metamask.internalAccounts.selectedAccount = ''; diff --git a/ui/selectors/multichain.test.ts b/ui/selectors/multichain.test.ts index d811f57798c3..fd87387a22e3 100644 --- a/ui/selectors/multichain.test.ts +++ b/ui/selectors/multichain.test.ts @@ -1,3 +1,4 @@ +import { Cryptocurrency } from '@metamask/assets-controllers'; import { getNativeCurrency } from '../ducks/metamask/metamask'; import { MULTICHAIN_PROVIDER_CONFIGS, @@ -12,6 +13,7 @@ import { import { CHAIN_IDS } from '../../shared/constants/network'; import { AccountsState } from './accounts'; import { + MultichainState, getMultichainCurrentChainId, getMultichainCurrentCurrency, getMultichainDefaultToken, @@ -25,15 +27,16 @@ import { } from './multichain'; import { getCurrentCurrency, getCurrentNetwork, getShouldShowFiat } from '.'; -type TestState = AccountsState & { - metamask: { - preferences: { showFiatInTestnets: boolean }; - providerConfig: { type: string; ticker: string; chainId: string }; - currentCurrency: string; - currencyRates: Record; - completedOnboarding: boolean; +type TestState = MultichainState & + AccountsState & { + metamask: { + preferences: { showFiatInTestnets: boolean }; + providerConfig: { type: string; ticker: string; chainId: string }; + currentCurrency: string; + currencyRates: Record; + completedOnboarding: boolean; + }; }; -}; function getEvmState(): TestState { return { @@ -57,6 +60,22 @@ function getEvmState(): TestState { selectedAccount: MOCK_ACCOUNT_EOA.id, accounts: MOCK_ACCOUNTS, }, + balances: { + [MOCK_ACCOUNT_BIP122_P2WPKH.id]: { + 'bip122:000000000019d6689c085ae165831e93/slip44:0': { + amount: '1.00000000', + unit: 'BTC', + }, + }, + }, + fiatCurrency: 'usd', + cryptocurrencies: [Cryptocurrency.Btc], + rates: { + btc: { + conversionDate: 0, + conversionRate: '100000', + }, + }, }, }; } diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 3e9070b1a3ab..62c607397073 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -1,5 +1,6 @@ -import { isEvmAccountType } from '@metamask/keyring-api'; +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { ProviderConfig } from '@metamask/network-controller'; +import type { RatesControllerState } from '@metamask/assets-controllers'; import { CaipChainId, KnownCaipNamespace, @@ -16,6 +17,7 @@ import { getNativeCurrency, getProviderConfig, } from '../ducks/metamask/metamask'; +import { BalancesControllerState } from '../../app/scripts/lib/accounts/BalancesController'; import { AccountsState } from './accounts'; import { getAllNetworks, @@ -28,12 +30,16 @@ import { getShouldShowFiat, } from '.'; -export type MultichainState = AccountsState & { - metamask: { - // TODO: Use states from new {Rates,Balances,Chain}Controller - }; +export type RatesState = { + metamask: RatesControllerState; +}; + +export type BalancesState = { + metamask: BalancesControllerState; }; +export type MultichainState = AccountsState & RatesState & BalancesState; + export type MultichainNetwork = { nickname: string; isEvmNetwork: boolean; @@ -50,8 +56,9 @@ export function getMultichainNetworkProviders( export function getMultichainNetwork( state: MultichainState, + account?: InternalAccount, ): MultichainNetwork { - const isEvm = getMultichainIsEvm(state); + const isEvm = getMultichainIsEvm(state, account); // EVM networks const evmNetworks: ProviderConfig[] = getAllNetworks(state); @@ -80,7 +87,7 @@ export function getMultichainNetwork( // this as a CAIP-2 namespace and apply our filter with it // For non-EVM, we know we have a selected account, since the logic `isEvm` is based // on having a non-EVM account being selected! - const selectedAccount = getSelectedInternalAccount(state); + const selectedAccount = account ?? getSelectedInternalAccount(state); const nonEvmNetworks = getMultichainNetworkProviders(state); const nonEvmNetwork = nonEvmNetworks.find((provider) => { const { namespace } = parseCaipChainId(provider.chainId); @@ -108,11 +115,14 @@ export function getMultichainNetwork( // a popup (for ethereum related stuffs) is being shown (and uses this function), then the native // currency will be BTC.. -export function getMultichainIsEvm(state: MultichainState) { +export function getMultichainIsEvm( + state: MultichainState, + account?: InternalAccount, +) { const isOnboarded = getCompletedOnboarding(state); // Selected account is not available during onboarding (this is used in // the AppHeader) - const selectedAccount = getMaybeSelectedInternalAccount(state); + const selectedAccount = account ?? getMaybeSelectedInternalAccount(state); // There are no selected account during onboarding. we default to the original EVM behavior. return ( @@ -128,20 +138,27 @@ export function getMultichainIsEvm(state: MultichainState) { * it returns a network, but it actually returns a provider configuration specific to a multichain setup. * * @param state - The redux state. + * @param account - The multichain account. * @returns The current multichain provider configuration. */ -export function getMultichainProviderConfig(state: MultichainState) { - return getMultichainNetwork(state).network; +export function getMultichainProviderConfig( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainNetwork(state, account).network; } export function getMultichainCurrentNetwork(state: MultichainState) { return getMultichainProviderConfig(state); } -export function getMultichainNativeCurrency(state: MultichainState) { - return getMultichainIsEvm(state) +export function getMultichainNativeCurrency( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainIsEvm(state, account) ? getNativeCurrency(state) - : getMultichainProviderConfig(state).ticker; + : getMultichainProviderConfig(state, account).ticker; } export function getMultichainCurrentCurrency(state: MultichainState) { @@ -159,19 +176,33 @@ export function getMultichainCurrentCurrency(state: MultichainState) { : getMultichainProviderConfig(state).ticker; } -export function getMultichainCurrencyImage(state: MultichainState) { - if (getMultichainIsEvm(state)) { +export function getMultichainCurrencyImage( + state: MultichainState, + account?: InternalAccount, +) { + if (getMultichainIsEvm(state, account)) { return getNativeCurrencyImage(state); } const provider = getMultichainProviderConfig( state, + account, ) as MultichainProviderConfig; return provider.rpcPrefs?.imageUrl; } -export function getMultichainShouldShowFiat(state: MultichainState) { - return getMultichainIsEvm(state) +export function getMultichainNativeCurrencyImage( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainCurrencyImage(state, account); +} + +export function getMultichainShouldShowFiat( + state: MultichainState, + account?: InternalAccount, +) { + return getMultichainIsEvm(state, account) ? getShouldShowFiat(state) : // For now we force this for non-EVM true; @@ -199,3 +230,11 @@ export function getMultichainIsMainnet(state: MultichainState) { // update this for other non-EVM networks later! chainId === MultichainNetworks.BITCOIN; } + +export function getMultichainBalances(state: MultichainState) { + return state.metamask.balances; +} + +export const getMultichainCoinRates = (state: MultichainState) => { + return state.metamask.rates; +}; From c9fb2ab36d1dac71e25fb441d950a7be01689750 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 24 Jun 2024 20:32:07 -0400 Subject: [PATCH 11/22] feat: add api spec test infrastructure (#24132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds tests using [api-specs](https://github.com/MetaMask/api-specs) via the [@open-rpc/test-coverage](https://github.com/open-rpc/test-coverage) tool. Check out the [test report](https://output.circle-artifacts.com/output/job/fed0bf65-b864-4c37-a501-0050498dbc50/artifacts/0/html-report/index.html) on CircleCI as an artifact. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24132?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2304 ## **Manual testing steps** 1. run `yarn build:test` 2. run `yarn test:api-specs` 3. see test report ## **Screenshots/Recordings** [Link to video from PI lane demos](https://www.notion.so/metamask-consensys/31-of-May-2024-04770f89aa924ab4b978aee89ce0af8c?pvs=4#46235d960d0e4da7ba7dcccac74f8606) ![image](https://github.com/MetaMask/metamask-extension/assets/364566/fe41484e-c92a-4a32-9113-3e9c0d02df71) ![image](https://github.com/MetaMask/metamask-extension/assets/364566/23a0d201-e572-4b3e-a7f1-e01d6992c5a1) ![image](https://github.com/MetaMask/metamask-extension/assets/364566/6d293610-d4ab-47d5-9eaf-5854203ad9f8) ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot Co-authored-by: jiexi Co-authored-by: Alex Donesky --- .circleci/config.yml | 35 + .gitignore | 4 + package.json | 6 + .../api-specs/ConfirmationRejectionRule.ts | 224 ++++++ test/e2e/api-specs/helpers.ts | 135 ++++ test/e2e/helpers.js | 18 +- test/e2e/run-openrpc-api-test-coverage.ts | 410 +++++++++++ yarn.lock | 654 +++++++++++++++++- 8 files changed, 1460 insertions(+), 26 deletions(-) create mode 100644 test/e2e/api-specs/ConfirmationRejectionRule.ts create mode 100644 test/e2e/api-specs/helpers.ts create mode 100644 test/e2e/run-openrpc-api-test-coverage.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 924193b6fea3..caa86668657b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -200,6 +200,9 @@ workflows: - test-e2e-chrome-rpc: requires: - prep-build-test + - test-api-specs: + requires: + - prep-build-test - test-e2e-chrome-multiple-providers: requires: - prep-build-test @@ -1048,6 +1051,38 @@ jobs: name: depcheck command: yarn depcheck + test-api-specs: + executor: node-browsers-medium-plus + steps: + - run: *shallow-git-clone + - 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 + - gh/install + - run: + name: test:api-specs + command: | + timeout 20m yarn test:api-specs --retries 2 + no_output_timeout: 5m + - run: + name: Comment on PR + command: | + if [ -f html-report/index.html ]; then + gh pr comment "${CIRCLE_PR_NUMBER}" --body ":x: API Spec Test Failed. View the report [here](https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/${CIRCLE_NODE_INDEX}/html-report/index.html)." + else + echo "API Spec Report not found!" + fi + when: on_fail + - store_artifacts: + path: html-report + destination: html-report + test-e2e-chrome: executor: node-browsers-medium-plus parallelism: 20 diff --git a/.gitignore b/.gitignore index d182260bfa8d..4f2d481b8807 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,7 @@ lavamoat/**/policy-debug.json # Attributions licenseInfos.json + +# API Spec tests +html-report/ + diff --git a/package.json b/package.json index 722e82bae00b..523f300806e9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", + "test:api-specs": "SELENIUM_BROWSER=chrome ts-node test/e2e/run-openrpc-api-test-coverage.ts", "test:e2e:mmi:ci": "yarn playwright test", "test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual", "test:e2e:mmi:regular": "yarn playwright test --project=mmi", @@ -448,6 +449,7 @@ "@lavamoat/lavadome-core": "0.0.10", "@lavamoat/lavapack": "^6.1.0", "@lgbot/madge": "^6.2.0", + "@metamask/api-specs": "^0.9.3", "@metamask/auto-changelog": "^2.1.0", "@metamask/build-utils": "^1.0.0", "@metamask/eslint-config": "^9.0.0", @@ -461,6 +463,10 @@ "@metamask/test-bundler": "^1.0.0", "@metamask/test-dapp": "^8.4.0", "@octokit/core": "^3.6.0", + "@open-rpc/meta-schema": "^1.14.6", + "@open-rpc/mock-server": "^1.7.5", + "@open-rpc/schema-utils-js": "^1.16.2", + "@open-rpc/test-coverage": "^2.2.2", "@playwright/test": "^1.39.0", "@sentry/cli": "^2.19.4", "@storybook/addon-a11y": "^7.6.19", diff --git a/test/e2e/api-specs/ConfirmationRejectionRule.ts b/test/e2e/api-specs/ConfirmationRejectionRule.ts new file mode 100644 index 000000000000..9a8348357a69 --- /dev/null +++ b/test/e2e/api-specs/ConfirmationRejectionRule.ts @@ -0,0 +1,224 @@ +import Rule from '@open-rpc/test-coverage/build/rules/rule'; +import { Call } from '@open-rpc/test-coverage/build/coverage'; +import { + ContentDescriptorObject, + ExampleObject, + ExamplePairingObject, + MethodObject, +} from '@open-rpc/meta-schema'; +import paramsToObj from '@open-rpc/test-coverage/build/utils/params-to-obj'; +import { Driver } from '../webdriver/driver'; +import { WINDOW_TITLES, switchToOrOpenDapp } from '../helpers'; +import { addToQueue } from './helpers'; + +type ConfirmationsRejectRuleOptions = { + driver: Driver; + only: string[]; +}; +// this rule makes sure that all confirmation requests are rejected. +// it also validates that the JSON-RPC response is an error with +// error code 4001 (user rejected request) +export class ConfirmationsRejectRule implements Rule { + private driver: Driver; + + private only: string[]; + + private rejectButtonInsteadOfCancel: string[]; + + private requiresEthAccountsPermission: string[]; + + constructor(options: ConfirmationsRejectRuleOptions) { + this.driver = options.driver; + this.only = options.only; + this.rejectButtonInsteadOfCancel = [ + 'personal_sign', + 'eth_signTypedData_v4', + ]; + this.requiresEthAccountsPermission = [ + 'personal_sign', + 'eth_signTypedData_v4', + 'eth_getEncryptionPublicKey', + ]; + } + + getTitle() { + return 'Confirmations Rejection Rule'; + } + + async beforeRequest(_: unknown, call: Call) { + await new Promise((resolve, reject) => { + addToQueue({ + name: 'beforeRequest', + resolve, + reject, + task: async () => { + try { + if (this.requiresEthAccountsPermission.includes(call.methodName)) { + const requestPermissionsRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_requestPermissions', + params: [{ eth_accounts: {} }], + }); + + await this.driver.executeScript( + `window.ethereum.request(${requestPermissionsRequest})`, + ); + const screenshot = await this.driver.driver.takeScreenshot(); + call.attachments = call.attachments || []; + call.attachments.push({ + type: 'image', + data: `data:image/png;base64,${screenshot}`, + }); + + await this.driver.waitUntilXWindowHandles(3); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await this.driver.findClickableElements({ + text: 'Next', + tag: 'button', + }); + + const screenshotTwo = await this.driver.driver.takeScreenshot(); + call.attachments.push({ + type: 'image', + data: `data:image/png;base64,${screenshotTwo}`, + }); + + await this.driver.clickElement({ + text: 'Next', + tag: 'button', + }); + + await this.driver.clickElement({ + text: 'Confirm', + tag: 'button', + }); + + await switchToOrOpenDapp(this.driver); + } + } catch (e) { + console.log(e); + } + }, + }); + }); + } + + async afterRequest(_: unknown, call: Call) { + await new Promise((resolve, reject) => { + addToQueue({ + name: 'afterRequest', + resolve, + reject, + task: async () => { + try { + await this.driver.waitUntilXWindowHandles(3); + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + let text = 'Cancel'; + if (this.rejectButtonInsteadOfCancel.includes(call.methodName)) { + await this.driver.findClickableElements({ + text: 'Reject', + tag: 'button', + }); + text = 'Reject'; + } else { + await this.driver.findClickableElements({ + text: 'Cancel', + tag: 'button', + }); + } + const screenshot = await this.driver.driver.takeScreenshot(); + call.attachments = call.attachments || []; + call.attachments.push({ + type: 'image', + data: `data:image/png;base64,${screenshot}`, + }); + await this.driver.clickElement({ text, tag: 'button' }); + // make sure to switch back to the dapp or else the next test will fail on the wrong window + await switchToOrOpenDapp(this.driver); + } catch (e) { + console.log(e); + } + }, + }); + }); + } + + // get all the confirmation calls to make and expect to pass + getCalls(_: unknown, method: MethodObject) { + const calls: Call[] = []; + const isMethodAllowed = this.only ? this.only.includes(method.name) : true; + if (isMethodAllowed) { + if (method.examples) { + // pull the first example + const e = method.examples[0]; + const ex = e as ExamplePairingObject; + + if (!ex.result) { + return calls; + } + const p = ex.params.map((_e) => (_e as ExampleObject).value); + const params = + method.paramStructure === 'by-name' + ? paramsToObj(p, method.params as ContentDescriptorObject[]) + : p; + calls.push({ + title: `${this.getTitle()} - with example ${ex.name}`, + methodName: method.name, + params, + url: '', + resultSchema: (method.result as ContentDescriptorObject).schema, + expectedResult: (ex.result as ExampleObject).value, + }); + } else { + // naively call the method with no params + calls.push({ + title: `${method.name} > confirmation rejection`, + methodName: method.name, + params: [], + url: '', + resultSchema: (method.result as ContentDescriptorObject).schema, + }); + } + } + return calls; + } + + async afterResponse(_: unknown, call: Call) { + await new Promise((resolve, reject) => { + addToQueue({ + name: 'afterResponse', + resolve, + reject, + task: async () => { + try { + if (this.requiresEthAccountsPermission.includes(call.methodName)) { + const revokePermissionsRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_revokePermissions', + params: [{ eth_accounts: {} }], + }); + + await this.driver.executeScript( + `window.ethereum.request(${revokePermissionsRequest})`, + ); + } + } catch (e) { + console.log(e); + } + }, + }); + }); + } + + validateCall(call: Call) { + if (call.error) { + call.valid = call.error.code === 4001; + if (!call.valid) { + call.reason = `Expected error code 4001, got ${call.error.code}`; + } + } + return call; + } +} diff --git a/test/e2e/api-specs/helpers.ts b/test/e2e/api-specs/helpers.ts new file mode 100644 index 000000000000..d4f4d3be22c5 --- /dev/null +++ b/test/e2e/api-specs/helpers.ts @@ -0,0 +1,135 @@ +import { v4 as uuid } from 'uuid'; +import { ErrorObject } from '@open-rpc/meta-schema'; +import { Driver } from '../webdriver/driver'; + +// eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/no-explicit-any +declare let window: any; + +type QueueItem = { + task: () => Promise; + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; + name: string; +}; + +export const taskQueue: QueueItem[] = []; +let isProcessing = false; + +export const processQueue = async () => { + if (isProcessing || taskQueue.length === 0) { + return; + } + + isProcessing = true; + const item = taskQueue.shift(); + if (!item) { + return; + } + const { task, resolve, reject }: QueueItem | undefined = item; + try { + const result = await task(); + resolve(result); + } catch (error) { + reject(error); + } finally { + isProcessing = false; + await processQueue(); + } +}; + +export const addToQueue = ({ task, resolve, reject, name }: QueueItem) => { + taskQueue.push({ task, resolve, reject, name }); + return processQueue(); +}; + +export const pollForResult = async ( + driver: Driver, + generatedKey: string, +): Promise => { + let result; + // eslint-disable-next-line no-loop-func + await new Promise((resolve, reject) => { + addToQueue({ + name: 'pollResult', + resolve, + reject, + task: async () => { + result = await driver.executeScript( + `return window['${generatedKey}'];`, + ); + + while (result === null) { + // Continue polling if result is not set + await driver.delay(500); + result = await driver.executeScript( + `return window['${generatedKey}'];`, + ); + } + + // clear the result + await driver.executeScript(`delete window['${generatedKey}'];`); + + return result; + }, + }); + }); + if (result !== undefined) { + return result; + } + return pollForResult(driver, generatedKey); +}; + +export const createDriverTransport = (driver: Driver) => { + return async ( + _: string, + method: string, + params: unknown[] | Record, + ) => { + const generatedKey = uuid(); + return new Promise((resolve, reject) => { + const execute = async () => { + await addToQueue({ + name: 'transport', + resolve, + reject, + task: async () => { + // don't wait for executeScript to finish window.ethereum promise + // we need this because if we wait for the promise to resolve it + // will hang in selenium since it can only do one thing at a time. + // the workaround is to put the response on window.asyncResult and poll for it. + driver.executeScript( + ([m, p, g]: [ + string, + unknown[] | Record, + string, + ]) => { + window[g] = null; + window.ethereum + .request({ method: m, params: p }) + .then((r: unknown) => { + window[g] = { result: r }; + }) + .catch((e: ErrorObject) => { + window[g] = { + error: { + code: e.code, + message: e.message, + data: e.data, + }, + }; + }); + }, + method, + params, + generatedKey, + ); + }, + }); + }; + return execute(); + }).then(async () => { + const response = await pollForResult(driver, generatedKey); + return response; + }); + }; +}; diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index f3ab5f59d3c0..41951f973aa9 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -44,6 +44,7 @@ async function withFixtures(options, testSuite) { title, ignoredConsoleErrors = [], dappPath = undefined, + disableGanache, dappPaths, testSpecificMock = function () { // do nothing. @@ -54,7 +55,10 @@ async function withFixtures(options, testSuite) { } = options; const fixtureServer = new FixtureServer(); - const ganacheServer = new Ganache(); + let ganacheServer; + if (!disableGanache) { + ganacheServer = new Ganache(); + } const bundlerServer = new Bundler(); const https = await mockttp.generateCACertificate(); const mockServer = mockttp.getLocal({ https, cors: true }); @@ -67,10 +71,12 @@ async function withFixtures(options, testSuite) { let driver; let failed = false; try { - await ganacheServer.start(ganacheOptions); + if (!disableGanache) { + await ganacheServer.start(ganacheOptions); + } let contractRegistry; - if (smartContract) { + if (smartContract && !disableGanache) { const ganacheSeeder = new GanacheSeeder(ganacheServer.getProvider()); const contracts = smartContract instanceof Array ? smartContract : [smartContract]; @@ -97,7 +103,7 @@ async function withFixtures(options, testSuite) { }); } - if (useBundler) { + if (!disableGanache && useBundler) { await initBundler(bundlerServer, ganacheServer, usePaymaster); } @@ -263,7 +269,9 @@ async function withFixtures(options, testSuite) { } finally { if (!failed || process.env.E2E_LEAVE_RUNNING !== 'true') { await fixtureServer.stop(); - await ganacheServer.quit(); + if (ganacheServer) { + await ganacheServer.quit(); + } if (ganacheOptions?.concurrent) { secondaryGanacheServer.forEach(async (server) => { diff --git a/test/e2e/run-openrpc-api-test-coverage.ts b/test/e2e/run-openrpc-api-test-coverage.ts new file mode 100644 index 000000000000..f192f6088954 --- /dev/null +++ b/test/e2e/run-openrpc-api-test-coverage.ts @@ -0,0 +1,410 @@ +import testCoverage from '@open-rpc/test-coverage'; +import { parseOpenRPCDocument } from '@open-rpc/schema-utils-js'; +import HtmlReporter from '@open-rpc/test-coverage/build/reporters/html-reporter'; +import ExamplesRule from '@open-rpc/test-coverage/build/rules/examples-rule'; +import JsonSchemaFakerRule from '@open-rpc/test-coverage/build/rules/json-schema-faker-rule'; + +import { + ExampleObject, + ExamplePairingObject, + MethodObject, +} from '@open-rpc/meta-schema'; +import openrpcDocument from '@metamask/api-specs'; +import { ConfirmationsRejectRule } from './api-specs/ConfirmationRejectionRule'; + +import { Driver, PAGES } from './webdriver/driver'; + +import { createDriverTransport } from './api-specs/helpers'; + +import FixtureBuilder from './fixture-builder'; +import { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + ACCOUNT_1, +} from './helpers'; + +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires +const mockServer = require('@open-rpc/mock-server/build/index').default; + +async function main() { + const port = 8545; + const chainId = 1337; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder().build(), + disableGanache: true, + title: 'api-specs coverage', + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + // Navigate to extension home screen + await driver.navigate(PAGES.HOME); + + // Open Dapp + await openDapp(driver, undefined, DAPP_URL); + + const transport = createDriverTransport(driver); + + const transaction = + openrpcDocument.components?.schemas?.TransactionInfo?.allOf?.[0]; + + if (transaction) { + delete transaction.unevaluatedProperties; + } + + const chainIdMethod = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_chainId', + ); + (chainIdMethod as MethodObject).examples = [ + { + name: 'chainIdExample', + description: 'Example of a chainId request', + params: [], + result: { + name: 'chainIdResult', + value: `0x${chainId.toString(16)}`, + }, + }, + ]; + + const getBalanceMethod = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_getBalance', + ); + + (getBalanceMethod as MethodObject).examples = [ + { + name: 'getBalanceExample', + description: 'Example of a getBalance request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + { + name: 'tag', + value: 'latest', + }, + ], + result: { + name: 'getBalanceResult', + value: '0x1a8819e0c9bab700', // can we get this from a variable too + }, + }, + ]; + + const blockNumber = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_blockNumber', + ); + + (blockNumber as MethodObject).examples = [ + { + name: 'blockNumberExample', + description: 'Example of a blockNumber request', + params: [], + result: { + name: 'blockNumberResult', + value: '0x1', + }, + }, + ]; + + const personalSign = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'personal_sign', + ); + + (personalSign as MethodObject).examples = [ + { + name: 'personalSignExample', + description: 'Example of a personalSign request', + params: [ + { + name: 'data', + value: '0xdeadbeef', + }, + { + name: 'address', + value: ACCOUNT_1, + }, + ], + result: { + name: 'personalSignResult', + value: '0x1a8819e0c9bab700', + }, + }, + ]; + + const switchEthereumChain = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'wallet_switchEthereumChain', + ); + (switchEthereumChain as MethodObject).examples = [ + { + name: 'wallet_switchEthereumChain', + description: + 'Example of a wallet_switchEthereumChain request to sepolia', + params: [ + { + name: 'SwitchEthereumChainParameter', + value: { + chainId: '0xaa36a7', + }, + }, + ], + result: { + name: 'wallet_switchEthereumChain', + value: null, + }, + }, + ]; + + const signTypedData4 = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_signTypedData_v4', + ); + + const signTypedData4Example = (signTypedData4 as MethodObject) + .examples?.[0] as ExamplePairingObject; + + // just update address for signTypedData + (signTypedData4Example.params[0] as ExampleObject).value = ACCOUNT_1; + + // update chainId for signTypedData + ( + signTypedData4Example.params[1] as ExampleObject + ).value.domain.chainId = 1337; + + // net_version missing from execution-apis. see here: https://github.com/ethereum/execution-apis/issues/540 + const netVersion: MethodObject = { + name: 'net_version', + summary: 'Returns the current network ID.', + params: [], + result: { + description: 'Returns the current network ID.', + name: 'net_version', + schema: { + type: 'string', + }, + }, + description: 'Returns the current network ID.', + examples: [ + { + name: 'net_version', + description: 'Example of a net_version request', + params: [], + result: { + name: 'net_version', + description: 'The current network ID', + value: '0x1', + }, + }, + ], + }; + // add net_version + (openrpcDocument.methods as MethodObject[]).push( + netVersion as unknown as MethodObject, + ); + + const getEncryptionPublicKey = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_getEncryptionPublicKey', + ); + + (getEncryptionPublicKey as MethodObject).examples = [ + { + name: 'getEncryptionPublicKeyExample', + description: 'Example of a getEncryptionPublicKey request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + ], + result: { + name: 'getEncryptionPublicKeyResult', + value: '0x1a8819e0c9bab700', + }, + }, + ]; + + const getTransactionCount = openrpcDocument.methods.find( + (m) => (m as MethodObject).name === 'eth_getTransactionCount', + ); + (getTransactionCount as MethodObject).examples = [ + { + name: 'getTransactionCountExampleEarliest', + description: 'Example of a pending getTransactionCount request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + { + name: 'tag', + value: 'earliest', + }, + ], + result: { + name: 'getTransactionCountResult', + value: '0x0', + }, + }, + { + name: 'getTransactionCountExampleFinalized', + description: 'Example of a pending getTransactionCount request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + { + name: 'tag', + value: 'finalized', + }, + ], + result: { + name: 'getTransactionCountResult', + value: '0x0', + }, + }, + { + name: 'getTransactionCountExampleSafe', + description: 'Example of a pending getTransactionCount request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + { + name: 'tag', + value: 'safe', + }, + ], + result: { + name: 'getTransactionCountResult', + value: '0x0', + }, + }, + { + name: 'getTransactionCountExample', + description: 'Example of a getTransactionCount request', + params: [ + { + name: 'address', + value: ACCOUNT_1, + }, + { + name: 'tag', + value: 'latest', + }, + ], + result: { + name: 'getTransactionCountResult', + value: '0x0', + }, + }, + // returns a number right now. see here: https://github.com/MetaMask/metamask-extension/pull/14822 + // { + // name: 'getTransactionCountExamplePending', + // description: 'Example of a pending getTransactionCount request', + // params: [ + // { + // name: 'address', + // value: ACCOUNT_1, + // }, + // { + // name: 'tag', + // value: 'pending', + // }, + // ], + // result: { + // name: 'getTransactionCountResult', + // value: '0x0', + // }, + // }, + ]; + + const server = mockServer(port, openrpcDocument); + server.start(); + + // TODO: move these to a "Confirmation" tag in api-specs + const methodsWithConfirmations = [ + 'wallet_requestPermissions', + 'eth_requestAccounts', + 'wallet_watchAsset', + 'personal_sign', // requires permissions for eth_accounts + 'wallet_addEthereumChain', + 'eth_signTypedData_v4', // requires permissions for eth_accounts + 'wallet_switchEthereumChain', + + // commented out because its not returning 4001 error. + // see here https://github.com/MetaMask/metamask-extension/issues/24227 + // 'eth_getEncryptionPublicKey', // requires permissions for eth_accounts + ]; + const filteredMethods = openrpcDocument.methods + .filter((_m: unknown) => { + const m = _m as MethodObject; + return ( + m.name.includes('snap') || + m.name.includes('Snap') || + m.name.toLowerCase().includes('account') || + m.name.includes('crypt') || + m.name.includes('blob') || + m.name.includes('sendTransaction') || + m.name.startsWith('wallet_scanQRCode') || + methodsWithConfirmations.includes(m.name) || + // filters are currently 0 prefixed for odd length on + // extension which doesn't pass spec + // see here: https://github.com/MetaMask/eth-json-rpc-filters/issues/152 + m.name.includes('filter') || + m.name.includes('Filter') + ); + }) + .map((m) => (m as MethodObject).name); + + const testCoverageResults = await testCoverage({ + openrpcDocument: (await parseOpenRPCDocument( + openrpcDocument as never, + )) as never, + transport, + reporters: [ + 'console-streaming', + new HtmlReporter({ autoOpen: !process.env.CI }), + ], + skip: [ + 'eth_coinbase', + // these 2 methods below are not supported by MetaMask extension yet and + // don't get passed through. See here: https://github.com/MetaMask/metamask-extension/issues/24225 + 'eth_getBlockReceipts', + 'eth_maxPriorityFeePerGas', + ], + rules: [ + new JsonSchemaFakerRule({ + only: [], + skip: filteredMethods, + numCalls: 2, + }), + new ExamplesRule({ + only: [], + skip: filteredMethods, + }), + new ConfirmationsRejectRule({ + driver, + only: methodsWithConfirmations, + }), + ], + }); + + await driver.quit(); + + // if any of the tests failed, exit with a non-zero code + if (testCoverageResults.every((r) => r.valid)) { + process.exit(0); + } else { + process.exit(1); + } + }, + ); +} + +main(); diff --git a/yarn.lock b/yarn.lock index c67266bb695b..ea82d4dee42f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1609,7 +1609,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5": +"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.5": version: 7.24.6 resolution: "@babel/runtime@npm:7.24.6" dependencies: @@ -3569,6 +3569,20 @@ __metadata: languageName: node linkType: hard +"@floating-ui/react@npm:^0.26.9": + version: 0.26.12 + resolution: "@floating-ui/react@npm:0.26.12" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@floating-ui/utils": "npm:^0.2.0" + tabbable: "npm:^6.0.0" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/da77f6b99ed0c8d5169f0ed287304615bef7c66b7a0011e4425e843016f6450a928bc27310a861fb14f8a1e58ef11fbdd92550583440f11af5d1a905968453a6 + languageName: node + linkType: hard + "@floating-ui/utils@npm:^0.1.3": version: 0.1.6 resolution: "@floating-ui/utils@npm:0.1.6" @@ -3576,6 +3590,13 @@ __metadata: languageName: node linkType: hard +"@floating-ui/utils@npm:^0.2.0": + version: 0.2.1 + resolution: "@floating-ui/utils@npm:0.2.1" + checksum: 10/33c9ab346e7b05c5a1e6a95bc902aafcfc2c9d513a147e2491468843bd5607531b06d0b9aa56aa491cbf22a6c2495c18ccfc4c0344baec54a689a7bb8e4898d6 + languageName: node + linkType: hard + "@fluent/syntax@npm:0.19.0": version: 0.19.0 resolution: "@fluent/syntax@npm:0.19.0" @@ -4119,6 +4140,87 @@ __metadata: languageName: node linkType: hard +"@json-schema-spec/json-pointer@npm:^0.1.2": + version: 0.1.2 + resolution: "@json-schema-spec/json-pointer@npm:0.1.2" + checksum: 10/2a691ffc11f1a266ca4d0c9e2c99791679d580f343ef69746fad623d1abcf4953adde987890e41f906767d7729604c0182341e9012388b73a44d5b21fb296453 + languageName: node + linkType: hard + +"@json-schema-tools/dereferencer@npm:1.5.1": + version: 1.5.1 + resolution: "@json-schema-tools/dereferencer@npm:1.5.1" + dependencies: + "@json-schema-tools/reference-resolver": "npm:^1.2.1" + "@json-schema-tools/traverse": "npm:^1.7.5" + fast-safe-stringify: "npm:^2.0.7" + checksum: 10/1852b6249916014ea0554c993d15fe2a32bfbbfe160d0effc720d41a5252c7cdbd7c37f77f2728c085d9024b3cd077bb7f7e774b8fb88ac1eb3359e069177b02 + languageName: node + linkType: hard + +"@json-schema-tools/dereferencer@npm:1.5.4": + version: 1.5.4 + resolution: "@json-schema-tools/dereferencer@npm:1.5.4" + dependencies: + "@json-schema-tools/reference-resolver": "npm:^1.2.4" + "@json-schema-tools/traverse": "npm:^1.7.8" + fast-safe-stringify: "npm:^2.0.7" + checksum: 10/2dc01d1d9208cc63af81b680dcd0e46716fe60123c38fc37051041fc2d704ce5f71554fdfbd0cb96845ff2f44c6108ac16d710fcf7cd51ffbe08812b1bee8779 + languageName: node + linkType: hard + +"@json-schema-tools/dereferencer@npm:1.5.5": + version: 1.5.5 + resolution: "@json-schema-tools/dereferencer@npm:1.5.5" + dependencies: + "@json-schema-tools/reference-resolver": "npm:^1.2.4" + "@json-schema-tools/traverse": "npm:^1.7.8" + fast-safe-stringify: "npm:^2.0.7" + checksum: 10/25d2ebb2741d5deee21570c39cb09e49911ac58712dc18741772de684f5c4f74f7c322f038e2e9b9091c2c2687a46dd08f3945f629faa7434296ada8ec9cc8d2 + languageName: node + linkType: hard + +"@json-schema-tools/meta-schema@npm:1.6.19": + version: 1.6.19 + resolution: "@json-schema-tools/meta-schema@npm:1.6.19" + checksum: 10/29e4fec73dc4ada7b451f6eab1251827158e619ae559c3c5b33c15d90b311812b08b933a9498261ebd7f7e88ce726786a611170e817b7167987db1e26955ddcb + languageName: node + linkType: hard + +"@json-schema-tools/meta-schema@npm:^1.6.10": + version: 1.7.4 + resolution: "@json-schema-tools/meta-schema@npm:1.7.4" + checksum: 10/6a688260eaac550d372325a39e7d4f44db7904a3fcaa3d3e0bf318b259007326592b53e511025ff35010ba0e0314dba338fd169338c5ea090328663f3e7cbd46 + languageName: node + linkType: hard + +"@json-schema-tools/reference-resolver@npm:1.2.4": + version: 1.2.4 + resolution: "@json-schema-tools/reference-resolver@npm:1.2.4" + dependencies: + "@json-schema-spec/json-pointer": "npm:^0.1.2" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/1ad98d011e5aad72000112215615715593a0a244ca82dbf6008cc93bfcd14ef99a0796ab4e808faee083dc13182dc9ab2d01ca5db4f44ca880f45de2f5ea2437 + languageName: node + linkType: hard + +"@json-schema-tools/reference-resolver@npm:^1.2.1, @json-schema-tools/reference-resolver@npm:^1.2.4": + version: 1.2.5 + resolution: "@json-schema-tools/reference-resolver@npm:1.2.5" + dependencies: + "@json-schema-spec/json-pointer": "npm:^0.1.2" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/0f48098ea6df853a56fc7c758974eee4c5b7e3979123f49f52929c82a1eb263c7d0154efc6671325920d670494b05cae4d4625c6204023b4b1fed6e5f93ccb96 + languageName: node + linkType: hard + +"@json-schema-tools/traverse@npm:^1.7.5, @json-schema-tools/traverse@npm:^1.7.8": + version: 1.10.3 + resolution: "@json-schema-tools/traverse@npm:1.10.3" + checksum: 10/690623740d223ea373d8e561dad5c70bf86461bcedc5fc45da01c87bcdf3284bbdbad3006d4a423f8d82e4b2d4580e45f92c0b272f006024fb597d7f01876215 + languageName: node + linkType: hard + "@juggle/resize-observer@npm:^3.3.1": version: 3.4.0 resolution: "@juggle/resize-observer@npm:3.4.0" @@ -4344,6 +4446,33 @@ __metadata: languageName: node linkType: hard +"@mantine/core@npm:^7.8.0": + version: 7.8.0 + resolution: "@mantine/core@npm:7.8.0" + dependencies: + "@floating-ui/react": "npm:^0.26.9" + clsx: "npm:2.1.0" + react-number-format: "npm:^5.3.1" + react-remove-scroll: "npm:^2.5.7" + react-textarea-autosize: "npm:8.5.3" + type-fest: "npm:^4.12.0" + peerDependencies: + "@mantine/hooks": 7.8.0 + react: ^18.2.0 + react-dom: ^18.2.0 + checksum: 10/9d3ba53cd41a7b725579dec3723ee92444b45c51a0ed7a283caad047c1a9d3d4c8edf2cf143c4abf8959f3ade62cc7cdcb552fe1f4bfc1ef0f6a6d6136ce958f + languageName: node + linkType: hard + +"@mantine/hooks@npm:^7.8.0": + version: 7.8.0 + resolution: "@mantine/hooks@npm:7.8.0" + peerDependencies: + react: ^18.2.0 + checksum: 10/723d963995076574842ed3296a9acc63fd4a279d265a80edfe7f32c0359f70821e86dc5da5c37c13ce4ce8edfcbbf2c0c32a3024c60a7167172034cf6ddac220 + languageName: node + linkType: hard + "@material-ui/core@npm:^4.11.0": version: 4.11.0 resolution: "@material-ui/core@npm:4.11.0" @@ -4672,6 +4801,13 @@ __metadata: languageName: node linkType: hard +"@metamask/api-specs@npm:^0.9.3": + version: 0.9.3 + resolution: "@metamask/api-specs@npm:0.9.3" + checksum: 10/803852ba43a0fbabb43aeba2ca63e43d22a99d35710700aa04c92cc85184c93024b052b2ee43831762341848de42d172c99485fa7b659249e75255ff8d29d0b2 + languageName: node + linkType: hard + "@metamask/approval-controller@npm:^6.0.1, @metamask/approval-controller@npm:^6.0.2": version: 6.0.2 resolution: "@metamask/approval-controller@npm:6.0.2" @@ -6894,6 +7030,147 @@ __metadata: languageName: node linkType: hard +"@open-rpc/examples@npm:^1.6.1": + version: 1.7.0 + resolution: "@open-rpc/examples@npm:1.7.0" + checksum: 10/b1c9730965051971f049a8728434e7ec2cd1c45546861dfd207fa3c915aae12db9ada5242d78c86ed8ea3d6c3f8c49dbac3a28155a9af1cfee0344817847b73e + languageName: node + linkType: hard + +"@open-rpc/html-reporter-react@npm:^0.0.4": + version: 0.0.4 + resolution: "@open-rpc/html-reporter-react@npm:0.0.4" + dependencies: + "@mantine/core": "npm:^7.8.0" + "@mantine/hooks": "npm:^7.8.0" + "@tabler/icons-react": "npm:^3.2.0" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" + react18-json-view: "npm:^0.2.8" + wouter: "npm:^3.1.2" + checksum: 10/4576f7217405eae19c832b1f76450734f05b38ef586560baa391e12e2ba36477cbebb84ea456d17eabe12b77d5e43269cbbe8201fe384e92ec815947d787d05f + languageName: node + linkType: hard + +"@open-rpc/meta-schema@npm:1.14.2": + version: 1.14.2 + resolution: "@open-rpc/meta-schema@npm:1.14.2" + checksum: 10/8689d447b7a7e9a01cb3479b086565cd16b29fbc04a7c867294264d03d2ada6d6b291e22f520742608509135d09d6b1efb6bf929e5a82943b298594d7e3e9650 + languageName: node + linkType: hard + +"@open-rpc/meta-schema@npm:^1.14.6": + version: 1.14.6 + resolution: "@open-rpc/meta-schema@npm:1.14.6" + checksum: 10/7cb672ea42c143c3fcb177ad04b935d56c38cb28fc7ede0a0bb50293e0e49dee81046c2d43bc57c8bbf9efbbb76356d60b4a8e408a03ecc8fa5952ef3e342316 + languageName: node + linkType: hard + +"@open-rpc/mock-server@npm:^1.7.5": + version: 1.7.5 + resolution: "@open-rpc/mock-server@npm:1.7.5" + dependencies: + "@open-rpc/examples": "npm:^1.6.1" + "@open-rpc/schema-utils-js": "npm:1.16.1" + "@open-rpc/server-js": "npm:1.9.3" + commander: "npm:^6.1.0" + lodash: "npm:^4.17.19" + bin: + open-rpc-mock-server: build/cli.js + checksum: 10/f80d3d51b4aeca498c38eca21cbe85e1840c4d5709d3d71781eac54c2ad79cd2c18da81c92c561baefd00f822d9d1f9c198c0e9e3cb2ffa9e2587f93ee0f28a3 + languageName: node + linkType: hard + +"@open-rpc/schema-utils-js@npm:1.15.0": + version: 1.15.0 + resolution: "@open-rpc/schema-utils-js@npm:1.15.0" + dependencies: + "@json-schema-tools/dereferencer": "npm:1.5.1" + "@json-schema-tools/meta-schema": "npm:^1.6.10" + "@json-schema-tools/reference-resolver": "npm:^1.2.1" + "@open-rpc/meta-schema": "npm:1.14.2" + ajv: "npm:^6.10.0" + detect-node: "npm:^2.0.4" + fast-safe-stringify: "npm:^2.0.7" + fs-extra: "npm:^9.0.0" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/88a9d27cdf77af13890f763c2aef660c15cc6a274268ae92b626ed7dab1cb4d1bf1bfc8470c49ef97c4cf40fb8e53aa4c95c27d17de5dfdbbffa0847bf79e3a9 + languageName: node + linkType: hard + +"@open-rpc/schema-utils-js@npm:1.16.1": + version: 1.16.1 + resolution: "@open-rpc/schema-utils-js@npm:1.16.1" + dependencies: + "@json-schema-tools/dereferencer": "npm:1.5.4" + "@json-schema-tools/meta-schema": "npm:1.6.19" + "@json-schema-tools/reference-resolver": "npm:1.2.4" + "@open-rpc/meta-schema": "npm:1.14.2" + ajv: "npm:^6.10.0" + detect-node: "npm:^2.0.4" + fast-safe-stringify: "npm:^2.0.7" + fs-extra: "npm:^9.0.0" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/090f77ea88cb6b292631e6635659127eff0b975e8985f67d2e7d5364c3e0ea847f202fede814b3ef6fd6b4257adc20673ce0aa76b4e914d06cb758c8330e388d + languageName: node + linkType: hard + +"@open-rpc/schema-utils-js@npm:^1.16.2": + version: 1.16.2 + resolution: "@open-rpc/schema-utils-js@npm:1.16.2" + dependencies: + "@json-schema-tools/dereferencer": "npm:1.5.5" + "@json-schema-tools/meta-schema": "npm:1.6.19" + "@json-schema-tools/reference-resolver": "npm:1.2.4" + "@open-rpc/meta-schema": "npm:1.14.2" + ajv: "npm:^6.10.0" + detect-node: "npm:^2.0.4" + fast-safe-stringify: "npm:^2.0.7" + fs-extra: "npm:^10.1.0" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/4ba6b8c5606e3910f1b54f8b1519c4f4c78b7d2dfda023ef709ff59d9294f6cb46bf88093aa6159823173f5ba9183ee2b4e4dee8988fac7d5a7eb806b9cdae4a + languageName: node + linkType: hard + +"@open-rpc/server-js@npm:1.9.3": + version: 1.9.3 + resolution: "@open-rpc/server-js@npm:1.9.3" + dependencies: + "@open-rpc/schema-utils-js": "npm:1.15.0" + body-parser: "npm:^1.19.0" + connect: "npm:^3.7.0" + cors: "npm:^2.8.5" + json-schema-faker: "npm:^0.5.0-rcv.26" + lodash: "npm:^4.17.19" + node-ipc: "npm:9.1.1" + ws: "npm:^8.0.0" + checksum: 10/27005f6a782f99892989a5e6df8d7d5ce00aa83f09f009d5826a19fe3d8c2d65ea51613a0d10ae4f798774279c4b5dd204aa5cf57eb657d7c74c9bb3a317d77e + languageName: node + linkType: hard + +"@open-rpc/test-coverage@npm:^2.2.2": + version: 2.2.2 + resolution: "@open-rpc/test-coverage@npm:2.2.2" + dependencies: + "@open-rpc/html-reporter-react": "npm:^0.0.4" + "@open-rpc/schema-utils-js": "npm:^1.16.2" + "@types/isomorphic-fetch": "npm:0.0.35" + "@types/lodash": "npm:^4.14.162" + ajv: "npm:^7.0.0" + colors: "npm:^1.3.3" + commander: "npm:^8.0.0" + isomorphic-fetch: "npm:^3.0.0" + json-schema-faker: "npm:^0.5.0-rcv.29" + lodash: "npm:^4.17.20" + bin: + open-rpc-test-coverage: bin/cli.js + checksum: 10/fc764031d8395dca73187684143f07cd2f6be854bedbd943b086e46f94e5c4207942bf87f1d4ac66f4220f209d6d4a7d50b0eb70d4586e2d07a4e086f0e344b1 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -9104,6 +9381,24 @@ __metadata: languageName: node linkType: hard +"@tabler/icons-react@npm:^3.2.0": + version: 3.2.0 + resolution: "@tabler/icons-react@npm:3.2.0" + dependencies: + "@tabler/icons": "npm:3.2.0" + peerDependencies: + react: ">= 16" + checksum: 10/743349a5c7d8cff1d55a101147c2fedc19dc844d0ec9f3de367383382ee314c444166cf137aca0154a7eaf8bcb4f3398a89bda9eb72a04f8c3e6ceb8dc582492 + languageName: node + linkType: hard + +"@tabler/icons@npm:3.2.0": + version: 3.2.0 + resolution: "@tabler/icons@npm:3.2.0" + checksum: 10/4fe4de5ad3cdb54d7c711523dc6e1fc43605bca3fcfc777277da1713184ed5f9034010435c09a5bf928d6774cd643297483394a183aead42eb8f8a6626b70bcc + languageName: node + linkType: hard + "@testing-library/dom@npm:^7.17.1": version: 7.22.2 resolution: "@testing-library/dom@npm:7.22.2" @@ -9992,6 +10287,13 @@ __metadata: languageName: node linkType: hard +"@types/isomorphic-fetch@npm:0.0.35": + version: 0.0.35 + resolution: "@types/isomorphic-fetch@npm:0.0.35" + checksum: 10/cff2d6eb7cf69dc77b88183f029b18007847a86f2b5f02a59f1c7444771d97443b8e010fec24860c0454a363bcb81afeeaacf7094a31ef337115f920285ac7cf + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.3 resolution: "@types/istanbul-lib-coverage@npm:2.0.3" @@ -10089,10 +10391,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.14.136, @types/lodash@npm:^4.14.167": - version: 4.14.184 - resolution: "@types/lodash@npm:4.14.184" - checksum: 10/8906648e102cae18719e4ba53f49b44b1d0dec8bd8082c5aa0c9ec1012b3a6e9ac268fde226ea5ee9e9cf124ce8a564d06dedb1d317fd78f74a0c8b4a5e2d793 +"@types/lodash@npm:^4.14.136, @types/lodash@npm:^4.14.162, @types/lodash@npm:^4.14.167": + version: 4.17.0 + resolution: "@types/lodash@npm:4.17.0" + checksum: 10/2053203292b5af99352d108656ceb15d39da5922fc3fb8186e1552d65c82d6e545372cc97f36c95873aa7186404d59d9305e9d49254d4ae55e77df1e27ab7b5d languageName: node linkType: hard @@ -11722,7 +12024,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.1.0, ajv@npm:^6.10.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -11734,6 +12036,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^7.0.0": + version: 7.2.4 + resolution: "ajv@npm:7.2.4" + dependencies: + fast-deep-equal: "npm:^3.1.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + uri-js: "npm:^4.2.2" + checksum: 10/ed241a8986f80777713a7ffde37cdea8d112631623bbc7f0d867689bcb7af41f24a7ea2750c4dd8be681bf7fea314e05c8b4521a86bfb5882acd2432fc5335df + languageName: node + linkType: hard + "ansi-align@npm:^2.0.0": version: 2.0.0 resolution: "ansi-align@npm:2.0.0" @@ -12423,6 +12737,13 @@ __metadata: languageName: node linkType: hard +"at-least-node@npm:^1.0.0": + version: 1.0.0 + resolution: "at-least-node@npm:1.0.0" + checksum: 10/463e2f8e43384f1afb54bc68485c436d7622acec08b6fad269b421cb1d29cebb5af751426793d0961ed243146fe4dc983402f6d5a51b720b277818dbf6f2e49e + languageName: node + linkType: hard + "atob@npm:^2.1.2": version: 2.1.2 resolution: "atob@npm:2.1.2" @@ -13036,7 +13357,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.2, body-parser@npm:^1.15.2, body-parser@npm:^1.20.0": +"body-parser@npm:1.20.2, body-parser@npm:^1.15.2, body-parser@npm:^1.19.0, body-parser@npm:^1.20.0": version: 1.20.2 resolution: "body-parser@npm:1.20.2" dependencies: @@ -13760,6 +14081,13 @@ __metadata: languageName: node linkType: hard +"call-me-maybe@npm:^1.0.1": + version: 1.0.2 + resolution: "call-me-maybe@npm:1.0.2" + checksum: 10/3d375b6f810a82c751157b199daba60452876186c19ac653e81bfc5fc10d1e2ba7aedb8622367c3a8aca6879f0e6a29435a1193b35edb8f7fd8267a67ea32373 + languageName: node + linkType: hard + "callsites@npm:^3.0.0, callsites@npm:^3.1.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -14379,6 +14707,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:2.1.0": + version: 2.1.0 + resolution: "clsx@npm:2.1.0" + checksum: 10/2e0ce7c3b6803d74fc8147c408f88e79245583202ac14abd9691e2aebb9f312de44270b79154320d10bb7804a9197869635d1291741084826cff20820f31542b + languageName: node + linkType: hard + "clsx@npm:^1.0.4": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -14517,6 +14852,13 @@ __metadata: languageName: node linkType: hard +"colors@npm:^1.3.3": + version: 1.4.0 + resolution: "colors@npm:1.4.0" + checksum: 10/90b2d5465159813a3983ea72ca8cff75f784824ad70f2cc2b32c233e95bcfbcda101ebc6d6766bc50f57263792629bfb4f1f8a4dfbd1d240f229fc7f69b785fc + languageName: node + linkType: hard + "columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -14590,7 +14932,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.1": +"commander@npm:^6.1.0, commander@npm:^6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e @@ -14604,7 +14946,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^8.3.0": +"commander@npm:^8.0.0, commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" checksum: 10/6b7b5d334483ce24bd73c5dac2eab901a7dbb25fd983ea24a1eeac6e7166bb1967f641546e8abf1920afbde86a45fbfe5812fbc69d0dc451bb45ca416a12a3a3 @@ -16541,6 +16883,13 @@ __metadata: languageName: node linkType: hard +"easy-stack@npm:^1.0.0": + version: 1.0.1 + resolution: "easy-stack@npm:1.0.1" + checksum: 10/4b0d0f619db5a6c5a7aa4b8110b30f1adb956a42f3b77b5c2d6b5bbefe2f414643c2835624fd3eab9e6fe50cc76b1523dc8225c68db229af35200e644bcdd738 + languageName: node + linkType: hard + "ecdsa-sig-formatter@npm:1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" @@ -17958,6 +18307,13 @@ __metadata: languageName: node linkType: hard +"event-pubsub@npm:4.3.0": + version: 4.3.0 + resolution: "event-pubsub@npm:4.3.0" + checksum: 10/8a1af789f85050c263eb102d5bd724065bbdc60e3be693fa90efe21d407c0eeb242a79139207a9488289f9f97458e369bfde3294328f21d49addb202439ced20 + languageName: node + linkType: hard + "event-stream@npm:^3.3.4": version: 3.3.4 resolution: "event-stream@npm:3.3.4" @@ -19065,6 +19421,13 @@ __metadata: languageName: node linkType: hard +"format-util@npm:^1.0.3": + version: 1.0.5 + resolution: "format-util@npm:1.0.5" + checksum: 10/0c8622e54ad899ca184ff0b4999e9ff9567965051bade140911209d60554c2ea4d43075763c1cf574d2f740966afe46469c9284357919505cdddf1a0b65ff85c + languageName: node + linkType: hard + "format@npm:^0.2.0": version: 0.2.2 resolution: "format@npm:0.2.2" @@ -19141,7 +19504,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.0.0": +"fs-extra@npm:^10.0.0, fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" dependencies: @@ -19163,6 +19526,18 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^9.0.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: "npm:^1.0.0" + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/08600da1b49552ed23dfac598c8fc909c66776dd130fea54fbcad22e330f7fcc13488bb995f6bc9ce5651aa35b65702faf616fe76370ee56f1aade55da982dca + languageName: node + linkType: hard + "fs-minipass@npm:^1.2.7": version: 1.2.7 resolution: "fs-minipass@npm:1.2.7" @@ -22163,6 +22538,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 10/568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 + languageName: node + linkType: hard + "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -22948,6 +23333,22 @@ __metadata: languageName: node linkType: hard +"js-message@npm:1.0.5": + version: 1.0.5 + resolution: "js-message@npm:1.0.5" + checksum: 10/885d0de9e00bb72a4a8a1c12839192ece6872306ae54e82a71092f44b5a74512cb3507e79415499f53f007cded1060577d33a25a9648973cb93b10d57ef1c479 + languageName: node + linkType: hard + +"js-queue@npm:2.0.0": + version: 2.0.0 + resolution: "js-queue@npm:2.0.0" + dependencies: + easy-stack: "npm:^1.0.0" + checksum: 10/cc9e530267ca6991fd47d24af901816f745ee2b744c8eeff6dc199568f8dc4a53385c129b201fbc8cc6fab60e81e16fe7e1dae7fc0d0f508f81ee29f61f891b2 + languageName: node + linkType: hard + "js-sha3@npm:^0.9.2": version: 0.9.3 resolution: "js-sha3@npm:0.9.3" @@ -22973,7 +23374,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0": +"js-yaml@npm:^3.10.0, js-yaml@npm:^3.12.1, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -23244,6 +23645,29 @@ __metadata: languageName: node linkType: hard +"json-schema-faker@npm:^0.5.0-rcv.26, json-schema-faker@npm:^0.5.0-rcv.29": + version: 0.5.6 + resolution: "json-schema-faker@npm:0.5.6" + dependencies: + json-schema-ref-parser: "npm:^6.1.0" + jsonpath-plus: "npm:^7.2.0" + bin: + jsf: bin/gen.cjs + checksum: 10/cd7b44eb712d4975d0e87c38f38c4500168502e546505e5bc8cd6d33950d991114fe83c810247d5b91bdbe686174d5e8850de39322b4aadaf854202a4bbbffdc + languageName: node + linkType: hard + +"json-schema-ref-parser@npm:^6.1.0": + version: 6.1.0 + resolution: "json-schema-ref-parser@npm:6.1.0" + dependencies: + call-me-maybe: "npm:^1.0.1" + js-yaml: "npm:^3.12.1" + ono: "npm:^4.0.11" + checksum: 10/f36dbb0d9780bf57d68846658bc0e8ac8a17c9095b41a836819a0cfd8550dbdee4b043a0bb535cbce3c683f1ee39b791c208dce15b143e58e34c8abcc747cb89 + languageName: node + linkType: hard + "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -23368,6 +23792,13 @@ __metadata: languageName: node linkType: hard +"jsonpath-plus@npm:^7.2.0": + version: 7.2.0 + resolution: "jsonpath-plus@npm:7.2.0" + checksum: 10/f602445b1aa2d55abc2875859fd948f942980ef6400ca2a0362c7a6aa6f912467865262f4d092e04a16889fa74f0dbf6fd67b9dc9583485a5059be6e0a62c6c2 + languageName: node + linkType: hard + "jsonschema@npm:1.2.2": version: 1.2.2 resolution: "jsonschema@npm:1.2.2" @@ -25055,6 +25486,7 @@ __metadata: "@metamask/accounts-controller": "npm:^16.0.0" "@metamask/address-book-controller": "npm:^4.0.1" "@metamask/announcement-controller": "npm:^6.1.0" + "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" "@metamask/assets-controllers": "patch:@metamask/assets-controllers@patch%3A@metamask/assets-controllers@patch%253A@metamask/assets-controllers@npm%25253A30.0.0%2523~/.yarn/patches/@metamask-assets-controllers-npm-30.0.0-8747c20871.patch%253A%253Aversion=30.0.0&hash=9269c8%23~/.yarn/patches/@metamask-assets-controllers-patch-26d4328777.patch%3A%3Aversion=30.0.0&hash=1ba1a6#~/.yarn/patches/@metamask-assets-controllers-patch-a3b39b55a6.patch" "@metamask/auto-changelog": "npm:^2.1.0" @@ -25127,6 +25559,10 @@ __metadata: "@noble/ciphers": "npm:^0.5.2" "@noble/hashes": "npm:^1.3.3" "@octokit/core": "npm:^3.6.0" + "@open-rpc/meta-schema": "npm:^1.14.6" + "@open-rpc/mock-server": "npm:^1.7.5" + "@open-rpc/schema-utils-js": "npm:^1.16.2" + "@open-rpc/test-coverage": "npm:^2.2.2" "@playwright/test": "npm:^1.39.0" "@popperjs/core": "npm:^2.4.0" "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch" @@ -26079,6 +26515,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10/287c70d8e73ffc25624261a4989c783768aed95ecb60900f051d180cf83e311e3e59865bfd6e9d029cdb149dc20ba2f128a805e9429c5c4ce33b1416c65bbd14 + languageName: node + linkType: hard + "mixin-deep@npm:^1.2.0": version: 1.3.2 resolution: "mixin-deep@npm:1.3.2" @@ -26725,6 +27168,17 @@ __metadata: languageName: node linkType: hard +"node-ipc@npm:9.1.1": + version: 9.1.1 + resolution: "node-ipc@npm:9.1.1" + dependencies: + event-pubsub: "npm:4.3.0" + js-message: "npm:1.0.5" + js-queue: "npm:2.0.0" + checksum: 10/c83afe366a1c35dbe15f3ca8715e4ac85834e3f87a614ba2a06c1ec3a4a3a379b8ebf8f0d5222cdec18df94553e8c7d09ce96829d3d107d6b989d8b42f795593 + languageName: node + linkType: hard + "node-pre-gyp@npm:^0.12.0": version: 0.12.0 resolution: "node-pre-gyp@npm:0.12.0" @@ -27273,6 +27727,15 @@ __metadata: languageName: node linkType: hard +"ono@npm:^4.0.11": + version: 4.0.11 + resolution: "ono@npm:4.0.11" + dependencies: + format-util: "npm:^1.0.3" + checksum: 10/f7ba5f597ae62f9b02bb0c00de3af786ebfb02459223e81d39f714a9404fade2065b830102a1d25c4fa216f17a407d6b721add197c710f8afce9e851cfb12984 + languageName: node + linkType: hard + "open@npm:8.4.2, open@npm:^8.0.4, open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" @@ -29374,6 +29837,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.0" + peerDependencies: + react: ^18.2.0 + checksum: 10/ca5e7762ec8c17a472a3605b6f111895c9f87ac7d43a610ab7024f68cd833d08eda0625ce02ec7178cc1f3c957cf0b9273cdc17aa2cd02da87544331c43b1d21 + languageName: node + linkType: hard + "react-easy-swipe@npm:^0.0.21": version: 0.0.21 resolution: "react-easy-swipe@npm:0.0.21" @@ -29527,6 +30002,18 @@ __metadata: languageName: node linkType: hard +"react-number-format@npm:^5.3.1": + version: 5.3.4 + resolution: "react-number-format@npm:5.3.4" + dependencies: + prop-types: "npm:^15.7.2" + peerDependencies: + react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 10/c5bc021f20ecb5c3dca8da93c89ce583d92b60bfabcfbcfeb59a9fb29ec17d756b559d6a00ff9a185e6c3c5c3f3839afd4ddc1244ad8fbea20342baf74a8bd43 + languageName: node + linkType: hard + "react-popper@npm:^2.2.3": version: 2.2.4 resolution: "react-popper@npm:2.2.4" @@ -29568,9 +30055,9 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.3": - version: 2.3.4 - resolution: "react-remove-scroll-bar@npm:2.3.4" +"react-remove-scroll-bar@npm:^2.3.3, react-remove-scroll-bar@npm:^2.3.6": + version: 2.3.6 + resolution: "react-remove-scroll-bar@npm:2.3.6" dependencies: react-style-singleton: "npm:^2.2.1" tslib: "npm:^2.0.0" @@ -29580,7 +30067,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/ac028b3ed12e66972cab8656747736729b219dff5a600178d1650300a2a750ace37f7ec82146147d37b092b19874f45cf7a45edceff68ac1f59607a828ca089f + checksum: 10/5ab8eda61d5b10825447d11e9c824486c929351a471457c22452caa19b6898e18c3af6a46c3fa68010c713baed1eb9956106d068b4a1058bdcf97a1a9bbed734 languageName: node linkType: hard @@ -29603,6 +30090,25 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll@npm:^2.5.7": + version: 2.5.10 + resolution: "react-remove-scroll@npm:2.5.10" + dependencies: + react-remove-scroll-bar: "npm:^2.3.6" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/15f606482a614a92f8f65692cf27a1c1621d77a63c36f53a7bc4f2243799f2b04770083b313c4b3c2ed76f47d4046f52e86f95280ad5599389818fb882de7d6b + languageName: node + linkType: hard + "react-responsive-carousel@npm:^3.2.21": version: 3.2.21 resolution: "react-responsive-carousel@npm:3.2.21" @@ -29714,6 +30220,19 @@ __metadata: languageName: node linkType: hard +"react-textarea-autosize@npm:8.5.3": + version: 8.5.3 + resolution: "react-textarea-autosize@npm:8.5.3" + dependencies: + "@babel/runtime": "npm:^7.20.13" + use-composed-ref: "npm:^1.3.0" + use-latest: "npm:^1.2.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/4ade4329374f77414f55074826617e388d884b6c9401e0877b63d7f3715db07041431bb757265fb971c4ef15f3fde70ff6a05a5abb09ad3ff89334e1ff5c39ea + languageName: node + linkType: hard + "react-tippy@npm:^1.2.2": version: 1.2.2 resolution: "react-tippy@npm:1.2.2" @@ -29766,6 +30285,15 @@ __metadata: languageName: node linkType: hard +"react18-json-view@npm:^0.2.8": + version: 0.2.8 + resolution: "react18-json-view@npm:0.2.8" + peerDependencies: + react: ">=16.8.0" + checksum: 10/40c57c74713472e2e9cdfde6dee4bf5fb5e56a052772754f666a25405b0a7491a8eb26a7d7d55d727d5ffb04c638b2f47fa8ba9534edf87df621e7bd5dc26d61 + languageName: node + linkType: hard + "react@npm:^16.12.0": version: 16.14.0 resolution: "react@npm:16.14.0" @@ -29777,6 +30305,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/b9214a9bd79e99d08de55f8bef2b7fc8c39630be97c4e29d7be173d14a9a10670b5325e94485f74cd8bff4966ef3c78ee53c79a7b0b9b70cba20aa8973acc694 + languageName: node + linkType: hard + "read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -30152,6 +30689,13 @@ __metadata: languageName: node linkType: hard +"regexparam@npm:^3.0.0": + version: 3.0.0 + resolution: "regexparam@npm:3.0.0" + checksum: 10/80574803cb9f31d1ec7b10ce1db5be629868794ce331c7098fd9725c4c0891347c2fedc18b17de17bda2441da6ef30218b1ad2d481a4081e33ef773de04f9082 + languageName: node + linkType: hard + "regexpp@npm:^3.0.0": version: 3.2.0 resolution: "regexpp@npm:3.2.0" @@ -31355,6 +31899,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/0c4557aa37bafca44ff21dc0ea7c92e2dbcb298bc62eae92b29a39b029134f02fb23917d6ebc8b1fa536b4184934314c20d8864d156a9f6357f3398aaf7bfda8 + languageName: node + linkType: hard + "schema-utils@npm:^0.4.5": version: 0.4.5 resolution: "schema-utils@npm:0.4.5" @@ -33143,6 +33696,13 @@ __metadata: languageName: node linkType: hard +"tabbable@npm:^6.0.0": + version: 6.2.0 + resolution: "tabbable@npm:6.2.0" + checksum: 10/980fa73476026e99dcacfc0d6e000d41d42c8e670faf4682496d30c625495e412c4369694f2a15cf1e5252d22de3c396f2b62edbe8d60b5dadc40d09e3f2dde3 + languageName: node + linkType: hard + "table@npm:^5.4.6": version: 5.4.6 resolution: "table@npm:5.4.6" @@ -33983,10 +34543,10 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.0.0": - version: 4.15.0 - resolution: "type-fest@npm:4.15.0" - checksum: 10/8f897551877daa0df7bb17a21b6acd8a21ac5a0bdb14dbfd353b16013fed99f23c6d9c12a2c7685c8dededb4739ec8bfb120a914330f8b11a478a89758a11acc +"type-fest@npm:^4.0.0, type-fest@npm:^4.12.0": + version: 4.18.1 + resolution: "type-fest@npm:4.18.1" + checksum: 10/ff76e19cb969854161fea2de854073f346e159f5efff05906ece93cbde8a7161b9374121aca53782b44f754152cbacc70264c90ca1acc81ca917723acce5054f languageName: node linkType: hard @@ -34655,7 +35215,16 @@ __metadata: languageName: node linkType: hard -"use-isomorphic-layout-effect@npm:^1.1.2": +"use-composed-ref@npm:^1.3.0": + version: 1.3.0 + resolution: "use-composed-ref@npm:1.3.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/f771cbadfdc91e03b7ab9eb32d0fc0cc647755711801bf507e891ad38c4bbc5f02b2509acadf9c965ec9c5f2f642fd33bdfdfb17b0873c4ad0a9b1f5e5e724bf + languageName: node + linkType: hard + +"use-isomorphic-layout-effect@npm:^1.1.1, use-isomorphic-layout-effect@npm:^1.1.2": version: 1.1.2 resolution: "use-isomorphic-layout-effect@npm:1.1.2" peerDependencies: @@ -34667,6 +35236,20 @@ __metadata: languageName: node linkType: hard +"use-latest@npm:^1.2.1": + version: 1.2.1 + resolution: "use-latest@npm:1.2.1" + dependencies: + use-isomorphic-layout-effect: "npm:^1.1.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b0cbdd91f32e9a7fb4cd9d54934bef55dd6dbe90e2853506405e7c2ca78ca61dd34a6241f7138110a5013da02366138708f23f417c63524ad27aa43afa4196d6 + languageName: node + linkType: hard + "use-memo-one@npm:^1.1.1": version: 1.1.3 resolution: "use-memo-one@npm:1.1.3" @@ -34704,6 +35287,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.0.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.0 resolution: "use@npm:3.1.0" @@ -35505,6 +36097,13 @@ __metadata: languageName: node linkType: hard +"whatwg-fetch@npm:^3.4.1": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c + languageName: node + linkType: hard + "whatwg-mimetype@npm:^2.3.0": version: 2.3.0 resolution: "whatwg-mimetype@npm:2.3.0" @@ -35677,6 +36276,19 @@ __metadata: languageName: node linkType: hard +"wouter@npm:^3.1.2": + version: 3.1.2 + resolution: "wouter@npm:3.1.2" + dependencies: + mitt: "npm:^3.0.1" + regexparam: "npm:^3.0.0" + use-sync-external-store: "npm:^1.0.0" + peerDependencies: + react: ">=16.8.0" + checksum: 10/66e8dd68eeb06f13aa4008282fe049fbeaec3dfff97c577eb5a838e27af56e88f68dc9b1516c39016e9474c3a801ea1dadcb9df73a2324dcef57ffd53daa200c + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -35779,7 +36391,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:*, ws@npm:>=8.14.2, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": +"ws@npm:*, ws@npm:>=8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.2.3, ws@npm:^8.5.0, ws@npm:^8.8.0": version: 8.17.1 resolution: "ws@npm:8.17.1" peerDependencies: From c2a5d78627e2dbd7a78d345eec99966fa226f1f6 Mon Sep 17 00:00:00 2001 From: Devin <168687171+Devin-Apps@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:40:35 +0530 Subject: [PATCH 12/22] chore: Create a story for TokenInput component (#25237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR introduces a Storybook story for the TokenInput component to facilitate isolated UI testing and development. The story includes various controls for manipulating the component's props, ensuring a flexible testing environment. No conversion rate is provided by default, which is expected behavior. The story has been verified in the Storybook UI, and all tests and lint checks pass successfully. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25237?quickstart=1) ## **Related issues** ## **Manual testing steps** 1. Go to the latest build of storybook in this PR 2. Navigate to the TokenInput component in the Components/ folder. ## **Screenshots/Recordings** Screenshot 2024-06-12 at 12 10 52 AM Screenshot 2024-06-12 at 12 11 12 AM ## **Pre-merge author checklist** - [X] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [Devin Run](https://preview.devin.ai/devin/a3ef40c8d3b044609b46488c18361348) --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../ui/token-input/token-input.stories.tsx | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 ui/components/ui/token-input/token-input.stories.tsx diff --git a/ui/components/ui/token-input/token-input.stories.tsx b/ui/components/ui/token-input/token-input.stories.tsx new file mode 100644 index 000000000000..67c83bcbc5bf --- /dev/null +++ b/ui/components/ui/token-input/token-input.stories.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import TokenInput from './token-input.component'; + +const meta: Meta = { + title: 'Components/UI/TokenInput', + component: TokenInput, + argTypes: { + dataTestId: { control: 'text' }, + currentCurrency: { control: 'text' }, + onChange: { action: 'changed' }, + value: { control: 'text' }, + showFiat: { control: 'boolean' }, + hideConversion: { control: 'boolean' }, + token: { + control: 'object', + defaultValue: { + address: '0x0', + decimals: 18, + symbol: 'ETH', + }, + }, + tokenExchangeRates: { control: 'object' }, + nativeCurrency: { control: 'text' }, + tokens: { control: 'array' }, + }, + args: { + dataTestId: 'token-input', + currentCurrency: 'USD', + value: '0x0', + showFiat: true, + hideConversion: false, + token: { + address: '0x0', + decimals: 18, + symbol: 'ETH', + }, + tokenExchangeRates: {}, + nativeCurrency: 'ETH', + tokens: [ + { + address: '0x0', + decimals: 18, + symbol: 'ETH', + }, + ], + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; From 2840657f1e05cecd1870ec93c636a684ee071f31 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:49:19 +0200 Subject: [PATCH 13/22] chore(deps): bump keyring-controller and signature-controller (#25033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/keyring-controller` and `@metamask/signature-controller` versions. The main changes are related to KeyringController, with several changes on types returned from some of the methods. Version 16 also brings a safer method to interact with keyring instances directly, `withKeyring`, but it will be adopted in another PR See changelog for details on changes: https://github.com/MetaMask/core/blob/main/packages/keyring-controller/CHANGELOG.md#1600 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25033?quickstart=1) ## **Related issues** Fixes: #24276 Fixes: https://github.com/MetaMask/accounts-planning/issues/478 ## **Manual testing steps** Affected workflows may include: - Lock - Unlock - Add account - Remove account - Connect hardware wallet - All hardware wallet interactions ## **Screenshots/Recordings** N/A ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot Co-authored-by: Charly Chevalier Co-authored-by: gantunesr <17601467+gantunesr@users.noreply.github.com> --- ...ing-controller-npm-15.0.0-fa070ce311.patch | 120 ---- .../metamask-controller.actions.test.js | 62 +-- app/scripts/metamask-controller.js | 51 +- app/scripts/metamask-controller.test.js | 523 +++++++++++------- app/scripts/skip-onboarding.js | 4 +- lavamoat/browserify/beta/policy.json | 66 +-- lavamoat/browserify/flask/policy.json | 66 +-- lavamoat/browserify/main/policy.json | 66 +-- lavamoat/browserify/mmi/policy.json | 66 +-- package.json | 7 +- ui/store/actions.ts | 8 +- yarn.lock | 234 ++++---- 12 files changed, 569 insertions(+), 704 deletions(-) delete mode 100644 .yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch diff --git a/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch b/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch deleted file mode 100644 index 27d866a74888..000000000000 --- a/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch +++ /dev/null @@ -1,120 +0,0 @@ -diff --git a/dist/chunk-52QZQQKP.mjs b/dist/chunk-52QZQQKP.mjs -index 934f432c8013a6af5303726e1495bed2335fa078..8de2560fe81dfc3dbfef83dd0079482306c425d9 100644 ---- a/dist/chunk-52QZQQKP.mjs -+++ b/dist/chunk-52QZQQKP.mjs -@@ -2,7 +2,8 @@ import { - __privateAdd, - __privateGet, - __privateMethod, -- __privateSet -+ __privateSet, -+ KeyringControllerError - } from "./chunk-NAAWD7HX.mjs"; - - // src/KeyringController.ts -@@ -582,6 +583,18 @@ var KeyringController = class extends BaseController { - }) - ); - serializedKeyrings.push(...__privateGet(this, _unsupportedKeyrings)); -+ /** -+ * ============================== PATCH INFORMATION ============================== -+ * The HD keyring is the default keyring for all wallets if this keyring is missing -+ * for some reason we should avoid saving the keyrings -+ * -+ * The upstream fix is here: https://github.com/MetaMask/core/pull/4168 -+ * -+ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` -+ */ -+ if (!serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)) { -+ throw new Error(KeyringControllerError.NoHdKeyring); -+ } - let vault; - let newEncryptionKey; - if (__privateGet(this, _cacheEncryptionKey)) { -@@ -1087,9 +1100,16 @@ getKeyringBuilderForType_fn = function(type) { - }; - _addQRKeyring = new WeakSet(); - addQRKeyring_fn = async function() { -- const qrKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */, { -- accounts: [] -- }); -+ /** -+ * Patch for @metamask/keyring-controller v13.0.0 -+ * Below code change will fix the issue 23804, The intial code added a empty accounts as argument when creating a new QR keyring. -+ * cause the new Keystone MetamaskKeyring default properties all are undefined during deserialise() process. -+ * Please refer to PR 23903 for detail. -+ * -+ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` -+ */ -+ // @ts-expect-error See patch note -+ const qrKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device"); - const accounts = await qrKeyring.getAccounts(); - await __privateMethod(this, _checkForDuplicate, checkForDuplicate_fn).call(this, "QR Hardware Wallet Device" /* qr */, accounts); - __privateGet(this, _keyrings).push(qrKeyring); -diff --git a/dist/chunk-CHLPTPMZ.js b/dist/chunk-CHLPTPMZ.js -index bef1a8e9dd5efe426f8aaaba1fe4501b124f7e87..7b48c000e54708da2a689e2d6cb1b61a279f1205 100644 ---- a/dist/chunk-CHLPTPMZ.js -+++ b/dist/chunk-CHLPTPMZ.js -@@ -50,6 +50,7 @@ var KeyringControllerError = /* @__PURE__ */ ((KeyringControllerError2) => { - KeyringControllerError2["ExpiredCredentials"] = "KeyringController - Encryption key and salt provided are expired"; - KeyringControllerError2["NoKeyringBuilder"] = "KeyringController - No keyringBuilder found for keyring"; - KeyringControllerError2["DataType"] = "KeyringController - Incorrect data type provided"; -+ KeyringControllerError2["NoHdKeyring"] = "KeyringController - No HD Keyring found"; - return KeyringControllerError2; - })(KeyringControllerError || {}); - -diff --git a/dist/chunk-GXM4O6HW.js b/dist/chunk-GXM4O6HW.js -index f7539e2e6354f418cbb095cc1a2cda01a5bdeae6..978f4426536c594568ecc56f1c27881db4bfa861 100644 ---- a/dist/chunk-GXM4O6HW.js -+++ b/dist/chunk-GXM4O6HW.js -@@ -582,6 +582,18 @@ var KeyringController = class extends _basecontroller.BaseController { - }) - ); - serializedKeyrings.push(..._chunkCHLPTPMZjs.__privateGet.call(void 0, this, _unsupportedKeyrings)); -+ /** -+ * ============================== PATCH INFORMATION ============================== -+ * The HD keyring is the default keyring for all wallets if this keyring is missing -+ * for some reason we should avoid saving the keyrings -+ * -+ * The upstream fix is here: https://github.com/MetaMask/core/pull/4168 -+ * -+ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` -+ */ -+ if (!serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)) { -+ throw new Error(_chunkCHLPTPMZjs.KeyringControllerError.NoHdKeyring); -+ } - let vault; - let newEncryptionKey; - if (_chunkCHLPTPMZjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -@@ -1087,9 +1099,16 @@ getKeyringBuilderForType_fn = function(type) { - }; - _addQRKeyring = new WeakSet(); - addQRKeyring_fn = async function() { -- const qrKeyring = await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */, { -- accounts: [] -- }); -+ /** -+ * Patch for @metamask/keyring-controller v13.0.0 -+ * Below code change will fix the issue 23804, The intial code added a empty accounts as argument when creating a new QR keyring. -+ * cause the new Keystone MetamaskKeyring default properties all are undefined during deserialise() process. -+ * Please refer to PR 23903 for detail. -+ * -+ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` -+ */ -+ // @ts-expect-error See patch note -+ const qrKeyring = await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device"); - const accounts = await qrKeyring.getAccounts(); - await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _checkForDuplicate, checkForDuplicate_fn).call(this, "QR Hardware Wallet Device" /* qr */, accounts); - _chunkCHLPTPMZjs.__privateGet.call(void 0, this, _keyrings).push(qrKeyring); -diff --git a/dist/chunk-NAAWD7HX.mjs b/dist/chunk-NAAWD7HX.mjs -index b5de23aabec9d502e8e6423480ffaaff26257bfc..a0c027a7c13828883ec5c05cbb7eab92c41f0dc3 100644 ---- a/dist/chunk-NAAWD7HX.mjs -+++ b/dist/chunk-NAAWD7HX.mjs -@@ -50,6 +50,7 @@ var KeyringControllerError = /* @__PURE__ */ ((KeyringControllerError2) => { - KeyringControllerError2["ExpiredCredentials"] = "KeyringController - Encryption key and salt provided are expired"; - KeyringControllerError2["NoKeyringBuilder"] = "KeyringController - No keyringBuilder found for keyring"; - KeyringControllerError2["DataType"] = "KeyringController - Incorrect data type provided"; -+ KeyringControllerError2["NoHdKeyring"] = "KeyringController - No HD Keyring found"; - return KeyringControllerError2; - })(KeyringControllerError || {}); - diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index 28dcc00c35a9..29ba6f11515a 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -164,57 +164,41 @@ describe('MetaMaskController', function () { }); describe('#importAccountWithStrategy', function () { - it('two sequential calls with same strategy give same result', async function () { - let keyringControllerState1; - let keyringControllerState2; + it('throws an error when importing the same account twice', async function () { const importPrivkey = '4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'; - await metamaskController.createNewVaultAndKeychain('test@123'); - await Promise.all([ + + await metamaskController.importAccountWithStrategy('privateKey', [ + importPrivkey, + ]); + + await expect( metamaskController.importAccountWithStrategy('privateKey', [ importPrivkey, ]), - Promise.resolve(1).then(() => { - keyringControllerState1 = JSON.stringify( - metamaskController.keyringController.state, - ); - metamaskController.importAccountWithStrategy('privateKey', [ - importPrivkey, - ]); - }), - Promise.resolve(2).then(() => { - keyringControllerState2 = JSON.stringify( - metamaskController.keyringController.state, - ); - }), - ]); - expect(keyringControllerState1).toStrictEqual(keyringControllerState2); + ).rejects.toThrow( + 'KeyringController - The account you are trying to import is a duplicate', + ); }); }); describe('#createNewVaultAndRestore', function () { it('two successive calls with same inputs give same result', async function () { - const result1 = await metamaskController.createNewVaultAndRestore( - 'test@123', - TEST_SEED, - ); - const result2 = await metamaskController.createNewVaultAndRestore( - 'test@123', - TEST_SEED, - ); + await metamaskController.createNewVaultAndRestore('test@123', TEST_SEED); + const result1 = metamaskController.keyringController.state; + await metamaskController.createNewVaultAndRestore('test@123', TEST_SEED); + const result2 = metamaskController.keyringController.state; expect(result1).toStrictEqual(result2); }); }); describe('#createNewVaultAndKeychain', function () { it('two successive calls with same inputs give same result', async function () { - const result1 = await metamaskController.createNewVaultAndKeychain( - 'test@123', - ); - const result2 = await metamaskController.createNewVaultAndKeychain( - 'test@123', - ); + await metamaskController.createNewVaultAndKeychain('test@123'); + const result1 = metamaskController.keyringController.state; + await metamaskController.createNewVaultAndKeychain('test@123'); + const result2 = metamaskController.keyringController.state; expect(result1).not.toStrictEqual(undefined); expect(result1).toStrictEqual(result2); }); @@ -222,9 +206,13 @@ describe('MetaMaskController', function () { describe('#setLocked', function () { it('should lock the wallet', async function () { - const { isUnlocked, keyrings } = await metamaskController.setLocked(); - expect(isUnlocked).toStrictEqual(false); - expect(keyrings).toStrictEqual([]); + await metamaskController.setLocked(); + expect( + metamaskController.keyringController.state.isUnlocked, + ).toStrictEqual(false); + expect(metamaskController.keyringController.state.keyrings).toStrictEqual( + [], + ); }); }); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 799614e73cdb..298691e95695 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4015,7 +4015,7 @@ export default class MetamaskController extends EventEmitter { } // create new vault - const vault = await this.keyringController.createNewVaultAndRestore( + await this.keyringController.createNewVaultAndRestore( password, this._convertMnemonicToWordlistIndices(seedPhraseAsBuffer), ); @@ -4029,8 +4029,6 @@ export default class MetamaskController extends EventEmitter { // Ledger Keyring GitHub downtime this.setLedgerTransportPreference(); } - - return vault; } finally { releaseLock(); } @@ -4070,8 +4068,7 @@ export default class MetamaskController extends EventEmitter { } // This account has assets, so check the next one - ({ addedAccountAddress: address } = - await this.keyringController.addNewAccount(count)); + address = await this.keyringController.addNewAccount(count); } } @@ -4461,36 +4458,28 @@ export default class MetamaskController extends EventEmitter { const keyring = await this.getKeyringForDevice(deviceName, hdPath); keyring.setAccountToUnlock(index); - const oldAccounts = await this.keyringController.getAccounts(); - const keyState = await this.keyringController.addNewAccountForKeyring( - keyring, + const unlockedAccount = + await this.keyringController.addNewAccountForKeyring(keyring); + const label = this.getAccountLabel( + deviceName === HardwareDeviceNames.qr ? keyring.getName() : deviceName, + index, + hdPathDescription, ); - const newAccounts = await this.keyringController.getAccounts(); - newAccounts.forEach((address) => { - if (!oldAccounts.includes(address)) { - const label = this.getAccountLabel( - deviceName === HardwareDeviceNames.qr - ? keyring.getName() - : deviceName, - index, - hdPathDescription, - ); - // Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc - this.preferencesController.setAccountLabel(address, label); - // Select the account - this.preferencesController.setSelectedAddress(address); + // Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc + this.preferencesController.setAccountLabel(unlockedAccount, label); + // Select the account + this.preferencesController.setSelectedAddress(unlockedAccount); - // It is expected that the account also exist in the accounts-controller - // in other case, an error shall be thrown - const account = this.accountsController.getAccountByAddress(address); - this.accountsController.setAccountName(account.id, label); - } - }); + // It is expected that the account also exist in the accounts-controller + // in other case, an error shall be thrown + const account = + this.accountsController.getAccountByAddress(unlockedAccount); + this.accountsController.setAccountName(account.id, label); const accounts = this.accountsController.listAccounts(); const { identities } = this.preferencesController.store.getState(); - return { ...keyState, identities, accounts }; + return { unlockedAccount, identities, accounts }; } // @@ -4506,7 +4495,7 @@ export default class MetamaskController extends EventEmitter { async addNewAccount(accountCount) { const oldAccounts = await this.keyringController.getAccounts(); - const { addedAccountAddress } = await this.keyringController.addNewAccount( + const addedAccountAddress = await this.keyringController.addNewAccount( accountCount, ); @@ -4678,7 +4667,7 @@ export default class MetamaskController extends EventEmitter { * @param {any} args - The data required by that strategy to import an account. */ async importAccountWithStrategy(strategy, args) { - const { importedAccountAddress } = + const importedAccountAddress = await this.keyringController.importAccountWithStrategy(strategy, args); // set new account as selected this.preferencesController.setSelectedAddress(importedAccountAddress); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index f6c7e4a25b3b..06434643f93f 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -27,6 +27,8 @@ import { RatesController, TokenListController, } from '@metamask/assets-controllers'; +import { TrezorKeyring } from '@metamask/eth-trezor-keyring'; +import { LedgerKeyring } from '@metamask/eth-ledger-bridge-keyring'; import { NETWORK_TYPES } from '../../shared/constants/network'; import { createTestProviderTools } from '../../test/stub/provider'; import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; @@ -127,6 +129,64 @@ jest.mock( }, ); +const KNOWN_PUBLIC_KEY = + '02065bc80d3d12b3688e4ad5ab1e9eda6adf24aec2518bfc21b87c99d4c5077ab0'; + +const KNOWN_PUBLIC_KEY_ADDRESSES = [ + { + address: '0x0e122670701207DB7c6d7ba9aE07868a4572dB3f', + balance: null, + index: 0, + }, + { + address: '0x2ae19DAd8b2569F7Bb4606D951Cc9495631e818E', + balance: null, + index: 1, + }, + { + address: '0x0051140bAaDC3E9AC92A4a90D18Bb6760c87e7ac', + balance: null, + index: 2, + }, + { + address: '0x9DBCF67CC721dBd8Df28D7A0CbA0fa9b0aFc6472', + balance: null, + index: 3, + }, + { + address: '0x828B2c51c5C1bB0c57fCD2C108857212c95903DE', + balance: null, + index: 4, + }, +]; + +const buildMockKeyringBridge = (publicKeyPayload) => + jest.fn(() => ({ + init: jest.fn(), + dispose: jest.fn(), + updateTransportMethod: jest.fn(), + getPublicKey: jest.fn(async () => publicKeyPayload), + })); + +jest.mock('@metamask/eth-trezor-keyring', () => ({ + ...jest.requireActual('@metamask/eth-trezor-keyring'), + TrezorConnectBridge: buildMockKeyringBridge({ + success: true, + payload: { + publicKey: KNOWN_PUBLIC_KEY, + chainCode: '0x1', + }, + }), +})); + +jest.mock('@metamask/eth-ledger-bridge-keyring', () => ({ + ...jest.requireActual('@metamask/eth-ledger-bridge-keyring'), + LedgerIframeBridge: buildMockKeyringBridge({ + publicKey: KNOWN_PUBLIC_KEY, + chainCode: '0x1', + }), +})); + const mockIsManifestV3 = jest.fn().mockReturnValue(false); jest.mock('../../shared/modules/mv3.utils', () => ({ get isManifestV3() { @@ -754,257 +814,290 @@ describe('MetaMaskController', () => { }); }); - describe('connectHardware', () => { - it('should throw if it receives an unknown device name', async () => { - const result = metamaskController.connectHardware( - 'Some random device name', - 0, - `m/44/0'/0'`, - ); - - await expect(result).rejects.toThrow( - 'MetamaskController:getKeyringForDevice - Unknown device', - ); + describe('hardware keyrings', () => { + beforeEach(async () => { + await metamaskController.createNewVaultAndKeychain('test@123'); }); - it('should add the Trezor Hardware keyring', async () => { - jest.spyOn(metamaskController.keyringController, 'addNewKeyring'); - await metamaskController - .connectHardware(HardwareDeviceNames.trezor, 0) - .catch(() => null); - const keyrings = - await metamaskController.keyringController.getKeyringsByType( - KeyringType.trezor, + describe('connectHardware', () => { + it('should throw if it receives an unknown device name', async () => { + const result = metamaskController.connectHardware( + 'Some random device name', + 0, + `m/44/0'/0'`, ); - expect( - metamaskController.keyringController.addNewKeyring, - ).toHaveBeenCalledWith(KeyringType.trezor); - expect(keyrings).toHaveLength(1); - }); - it('should add the Ledger Hardware keyring', async () => { - jest.spyOn(metamaskController.keyringController, 'addNewKeyring'); - await metamaskController - .connectHardware(HardwareDeviceNames.ledger, 0) - .catch(() => null); - const keyrings = - await metamaskController.keyringController.getKeyringsByType( - KeyringType.ledger, + await expect(result).rejects.toThrow( + 'MetamaskController:getKeyringForDevice - Unknown device', ); - expect( - metamaskController.keyringController.addNewKeyring, - ).toHaveBeenCalledWith(KeyringType.ledger); - expect(keyrings).toHaveLength(1); - }); - }); + }); - describe('getPrimaryKeyringMnemonic', () => { - it('should return a mnemonic as a Uint8Array', () => { - const mockMnemonic = - 'above mercy benefit hospital call oval domain student sphere interest argue shock'; - const mnemonicIndices = mockMnemonic - .split(' ') - .map((word) => englishWordlist.indexOf(word)); - const uint8ArrayMnemonic = new Uint8Array( - new Uint16Array(mnemonicIndices).buffer, - ); + it('should add the Trezor Hardware keyring and return the first page of accounts', async () => { + jest.spyOn(metamaskController.keyringController, 'addNewKeyring'); - const mockHDKeyring = { - type: 'HD Key Tree', - mnemonic: uint8ArrayMnemonic, - }; - jest - .spyOn(metamaskController.keyringController, 'getKeyringsByType') - .mockReturnValue([mockHDKeyring]); + const firstPage = await metamaskController.connectHardware( + HardwareDeviceNames.trezor, + 0, + ); - const recoveredMnemonic = - metamaskController.getPrimaryKeyringMnemonic(); + expect( + metamaskController.keyringController.addNewKeyring, + ).toHaveBeenCalledWith(KeyringType.trezor); + expect( + metamaskController.keyringController.state.keyrings[1].type, + ).toBe(TrezorKeyring.type); + expect(firstPage).toStrictEqual(KNOWN_PUBLIC_KEY_ADDRESSES); + }); - expect(recoveredMnemonic).toStrictEqual(uint8ArrayMnemonic); - }); - }); + it('should add the Ledger Hardware keyring and return the first page of accounts', async () => { + jest.spyOn(metamaskController.keyringController, 'addNewKeyring'); - describe('checkHardwareStatus', () => { - it('should throw if it receives an unknown device name', async () => { - const result = metamaskController.checkHardwareStatus( - 'Some random device name', - `m/44/0'/0'`, - ); - await expect(result).rejects.toThrow( - 'MetamaskController:getKeyringForDevice - Unknown device', - ); - }); + const firstPage = await metamaskController.connectHardware( + HardwareDeviceNames.ledger, + 0, + ); - it('should be locked by default', async () => { - await metamaskController - .connectHardware(HardwareDeviceNames.trezor, 0) - .catch(() => null); - const status = await metamaskController.checkHardwareStatus( - HardwareDeviceNames.trezor, - ); - expect(status).toStrictEqual(false); + expect( + metamaskController.keyringController.addNewKeyring, + ).toHaveBeenCalledWith(KeyringType.ledger); + expect( + metamaskController.keyringController.state.keyrings[1].type, + ).toBe(LedgerKeyring.type); + expect(firstPage).toStrictEqual(KNOWN_PUBLIC_KEY_ADDRESSES); + }); }); - }); - describe('forgetDevice', () => { - it('should throw if it receives an unknown device name', async () => { - const result = metamaskController.forgetDevice( - 'Some random device name', - ); - await expect(result).rejects.toThrow( - 'MetamaskController:getKeyringForDevice - Unknown device', - ); - }); + describe('checkHardwareStatus', () => { + it('should throw if it receives an unknown device name', async () => { + const result = metamaskController.checkHardwareStatus( + 'Some random device name', + `m/44/0'/0'`, + ); + await expect(result).rejects.toThrow( + 'MetamaskController:getKeyringForDevice - Unknown device', + ); + }); - it('should remove the identities when the device is forgotten', async () => { - jest.spyOn(window, 'open').mockReturnValue(); + [HardwareDeviceNames.trezor, HardwareDeviceNames.ledger].forEach( + (device) => { + describe(`using ${device}`, () => { + it('should be unlocked by default', async () => { + await metamaskController.connectHardware(device, 0); - const localMetaMaskController = new MetaMaskController({ - showUserConfirmation: noop, - encryptor: mockEncryptor, - initState: { - ...cloneDeep(firstTimeState), - KeyringController: { - keyrings: [{ type: KeyringType.trezor, accounts: ['0x123'] }], - isUnlocked: true, - }, - PreferencesController: { - identities: { - '0x123': { name: 'Trezor 1', address: '0x123' }, - }, - selectedAddress: '0x123', - }, - }, - initLangCode: 'en_US', - platform: { - showTransactionNotification: () => undefined, - getVersion: () => 'foo', + const status = await metamaskController.checkHardwareStatus( + device, + ); + + expect(status).toStrictEqual(true); + }); + }); }, - browser: browserPolyfillMock, - infuraProjectId: 'foo', - isFirstMetaMaskControllerSetup: true, + ); + }); + + describe('forgetDevice', () => { + it('should throw if it receives an unknown device name', async () => { + const result = metamaskController.forgetDevice( + 'Some random device name', + ); + await expect(result).rejects.toThrow( + 'MetamaskController:getKeyringForDevice - Unknown device', + ); }); - await localMetaMaskController.keyringController.createNewVaultAndKeychain( - 'password', - ); + it('should remove the identities when the device is forgotten', async () => { + await metamaskController.connectHardware( + HardwareDeviceNames.trezor, + 0, + ); + await metamaskController.unlockHardwareWalletAccount( + 0, + HardwareDeviceNames.trezor, + ); + const hardwareKeyringAccount = + metamaskController.keyringController.state.keyrings[1].accounts[0]; - await localMetaMaskController.keyringController.addNewKeyring( - 'Trezor Hardware', - { - accounts: ['0x123'], - }, - ); + await metamaskController.forgetDevice(HardwareDeviceNames.trezor); - await localMetaMaskController.forgetDevice(HardwareDeviceNames.trezor); - const { identities: updatedIdentities } = - localMetaMaskController.preferencesController.store.getState(); - expect(updatedIdentities['0x123']).toBeUndefined(); - }); + expect( + Object.keys( + metamaskController.preferencesController.store.getState() + .identities, + ), + ).not.toContain(hardwareKeyringAccount); + expect( + metamaskController.accountsController + .listAccounts() + .some((account) => account.address === hardwareKeyringAccount), + ).toStrictEqual(false); + }); - it('should wipe all the keyring info', async () => { - await metamaskController - .connectHardware(HardwareDeviceNames.trezor, 0) - .catch(() => null); - await metamaskController.forgetDevice(HardwareDeviceNames.trezor); - const keyrings = - await metamaskController.keyringController.getKeyringsByType( - KeyringType.trezor, + it('should wipe all the keyring info', async () => { + await metamaskController.connectHardware( + HardwareDeviceNames.trezor, + 0, ); - expect(keyrings[0].accounts).toStrictEqual([]); - expect(keyrings[0].page).toStrictEqual(0); - expect(keyrings[0].isUnlocked()).toStrictEqual(false); + await metamaskController.forgetDevice(HardwareDeviceNames.trezor); + const keyrings = + await metamaskController.keyringController.getKeyringsByType( + KeyringType.trezor, + ); + + expect(keyrings[0].accounts).toStrictEqual([]); + expect(keyrings[0].page).toStrictEqual(0); + expect(keyrings[0].isUnlocked()).toStrictEqual(false); + }); }); - }); - describe('unlockHardwareWalletAccount', () => { - const accountToUnlock = 10; - beforeEach(async () => { - await metamaskController.keyringController.createNewVaultAndRestore( - 'password', - TEST_SEED, - ); - jest.spyOn(window, 'open').mockReturnValue(); - jest - .spyOn( - metamaskController.keyringController, - 'addNewAccountForKeyring', - ) - .mockReturnValue('0x123'); + describe('unlockHardwareWalletAccount', () => { + const accountToUnlock = 0; - jest - .spyOn(metamaskController.keyringController, 'getAccounts') - .mockResolvedValueOnce(['0x1']) - .mockResolvedValueOnce(['0x2']) - .mockResolvedValueOnce(['0x3']); - jest - .spyOn(metamaskController.preferencesController, 'setSelectedAddress') - .mockReturnValue(); - jest - .spyOn(metamaskController.preferencesController, 'setAccountLabel') - .mockReturnValue(); + [HardwareDeviceNames.trezor, HardwareDeviceNames.ledger].forEach( + (device) => { + describe(`using ${device}`, () => { + beforeEach(async () => { + await metamaskController.connectHardware(device, 0); + }); - jest - .spyOn(metamaskController.accountsController, 'getAccountByAddress') - .mockReturnValue({ - account: { - id: '2d47e693-26c2-47cb-b374-6151199bbe3f', - }, - }); - jest - .spyOn(metamaskController.accountsController, 'setAccountName') - .mockReturnValue(); + it('should return the unlocked account', async () => { + const { unlockedAccount } = + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); + + expect(unlockedAccount).toBe( + KNOWN_PUBLIC_KEY_ADDRESSES[ + accountToUnlock + ].address.toLowerCase(), + ); + }); - await metamaskController.unlockHardwareWalletAccount( - accountToUnlock, - HardwareDeviceNames.trezor, - `m/44'/1'/0'/0`, - ); - }); + it('should add the unlocked account to KeyringController', async () => { + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); - it('should set unlockedAccount in the keyring', async () => { - const keyrings = - await metamaskController.keyringController.getKeyringsByType( - KeyringType.trezor, - ); - expect(keyrings[0].unlockedAccount).toStrictEqual(accountToUnlock); - }); + expect( + metamaskController.keyringController.state.keyrings[1] + .accounts, + ).toStrictEqual([ + KNOWN_PUBLIC_KEY_ADDRESSES[ + accountToUnlock + ].address.toLowerCase(), + ]); + }); - it('should call keyringController.addNewAccount', async () => { - expect( - metamaskController.keyringController.addNewAccountForKeyring, - ).toHaveBeenCalledTimes(1); - }); + it('should call keyringController.addNewAccountForKeyring', async () => { + jest.spyOn( + metamaskController.keyringController, + 'addNewAccountForKeyring', + ); - it('should call keyringController.getAccounts', async () => { - expect( - metamaskController.keyringController.getAccounts, - ).toHaveBeenCalledTimes(3); - }); + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); - it('should call preferencesController.setSelectedAddress', async () => { - expect( - metamaskController.preferencesController.setSelectedAddress, - ).toHaveBeenCalledTimes(1); - }); + expect( + metamaskController.keyringController.addNewAccountForKeyring, + ).toHaveBeenCalledTimes(1); + }); - it('should call preferencesController.setAccountLabel', async () => { - expect( - metamaskController.preferencesController.setAccountLabel, - ).toHaveBeenCalledTimes(1); - }); + it('should call preferencesController.setSelectedAddress', async () => { + jest.spyOn( + metamaskController.preferencesController, + 'setSelectedAddress', + ); - it('should call accountsController.getAccountByAddress', async () => { - expect( - metamaskController.accountsController.getAccountByAddress, - ).toHaveBeenCalledTimes(1); + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); + + expect( + metamaskController.preferencesController.setSelectedAddress, + ).toHaveBeenCalledTimes(1); + }); + + it('should call preferencesController.setAccountLabel', async () => { + jest.spyOn( + metamaskController.preferencesController, + 'setAccountLabel', + ); + + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); + + expect( + metamaskController.preferencesController.setAccountLabel, + ).toHaveBeenCalledTimes(1); + }); + + it('should call accountsController.getAccountByAddress', async () => { + jest.spyOn( + metamaskController.accountsController, + 'getAccountByAddress', + ); + + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); + + expect( + metamaskController.accountsController.getAccountByAddress, + ).toHaveBeenCalledTimes(1); + }); + + it('should call accountsController.setAccountName', async () => { + jest.spyOn( + metamaskController.accountsController, + 'setAccountName', + ); + + await metamaskController.unlockHardwareWalletAccount( + accountToUnlock, + device, + ); + + expect( + metamaskController.accountsController.setAccountName, + ).toHaveBeenCalledTimes(1); + }); + }); + }, + ); }); + }); - it('should call accountsController.setAccountName', async () => { - expect( - metamaskController.accountsController.setAccountName, - ).toHaveBeenCalledTimes(1); + describe('getPrimaryKeyringMnemonic', () => { + it('should return a mnemonic as a Uint8Array', () => { + const mockMnemonic = + 'above mercy benefit hospital call oval domain student sphere interest argue shock'; + const mnemonicIndices = mockMnemonic + .split(' ') + .map((word) => englishWordlist.indexOf(word)); + const uint8ArrayMnemonic = new Uint8Array( + new Uint16Array(mnemonicIndices).buffer, + ); + + const mockHDKeyring = { + type: 'HD Key Tree', + mnemonic: uint8ArrayMnemonic, + }; + jest + .spyOn(metamaskController.keyringController, 'getKeyringsByType') + .mockReturnValue([mockHDKeyring]); + + const recoveredMnemonic = + metamaskController.getPrimaryKeyringMnemonic(); + + expect(recoveredMnemonic).toStrictEqual(uint8ArrayMnemonic); }); }); diff --git a/app/scripts/skip-onboarding.js b/app/scripts/skip-onboarding.js index 86b07aae66e5..39c3b0b61865 100644 --- a/app/scripts/skip-onboarding.js +++ b/app/scripts/skip-onboarding.js @@ -100,13 +100,13 @@ async function generateVaultAndAccount(encodedSeedPhrase, password) { return new Uint8Array(new Uint16Array(indices).buffer); }; - const res = await krCtrl.createNewVaultAndRestore( + await krCtrl.createNewVaultAndRestore( password, _convertMnemonicToWordlistIndices(seedPhraseAsBuffer), ); const { vault } = krCtrl.state; - const account = res.keyrings[0].accounts[0]; + const account = krCtrl.state.keyrings[0].accounts[0]; return { vault, account }; } diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 23779d4b18df..8415dbd75002 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -526,8 +526,8 @@ "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, - "@keystonehq/metamask-airgapped-keyring>rlp": true, "browserify>buffer": true, + "ethereumjs-util>rlp": true, "uuid": true, "webpack>events": true } @@ -537,12 +537,17 @@ "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "@keystonehq/bc-ur-registry-eth": true, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, - "eth-lattice-keyring>rlp": true, "uuid": true } }, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": { + "globals": { + "TextEncoder": true + } + }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, @@ -591,12 +596,6 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true } }, - "@keystonehq/metamask-airgapped-keyring>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -1076,9 +1075,9 @@ "console.error": true }, "packages": { - "@metamask/assets-controllers>async-mutex": true, "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-filters>async-mutex": true, "@metamask/safe-event-emitter": true, "pify": true } @@ -1096,6 +1095,14 @@ "@metamask/utils": true } }, + "@metamask/eth-json-rpc-filters>async-mutex": { + "globals": { + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1586,13 +1593,13 @@ "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/assets-controllers>async-mutex": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, "@metamask/keyring-controller>ethereumjs-wallet": true, + "@metamask/name-controller>async-mutex": true, "@metamask/utils": true } }, @@ -1649,18 +1656,12 @@ "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util>create-hash": true - } - }, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true + "ethereumjs-util>create-hash": true, + "ethereumjs-util>rlp": true } }, "@metamask/logging-controller": { @@ -2309,6 +2310,7 @@ "@ethersproject/providers": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, @@ -2316,7 +2318,6 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/gas-fee-controller": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/utils": true, @@ -2443,15 +2444,6 @@ "uuid": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { "packages": { "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, @@ -2762,12 +2754,12 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>async-mutex": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2820,15 +2812,6 @@ "@trezor/connect-web>tslib": true } }, - "@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -3880,9 +3863,9 @@ "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, + "eth-lattice-keyring>gridplus-sdk>rlp": true, "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "eth-lattice-keyring>rlp": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethereumjs-util>ethereum-cryptography>hash.js": true, "lodash": true @@ -3981,6 +3964,11 @@ "ganache>abstract-level>buffer": true } }, + "eth-lattice-keyring>gridplus-sdk>rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>secp256k1": { "packages": { "@metamask/ppom-validator>elliptic": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 23779d4b18df..8415dbd75002 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -526,8 +526,8 @@ "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, - "@keystonehq/metamask-airgapped-keyring>rlp": true, "browserify>buffer": true, + "ethereumjs-util>rlp": true, "uuid": true, "webpack>events": true } @@ -537,12 +537,17 @@ "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "@keystonehq/bc-ur-registry-eth": true, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, - "eth-lattice-keyring>rlp": true, "uuid": true } }, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": { + "globals": { + "TextEncoder": true + } + }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, @@ -591,12 +596,6 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true } }, - "@keystonehq/metamask-airgapped-keyring>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -1076,9 +1075,9 @@ "console.error": true }, "packages": { - "@metamask/assets-controllers>async-mutex": true, "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-filters>async-mutex": true, "@metamask/safe-event-emitter": true, "pify": true } @@ -1096,6 +1095,14 @@ "@metamask/utils": true } }, + "@metamask/eth-json-rpc-filters>async-mutex": { + "globals": { + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1586,13 +1593,13 @@ "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/assets-controllers>async-mutex": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, "@metamask/keyring-controller>ethereumjs-wallet": true, + "@metamask/name-controller>async-mutex": true, "@metamask/utils": true } }, @@ -1649,18 +1656,12 @@ "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util>create-hash": true - } - }, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true + "ethereumjs-util>create-hash": true, + "ethereumjs-util>rlp": true } }, "@metamask/logging-controller": { @@ -2309,6 +2310,7 @@ "@ethersproject/providers": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, @@ -2316,7 +2318,6 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/gas-fee-controller": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/utils": true, @@ -2443,15 +2444,6 @@ "uuid": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { "packages": { "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, @@ -2762,12 +2754,12 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>async-mutex": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2820,15 +2812,6 @@ "@trezor/connect-web>tslib": true } }, - "@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -3880,9 +3863,9 @@ "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, + "eth-lattice-keyring>gridplus-sdk>rlp": true, "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "eth-lattice-keyring>rlp": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethereumjs-util>ethereum-cryptography>hash.js": true, "lodash": true @@ -3981,6 +3964,11 @@ "ganache>abstract-level>buffer": true } }, + "eth-lattice-keyring>gridplus-sdk>rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>secp256k1": { "packages": { "@metamask/ppom-validator>elliptic": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 23779d4b18df..8415dbd75002 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -526,8 +526,8 @@ "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, - "@keystonehq/metamask-airgapped-keyring>rlp": true, "browserify>buffer": true, + "ethereumjs-util>rlp": true, "uuid": true, "webpack>events": true } @@ -537,12 +537,17 @@ "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "@keystonehq/bc-ur-registry-eth": true, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, - "eth-lattice-keyring>rlp": true, "uuid": true } }, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": { + "globals": { + "TextEncoder": true + } + }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, @@ -591,12 +596,6 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true } }, - "@keystonehq/metamask-airgapped-keyring>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -1076,9 +1075,9 @@ "console.error": true }, "packages": { - "@metamask/assets-controllers>async-mutex": true, "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-filters>async-mutex": true, "@metamask/safe-event-emitter": true, "pify": true } @@ -1096,6 +1095,14 @@ "@metamask/utils": true } }, + "@metamask/eth-json-rpc-filters>async-mutex": { + "globals": { + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1586,13 +1593,13 @@ "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/assets-controllers>async-mutex": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, "@metamask/keyring-controller>ethereumjs-wallet": true, + "@metamask/name-controller>async-mutex": true, "@metamask/utils": true } }, @@ -1649,18 +1656,12 @@ "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util>create-hash": true - } - }, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true + "ethereumjs-util>create-hash": true, + "ethereumjs-util>rlp": true } }, "@metamask/logging-controller": { @@ -2309,6 +2310,7 @@ "@ethersproject/providers": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, @@ -2316,7 +2318,6 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/gas-fee-controller": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/utils": true, @@ -2443,15 +2444,6 @@ "uuid": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { "packages": { "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, @@ -2762,12 +2754,12 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>async-mutex": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -2820,15 +2812,6 @@ "@trezor/connect-web>tslib": true } }, - "@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -3880,9 +3863,9 @@ "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, + "eth-lattice-keyring>gridplus-sdk>rlp": true, "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "eth-lattice-keyring>rlp": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethereumjs-util>ethereum-cryptography>hash.js": true, "lodash": true @@ -3981,6 +3964,11 @@ "ganache>abstract-level>buffer": true } }, + "eth-lattice-keyring>gridplus-sdk>rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>secp256k1": { "packages": { "@metamask/ppom-validator>elliptic": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 0b8d3ad96621..da30479b362c 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -526,8 +526,8 @@ "@keystonehq/bc-ur-registry-eth": true, "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring": true, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": true, - "@keystonehq/metamask-airgapped-keyring>rlp": true, "browserify>buffer": true, + "ethereumjs-util>rlp": true, "uuid": true, "webpack>events": true } @@ -537,12 +537,17 @@ "@ethereumjs/tx": true, "@ethereumjs/tx>@ethereumjs/util": true, "@keystonehq/bc-ur-registry-eth": true, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": true, "@metamask/eth-trezor-keyring>hdkey": true, "browserify>buffer": true, - "eth-lattice-keyring>rlp": true, "uuid": true } }, + "@keystonehq/metamask-airgapped-keyring>@keystonehq/base-eth-keyring>rlp": { + "globals": { + "TextEncoder": true + } + }, "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store": { "packages": { "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, @@ -591,12 +596,6 @@ "@keystonehq/metamask-airgapped-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true } }, - "@keystonehq/metamask-airgapped-keyring>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true - } - }, "@lavamoat/lavadome-react": { "globals": { "Document.prototype": true, @@ -1361,9 +1360,9 @@ "console.error": true }, "packages": { - "@metamask/assets-controllers>async-mutex": true, "@metamask/eth-json-rpc-filters>@metamask/eth-query": true, "@metamask/eth-json-rpc-filters>@metamask/json-rpc-engine": true, + "@metamask/eth-json-rpc-filters>async-mutex": true, "@metamask/safe-event-emitter": true, "pify": true } @@ -1381,6 +1380,14 @@ "@metamask/utils": true } }, + "@metamask/eth-json-rpc-filters>async-mutex": { + "globals": { + "setTimeout": true + }, + "packages": { + "@trezor/connect-web>tslib": true + } + }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1871,13 +1878,13 @@ "@metamask/keyring-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask/assets-controllers>async-mutex": true, "@metamask/base-controller": true, "@metamask/browser-passworder": true, "@metamask/eth-sig-util": true, "@metamask/keyring-controller>@metamask/eth-hd-keyring": true, "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, "@metamask/keyring-controller>ethereumjs-wallet": true, + "@metamask/name-controller>async-mutex": true, "@metamask/utils": true } }, @@ -1934,18 +1941,12 @@ "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util": { "packages": { "@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography": true, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": true, "bn.js": true, "browserify>assert": true, "browserify>buffer": true, "browserify>insert-module-globals>is-buffer": true, - "ethereumjs-util>create-hash": true - } - }, - "@metamask/keyring-controller>ethereumjs-wallet>ethereumjs-util>rlp": { - "packages": { - "bn.js": true, - "browserify>buffer": true + "ethereumjs-util>create-hash": true, + "ethereumjs-util>rlp": true } }, "@metamask/logging-controller": { @@ -2594,6 +2595,7 @@ "@ethersproject/providers": true, "@metamask/eth-query": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@ethereumjs/tx": true, @@ -2601,7 +2603,6 @@ "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>@metamask/gas-fee-controller": true, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": true, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, "@metamask/utils": true, @@ -2728,15 +2729,6 @@ "uuid": true } }, - "@metamask/smart-transactions-controller>@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry": { "packages": { "@metamask/smart-transactions-controller>@metamask/transaction-controller>eth-method-registry>@metamask/ethjs-contract": true, @@ -3047,12 +3039,12 @@ "@metamask/eth-query": true, "@metamask/gas-fee-controller": true, "@metamask/metamask-eth-abis": true, + "@metamask/name-controller>async-mutex": true, "@metamask/network-controller": true, "@metamask/rpc-errors": true, "@metamask/transaction-controller>@metamask/base-controller": true, "@metamask/transaction-controller>@metamask/controller-utils": true, "@metamask/transaction-controller>@metamask/nonce-tracker": true, - "@metamask/transaction-controller>async-mutex": true, "@metamask/utils": true, "bn.js": true, "browserify>buffer": true, @@ -3105,15 +3097,6 @@ "@trezor/connect-web>tslib": true } }, - "@metamask/transaction-controller>async-mutex": { - "globals": { - "clearTimeout": true, - "setTimeout": true - }, - "packages": { - "@trezor/connect-web>tslib": true - } - }, "@metamask/user-operation-controller": { "globals": { "fetch": true @@ -4165,9 +4148,9 @@ "eth-lattice-keyring>gridplus-sdk>bitwise": true, "eth-lattice-keyring>gridplus-sdk>borc": true, "eth-lattice-keyring>gridplus-sdk>eth-eip712-util-browser": true, + "eth-lattice-keyring>gridplus-sdk>rlp": true, "eth-lattice-keyring>gridplus-sdk>secp256k1": true, "eth-lattice-keyring>gridplus-sdk>uuid": true, - "eth-lattice-keyring>rlp": true, "ethereumjs-util>ethereum-cryptography>bs58check": true, "ethereumjs-util>ethereum-cryptography>hash.js": true, "lodash": true @@ -4266,6 +4249,11 @@ "ganache>abstract-level>buffer": true } }, + "eth-lattice-keyring>gridplus-sdk>rlp": { + "globals": { + "TextEncoder": true + } + }, "eth-lattice-keyring>gridplus-sdk>secp256k1": { "packages": { "@metamask/ppom-validator>elliptic": true diff --git a/package.json b/package.json index 523f300806e9..0bde909457fd 100644 --- a/package.json +++ b/package.json @@ -244,9 +244,6 @@ "@babel/runtime@npm:^7.18.3": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.8.3": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.8.4": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", - "@metamask/keyring-controller@npm:^13.0.0": "patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch", - "@metamask/keyring-controller@npm:^12.2.0": "patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch", - "@metamask/keyring-controller@npm:^14.0.1": "patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch", "@spruceid/siwe-parser@npm:1.1.3": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", "@spruceid/siwe-parser@npm:2.1.0": "patch:@spruceid/siwe-parser@npm%3A2.1.0#~/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch", "@trezor/connect-web@npm:^9.2.2": "patch:@trezor/connect-web@npm%3A9.2.2#~/.yarn/patches/@trezor-connect-web-npm-9.2.2-a4de8e45fc.patch", @@ -314,7 +311,7 @@ "@metamask/gas-fee-controller": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch", "@metamask/jazzicon": "^2.0.0", "@metamask/keyring-api": "^8.0.0", - "@metamask/keyring-controller": "patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch", + "@metamask/keyring-controller": "^16.1.0", "@metamask/logging-controller": "^3.0.1", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^7.3.0", @@ -337,7 +334,7 @@ "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^15.0.2", - "@metamask/signature-controller": "^14.0.1", + "@metamask/signature-controller": "^16.0.0", "@metamask/smart-transactions-controller": "^10.1.2", "@metamask/snaps-controllers": "^9.0.0", "@metamask/snaps-execution-environments": "^6.4.0", diff --git a/ui/store/actions.ts b/ui/store/actions.ts index d5356de53f49..5b417a9a5e98 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -201,20 +201,15 @@ export function createNewVaultAndRestore( Buffer.from(seedPhrase, 'utf8').values(), ); - // TODO: Add types for vault - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let vault: any; return new Promise((resolve, reject) => { callBackgroundMethod( 'createNewVaultAndRestore', [password, encodedSeedPhrase], - (err, _vault) => { + (err) => { if (err) { reject(err); return; } - vault = _vault; resolve(); }, ); @@ -223,7 +218,6 @@ export function createNewVaultAndRestore( .then(() => { dispatch(showAccountsPage()); dispatch(hideLoadingIndication()); - return vault; }) .catch((err) => { dispatch(displayWarning(err.message)); diff --git a/yarn.lock b/yarn.lock index ea82d4dee42f..0748c38f545a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4286,6 +4286,20 @@ __metadata: languageName: node linkType: hard +"@keystonehq/metamask-airgapped-keyring@npm:^0.14.1": + version: 0.14.1 + resolution: "@keystonehq/metamask-airgapped-keyring@npm:0.14.1" + dependencies: + "@ethereumjs/tx": "npm:^4.0.2" + "@keystonehq/base-eth-keyring": "npm:^0.14.1" + "@keystonehq/bc-ur-registry-eth": "npm:^0.19.1" + "@metamask/obs-store": "npm:^9.0.0" + rlp: "npm:^2.2.6" + uuid: "npm:^8.3.2" + checksum: 10/8e34be8813c51488c7dc9b641ed17258740dda45fb72fe48670b077ecfb92273e0c5a2fbbab121b01d7e0906a3ec512f261fceb95da8089550021ab6a0c89c6b + languageName: node + linkType: hard + "@kurkle/color@npm:^0.3.0": version: 0.3.2 resolution: "@kurkle/color@npm:0.3.2" @@ -4808,7 +4822,7 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^6.0.1, @metamask/approval-controller@npm:^6.0.2": +"@metamask/approval-controller@npm:^6.0.2": version: 6.0.2 resolution: "@metamask/approval-controller@npm:6.0.2" dependencies: @@ -5729,21 +5743,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^5.1.0": - version: 5.1.0 - resolution: "@metamask/keyring-api@npm:5.1.0" - dependencies: - "@metamask/snaps-sdk": "npm:^3.1.1" - "@metamask/utils": "npm:^8.3.0" - "@types/uuid": "npm:^9.0.1" - superstruct: "npm:^1.0.3" - uuid: "npm:^9.0.0" - peerDependencies: - "@metamask/providers": ">=15 <17" - checksum: 10/f1c7ddfabd1d2ef45f8b05d1a1b3c71ce3bb4d8111e53d00acc71f68a18473c0e5f90583644c72ee85e52021857d9f9de3fc9aaec9819209b849408539909eca - languageName: node - linkType: hard - "@metamask/keyring-api@npm:^6.0.0, @metamask/keyring-api@npm:^6.1.1": version: 6.4.0 resolution: "@metamask/keyring-api@npm:6.4.0" @@ -5776,66 +5775,24 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:15.0.0": - version: 15.0.0 - resolution: "@metamask/keyring-controller@npm:15.0.0" - dependencies: - "@ethereumjs/util": "npm:^8.1.0" - "@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1" - "@metamask/base-controller": "npm:^5.0.1" - "@metamask/browser-passworder": "npm:^4.3.0" - "@metamask/eth-hd-keyring": "npm:^7.0.1" - "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-simple-keyring": "npm:^6.0.1" - "@metamask/keyring-api": "npm:^5.1.0" - "@metamask/message-manager": "npm:^8.0.1" - "@metamask/utils": "npm:^8.3.0" - async-mutex: "npm:^0.2.6" - ethereumjs-wallet: "npm:^1.0.1" - immer: "npm:^9.0.6" - checksum: 10/ae6c378ba68ee8e37fbf273a68f1ce0e5439e3f8c9b38a338fb9f9bd0fd6b12f9927e9c5f33a2ead87dd76e7665a71073187d744e7b810aecf24e2964b6af7a7 - languageName: node - linkType: hard - -"@metamask/keyring-controller@npm:^16.0.0": - version: 16.0.0 - resolution: "@metamask/keyring-controller@npm:16.0.0" +"@metamask/keyring-controller@npm:^16.0.0, @metamask/keyring-controller@npm:^16.1.0": + version: 16.1.0 + resolution: "@metamask/keyring-controller@npm:16.1.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" - "@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1" + "@keystonehq/metamask-airgapped-keyring": "npm:^0.14.1" "@metamask/base-controller": "npm:^5.0.2" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/eth-hd-keyring": "npm:^7.0.1" "@metamask/eth-sig-util": "npm:^7.0.1" "@metamask/eth-simple-keyring": "npm:^6.0.1" - "@metamask/keyring-api": "npm:^6.0.0" - "@metamask/message-manager": "npm:^8.0.2" - "@metamask/utils": "npm:^8.3.0" - async-mutex: "npm:^0.2.6" - ethereumjs-wallet: "npm:^1.0.1" - immer: "npm:^9.0.6" - checksum: 10/33c10f4c61acfa0f8de7a3d90c2dfc63be1d137866653254e3d5f68dbf4ee886415e2ab620009e5b82c2a112bfab6d09653f5ad16adeccecd87b89f1f1fa0b7c - languageName: node - linkType: hard - -"@metamask/keyring-controller@patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch": - version: 15.0.0 - resolution: "@metamask/keyring-controller@patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch::version=15.0.0&hash=3f5b2f" - dependencies: - "@ethereumjs/util": "npm:^8.1.0" - "@keystonehq/metamask-airgapped-keyring": "npm:^0.13.1" - "@metamask/base-controller": "npm:^5.0.1" - "@metamask/browser-passworder": "npm:^4.3.0" - "@metamask/eth-hd-keyring": "npm:^7.0.1" - "@metamask/eth-sig-util": "npm:^7.0.1" - "@metamask/eth-simple-keyring": "npm:^6.0.1" - "@metamask/keyring-api": "npm:^5.1.0" - "@metamask/message-manager": "npm:^8.0.1" + "@metamask/keyring-api": "npm:^6.1.1" + "@metamask/message-manager": "npm:^9.0.0" "@metamask/utils": "npm:^8.3.0" - async-mutex: "npm:^0.2.6" + async-mutex: "npm:^0.5.0" ethereumjs-wallet: "npm:^1.0.1" immer: "npm:^9.0.6" - checksum: 10/76116ea6ba8b85e1d3117f4ceebc31d27e591402d77afa9451f661e629b7c3a697d8fdf3b2a7ec536abf3d30d98fe92d2404c5356a2e8656c9886c38589b04f3 + checksum: 10/62ee509d236808048013d171cb700900729aac448809b12c3647d71465c467015dbd00318500f2ec58fded4a45a106a2415fc3a6ab123a956c1aa9189a8132e0 languageName: node linkType: hard @@ -5875,7 +5832,7 @@ __metadata: languageName: node linkType: hard -"@metamask/message-manager@npm:^8.0.1, @metamask/message-manager@npm:^8.0.2": +"@metamask/message-manager@npm:^8.0.2": version: 8.0.2 resolution: "@metamask/message-manager@npm:8.0.2" dependencies: @@ -5890,6 +5847,21 @@ __metadata: languageName: node linkType: hard +"@metamask/message-manager@npm:^9.0.0": + version: 9.0.0 + resolution: "@metamask/message-manager@npm:9.0.0" + dependencies: + "@metamask/base-controller": "npm:^5.0.2" + "@metamask/controller-utils": "npm:^10.0.0" + "@metamask/eth-sig-util": "npm:^7.0.1" + "@metamask/utils": "npm:^8.3.0" + "@types/uuid": "npm:^8.3.0" + jsonschema: "npm:^1.2.4" + uuid: "npm:^8.3.2" + checksum: 10/9b30deada10c64bf92594c3d9e372aa3f6e4cf5c95ab7681901d61b2651451d2aac10cd25390b1cca8fa08db764396758781d760edf3de190930d02acfcbb0c6 + languageName: node + linkType: hard + "@metamask/message-signing-snap@npm:^0.3.3": version: 0.3.3 resolution: "@metamask/message-signing-snap@npm:0.3.3" @@ -6074,11 +6046,11 @@ __metadata: linkType: hard "@metamask/permission-controller@npm:^9.0.2": - version: 9.1.1 - resolution: "@metamask/permission-controller@npm:9.1.1" + version: 9.1.0 + resolution: "@metamask/permission-controller@npm:9.1.0" dependencies: "@metamask/base-controller": "npm:^5.0.2" - "@metamask/controller-utils": "npm:^10.0.0" + "@metamask/controller-utils": "npm:^9.1.0" "@metamask/json-rpc-engine": "npm:^8.0.2" "@metamask/rpc-errors": "npm:^6.2.1" "@metamask/utils": "npm:^8.3.0" @@ -6088,7 +6060,7 @@ __metadata: nanoid: "npm:^3.1.31" peerDependencies: "@metamask/approval-controller": ^6.0.0 - checksum: 10/15b276863c8917779e6fa3aaa7df1cdc4e2342eb0f9a1cf75c84688bdc6ac63772315f8a2dbed68a3fa882e1d23dc61990d7c2308972f40c8241700b21f11677 + checksum: 10/bfae4c16cbe5b180b00ef029c3fa8d7f770247dfad4c0afc11822f4b0bd36373d6f749ac5507f23cf5dbd848096fa86ad2546be190c665c419cae58fcf0d7f00 languageName: node linkType: hard @@ -6348,24 +6320,24 @@ __metadata: languageName: node linkType: hard -"@metamask/signature-controller@npm:^14.0.1": - version: 14.0.1 - resolution: "@metamask/signature-controller@npm:14.0.1" +"@metamask/signature-controller@npm:^16.0.0": + version: 16.0.0 + resolution: "@metamask/signature-controller@npm:16.0.0" dependencies: - "@metamask/approval-controller": "npm:^6.0.1" - "@metamask/base-controller": "npm:^5.0.1" - "@metamask/controller-utils": "npm:^9.0.1" - "@metamask/keyring-controller": "npm:^14.0.1" + "@metamask/approval-controller": "npm:^6.0.2" + "@metamask/base-controller": "npm:^5.0.2" + "@metamask/controller-utils": "npm:^9.1.0" + "@metamask/keyring-controller": "npm:^16.0.0" "@metamask/logging-controller": "npm:^3.0.1" - "@metamask/message-manager": "npm:^8.0.1" + "@metamask/message-manager": "npm:^8.0.2" "@metamask/rpc-errors": "npm:^6.2.1" "@metamask/utils": "npm:^8.3.0" lodash: "npm:^4.17.21" peerDependencies: "@metamask/approval-controller": ^6.0.0 - "@metamask/keyring-controller": ^14.0.0 + "@metamask/keyring-controller": ^16.0.0 "@metamask/logging-controller": ^3.0.0 - checksum: 10/46fe7351048176d3e70adac9a9ca661d29240d7398dba7874083b8e67b99aa941c8636e84f263af5a992d526251c66bcc347af8437880fb3a434c1c22e8b595f + checksum: 10/410118733fa2fb95668beb2a0236a46330428cc1c0474a62b502762cd836ffbd380ba0397deecd32e046ef835d776727b33a88ddf4ca87630bba668a3e816f23 languageName: node linkType: hard @@ -10995,14 +10967,14 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/eslint-plugin@npm:7.10.0" + version: 7.11.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.11.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:7.10.0" - "@typescript-eslint/type-utils": "npm:7.10.0" - "@typescript-eslint/utils": "npm:7.10.0" - "@typescript-eslint/visitor-keys": "npm:7.10.0" + "@typescript-eslint/scope-manager": "npm:7.11.0" + "@typescript-eslint/type-utils": "npm:7.11.0" + "@typescript-eslint/utils": "npm:7.11.0" + "@typescript-eslint/visitor-keys": "npm:7.11.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -11013,7 +10985,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/dfe505cdf718dd29e8637b902e4c544c6b7d246d2051fd1936090423eb3dadfe2bd757de51e565e6fd80e74cf1918e191c26fee6df515100484ec3efd9b8d111 + checksum: 10/be95ed0bbd5b34c47239677ea39d531bcd8a18717a67d70a297bed5b0050b256159856bb9c1e894ac550d011c24bb5b4abf8056c5d70d0d5895f0cc1accd14ea languageName: node linkType: hard @@ -11036,20 +11008,20 @@ __metadata: linkType: hard "@typescript-eslint/parser@npm:^7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/parser@npm:7.10.0" + version: 7.11.0 + resolution: "@typescript-eslint/parser@npm:7.11.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.10.0" - "@typescript-eslint/types": "npm:7.10.0" - "@typescript-eslint/typescript-estree": "npm:7.10.0" - "@typescript-eslint/visitor-keys": "npm:7.10.0" + "@typescript-eslint/scope-manager": "npm:7.11.0" + "@typescript-eslint/types": "npm:7.11.0" + "@typescript-eslint/typescript-estree": "npm:7.11.0" + "@typescript-eslint/visitor-keys": "npm:7.11.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/1fa71049b2debf2f7f5366fb433e3d4c8e1591c2061a15fa8797d14623a2b6984340a59e7717acc013ce8c6a2ed32c5c0e811fe948b5936d41c2a5a09b61d130 + checksum: 10/0a32417aec62d7de04427323ab3fc8159f9f02429b24f739d8748e8b54fc65b0e3dbae8e4779c4b795f0d8e5f98a4d83a43b37ea0f50ebda51546cdcecf73caa languageName: node linkType: hard @@ -11073,22 +11045,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/scope-manager@npm:7.10.0" +"@typescript-eslint/scope-manager@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/scope-manager@npm:7.11.0" dependencies: - "@typescript-eslint/types": "npm:7.10.0" - "@typescript-eslint/visitor-keys": "npm:7.10.0" - checksum: 10/838a7a9573577d830b2f65801ce045abe6fad08ac7e04bac4cc9b2e5b7cbac07e645de9c79b9485f4cc361fe25da5319025aa0336fad618023fff62e4e980638 + "@typescript-eslint/types": "npm:7.11.0" + "@typescript-eslint/visitor-keys": "npm:7.11.0" + checksum: 10/79eff310405c6657ff092641e3ad51c6698c6708b915ecef945ebdd1737bd48e1458c5575836619f42dec06143ec0e3a826f3e551af590d297367da3d08f329e languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/type-utils@npm:7.10.0" +"@typescript-eslint/type-utils@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/type-utils@npm:7.11.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.10.0" - "@typescript-eslint/utils": "npm:7.10.0" + "@typescript-eslint/typescript-estree": "npm:7.11.0" + "@typescript-eslint/utils": "npm:7.11.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: @@ -11096,7 +11068,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/e62db9ffbfbccce60258108f7ed025005e04df18da897ff1b30049e3c10a47150e94c2fb5ac0ab9711ebb60517521213dcccbea6d08125107a87a67088a79042 + checksum: 10/ab6ebeff68a60fc40d0ace88e03d6b4242b8f8fe2fa300db161780d58777b57f69fa077cd482e1b673316559459bd20b8cc89a7f9f30e644bfed8293f77f0e4b languageName: node linkType: hard @@ -11121,10 +11093,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/types@npm:7.10.0" - checksum: 10/76075a7b87ddfff8e7e4aebf3d225e67bf79ead12a7709999d4d5c31611d9c0813ca69a9298f320efb018fe493ce3763c964a0e670a4c953d8eff000f10672c0 +"@typescript-eslint/types@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/types@npm:7.11.0" + checksum: 10/c6a0b47ef43649a59c9d51edfc61e367b55e519376209806b1c98385a8385b529e852c7a57e081fb15ef6a5dc0fc8e90bd5a508399f5ac2137f4d462e89cdc30 languageName: node linkType: hard @@ -11165,12 +11137,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.10.0" +"@typescript-eslint/typescript-estree@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.11.0" dependencies: - "@typescript-eslint/types": "npm:7.10.0" - "@typescript-eslint/visitor-keys": "npm:7.10.0" + "@typescript-eslint/types": "npm:7.11.0" + "@typescript-eslint/visitor-keys": "npm:7.11.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -11180,7 +11152,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/d11d0c45749c9bd4a187b6dfdf5600e36ba8c87667cd2020d9158667c47c32ec0bcb1ef3b7eee5577b667def5f7f33d8131092a0f221b3d3e8105078800f923f + checksum: 10/b98b101e42d3b91003510a5c5a83f4350b6c1cf699bf2e409717660579ffa71682bc280c4f40166265c03f9546ed4faedc3723e143f1ab0ed7f5990cc3dff0ae languageName: node linkType: hard @@ -11202,17 +11174,17 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/utils@npm:7.10.0" +"@typescript-eslint/utils@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/utils@npm:7.11.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:7.10.0" - "@typescript-eslint/types": "npm:7.10.0" - "@typescript-eslint/typescript-estree": "npm:7.10.0" + "@typescript-eslint/scope-manager": "npm:7.11.0" + "@typescript-eslint/types": "npm:7.11.0" + "@typescript-eslint/typescript-estree": "npm:7.11.0" peerDependencies: eslint: ^8.56.0 - checksum: 10/62327b585295f9c3aa2508aefac639d562b6f7f270a229aa3a2af8dbd055f4a4d230a8facae75a8a53bb8222b0041162072d259add56b541f8bdfda8da36ea5f + checksum: 10/fbef14e166a70ccc4527c0731e0338acefa28218d1a018aa3f5b6b1ad9d75c56278d5f20bda97cf77da13e0a67c4f3e579c5b2f1c2e24d676960927921b55851 languageName: node linkType: hard @@ -11264,13 +11236,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.10.0": - version: 7.10.0 - resolution: "@typescript-eslint/visitor-keys@npm:7.10.0" +"@typescript-eslint/visitor-keys@npm:7.11.0": + version: 7.11.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.11.0" dependencies: - "@typescript-eslint/types": "npm:7.10.0" + "@typescript-eslint/types": "npm:7.11.0" eslint-visitor-keys: "npm:^3.4.3" - checksum: 10/44b555a075bdff38e3e13c454ceaac50aa2546635e81f907d1ea84822c8887487d1d6bb4ff690f627da9585dc19ad07e228847c162c30bb06c46fb119899d8cc + checksum: 10/1f2cf1214638e9e78e052393c9e24295196ec4781b05951659a3997e33f8699a760ea3705c17d770e10eda2067435199e0136ab09e5fac63869e22f2da184d89 languageName: node linkType: hard @@ -25519,7 +25491,7 @@ __metadata: "@metamask/gas-fee-controller": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch" "@metamask/jazzicon": "npm:^2.0.0" "@metamask/keyring-api": "npm:^8.0.0" - "@metamask/keyring-controller": "patch:@metamask/keyring-controller@npm%3A15.0.0#~/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch" + "@metamask/keyring-controller": "npm:^16.1.0" "@metamask/logging-controller": "npm:^3.0.1" "@metamask/logo": "npm:^3.1.2" "@metamask/message-manager": "npm:^7.3.0" @@ -25543,7 +25515,7 @@ __metadata: "@metamask/safe-event-emitter": "npm:^3.1.1" "@metamask/scure-bip39": "npm:^2.0.3" "@metamask/selected-network-controller": "npm:^15.0.2" - "@metamask/signature-controller": "npm:^14.0.1" + "@metamask/signature-controller": "npm:^16.0.0" "@metamask/smart-transactions-controller": "npm:^10.1.2" "@metamask/snaps-controllers": "npm:^9.0.0" "@metamask/snaps-execution-environments": "npm:^6.4.0" From 9760248ff84e5a964b669bd5418dfd6cfbad13d4 Mon Sep 17 00:00:00 2001 From: Matteo Scurati Date: Tue, 25 Jun 2024 12:58:12 +0200 Subject: [PATCH 14/22] feat: add a new event to track (#25486) --- shared/constants/metametrics.ts | 1 + .../onboarding-flow/privacy-settings/privacy-settings.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index a5065ea2dadd..82c6a557be4f 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -580,6 +580,7 @@ export enum MetaMetricsEventName { OnboardingWalletAdvancedSettingsWithAuthenticating = 'Settings Updated with Authenticating', OnboardingWalletAdvancedSettingsWithoutAuthenticating = 'Settings Updated without Authenticating', OnboardingWalletAdvancedSettingsTurnOffProfileSyncing = 'Turn Off Profile Syncing', + OnboardingWalletAdvancedSettingsTurnOnProfileSyncing = 'Turn On Profile Syncing', OnboardingWalletImportAttempted = 'Wallet Import Attempted', OnboardingWalletVideoPlay = 'SRP Intro Video Played', OnboardingTwitterClick = 'External Link Clicked', diff --git a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js index 98b42d5e4d22..ea15950451c5 100644 --- a/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js +++ b/ui/pages/onboarding-flow/privacy-settings/privacy-settings.js @@ -253,6 +253,14 @@ export default function PrivacySettings() { ); } else { profileSyncingProps.setIsProfileSyncingEnabled(true); + trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: + MetaMetricsEventName.OnboardingWalletAdvancedSettingsTurnOnProfileSyncing, + properties: { + participateInMetaMetrics, + }, + }); } }; From a6990a2399da1fa19d3b14f53403694dadb4c185 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Tue, 25 Jun 2024 08:02:50 -0500 Subject: [PATCH 15/22] fix: UX: Remove legacy onboarding text (#25404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes the legacy onboarding text that was in place before the recent privacy notice update. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25404?quickstart=1) ## **Related issues** Fixes: N/A ## **Manual testing steps** 1. Go through onboarding 2. See new metametrics text 3. Look at code, don't see old onboarding text ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Jonathan Bursztyn --- app/_locales/de/messages.json | 40 -- app/_locales/el/messages.json | 40 -- app/_locales/en/messages.json | 40 -- app/_locales/es/messages.json | 40 -- app/_locales/fr/messages.json | 40 -- app/_locales/hi/messages.json | 40 -- app/_locales/id/messages.json | 40 -- app/_locales/ja/messages.json | 40 -- app/_locales/ko/messages.json | 40 -- app/_locales/pt/messages.json | 40 -- app/_locales/ru/messages.json | 40 -- app/_locales/tl/messages.json | 40 -- app/_locales/tr/messages.json | 40 -- app/_locales/vi/messages.json | 40 -- app/_locales/zh_CN/messages.json | 40 -- .../__snapshots__/metametrics.test.js.snap | 85 ++-- .../metametrics/metametrics.js | 403 +++++------------- .../metametrics/metametrics.test.js | 9 - 18 files changed, 156 insertions(+), 941 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 32affca2e5d6..731a805b1f2a 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Ich stimme zu" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Erlaubt Ihnen immer die Abmeldung über Einstellungen" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Diese Daten werden gesammelt und sind daher im Rahmen der Datenschutz-Grundverordnung (EU) 2016/679 anonym." - }, "onboardingMetametricsDescription": { "message": "Wir würden gerne grundlegende Nutzungs- und Diagnosedaten sammeln, um MetaMask zu verbessern. Sie sollten wissen, dass wir die Daten, die Sie uns hier zur Verfügung stellen, niemals verkaufen." }, "onboardingMetametricsDescription2": { "message": "Wenn wir Metriken sammeln, wird es immer wie folgt sein ..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask wird ..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask möchte Nutzungsdaten sammeln, um ein besseres Verständnis zu erhalten, wie unsere Nutzer mit MetaMask interagieren. Diese Daten werden verwendet, um Dienste anzubieten, was auf Ihrer Nutzung basierte Dienstverbesserungen einschließt." - }, "onboardingMetametricsDisagree": { "message": "Nein, danke!" }, @@ -3234,19 +3222,9 @@ "message": "Wir werden Sie informieren, wenn wir beschließen, diese Daten für andere Zwecke zu verwenden. Für weitere Informationen können Sie unsere $1 einsehen. Vergessen Sie nicht, dass Sie jederzeit zu Einstellungen gehen und sich abmelden können.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "*Wenn Sie Infura als Standard-RPC-Anbieter in MetaMask vewenden, speichert Infura Ihre IP-Adresse und Ihre Etherum-Wallet-Adresse, wenn Sie eine Transaktion senden. Wir speichern diese Daten in keinster Weise in unserem System, um sie miteinander in Verbindung zu bringen. Für weitere Informationen darüber, wie MetaMask und Infura in Hinischt auf Datenspeicherung zusammenarbeiten, sehen Sie sich bitte unser Update $1 an. Für mehr Informationen bezüglich unseren allgemeinen Datenschutzpraktiken, sehen Sie sich bitte unsere $2 an.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Datenschutzrichtlinie" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Datenschutzerklärung hier" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "hier" - }, "onboardingMetametricsModalTitle": { "message": "Benutzerdefiniertes Netzwerk hinzufügen" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Allgemein:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 speichert Ihre vollständige IP-Adresse*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 speichert Daten, die wir nicht benötigen, um die Dienstleistung zur Verfügung zu stellen (wie zum Beispiel Schlüssel, Adresse, Transaktions-Hashs oder Guthaben)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Nie" - }, "onboardingMetametricsNeverSellData": { "message": "$1 Sie können jederzeit über die Einstellungen entscheiden, ob Sie Ihre Nutzungsdaten freigeben oder löschen möchten.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Optional:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 Daten verkaufen. Niemals!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Anonymisierte Ereignisse für Klicks und Seitenaufrufe senden" - }, "onboardingMetametricsTitle": { "message": "Helfen Sie uns, MetaMask zu verbessern." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 57366b6653de..cfbe0bd874dd 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Συμφωνώ" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Σας επιτρέπεται πάντα να εξαιρεθείτε μέσω των Ρυθμίσεων" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Τα δεδομένα αυτά είναι συγκεντρωτικά και συνεπώς ανώνυμα για τους σκοπούς του Γενικού Κανονισμού για την Προστασία Δεδομένων (ΕΕ) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Θέλουμε να συλλέξουμε βασικά δεδομένα χρήσης και διάγνωσης για να βελτιώσουμε το MetaMask. Λάβετε υπόψη ότι δεν πουλάμε ποτέ τα δεδομένα που μας παρέχετε εδώ." }, "onboardingMetametricsDescription2": { "message": "Όταν συγκεντρώνουμε μετρήσεις, θα είναι πάντα..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "Το MetaMask θα..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "Το MetaMask θα ήθελε να συλλέγει δεδομένα χρήσης για να κατανοήσει καλύτερα τον τρόπο με τον οποίο οι χρήστες μας αλληλεπιδρούν με το MetaMask. Τα δεδομένα αυτά θα χρησιμοποιηθούν για την παροχή της υπηρεσίας, η οποία περιλαμβάνει τη βελτίωση της υπηρεσίας με βάση τη χρήση σας." - }, "onboardingMetametricsDisagree": { "message": "Όχι, ευχαριστώ" }, @@ -3234,19 +3222,9 @@ "message": "Θα σας ενημερώσουμε εάν αποφασίσουμε να χρησιμοποιήσουμε αυτά τα δεδομένα για άλλους σκοπούς. Για περισσότερες πληροφορίες, μπορείτε να ανατρέξετε στην $1. Να θυμάστε ότι μπορείτε να μεταβείτε στις ρυθμίσεις και να εξαιρεθείτε ανά πάσα στιγμή.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Όταν χρησιμοποιείτε την Infura ως τον προεπιλεγμένο πάροχο RPC στο MetaMask, η Infura θα συλλέγει τη διεύθυνση IP σας και τη διεύθυνση του πορτοφολιού σας στο Ethereum όταν αποστέλλετε μια συναλλαγή. Δεν αποθηκεύουμε αυτές τις πληροφορίες με τρόπο που να επιτρέπει στα συστήματά μας να συσχετίζουν αυτά τα δύο δεδομένα. Για περισσότερες πληροφορίες σχετικά με τον τρόπο με τον οποίο αλληλεπιδρούν το MetaMask και η Infura από την πλευρά της συλλογής δεδομένων, δείτε την ενημέρωσή μας $1. Για περισσότερες πληροφορίες σχετικά με τις πρακτικές απορρήτου μας γενικά, δείτε την ενημέρωσή μας $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Πολιτική Απορρήτου" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Πολιτική Απορρήτου εδώ" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "εδώ" - }, "onboardingMetametricsModalTitle": { "message": "Προσθήκη προσαρμοσμένου δικτύου" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Γενικές:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "To $1 συλλέγει την πλήρη διεύθυνση IP σας*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "To $1 συλλέγει πληροφορίες που δεν χρειαζόμαστε για την παροχή της υπηρεσίας (όπως κλειδιά, διευθύνσεις, αναλύσεις συναλλαγών ή υπόλοιπα)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Ποτέ" - }, "onboardingMetametricsNeverSellData": { "message": "$1 εσείς αποφασίζετε αν θέλετε να κοινοποιήσετε ή να διαγράψετε τα δεδομένα χρήσης σας μέσω των ρυθμίσεων ανά πάσα στιγμή.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Προαιρετικές:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "To $1 πουλάει δεδομένα. Ποτέ!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Αποστολή ανώνυμων συμβάντων κλικ και προβολής ιστοσελίδων" - }, "onboardingMetametricsTitle": { "message": "Βοηθήστε μας να βελτιώσουμε το MetaMask" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 814a31bdca50..a7573e179ef3 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3438,24 +3438,12 @@ "onboardingMetametricsAgree": { "message": "I agree" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Always allow you to opt-out via Settings" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679." - }, "onboardingMetametricsDescription": { "message": "We’d like to gather basic usage and diagnostics data to improve MetaMask. Know that we never sell the data you provide here." }, "onboardingMetametricsDescription2": { "message": "When we gather metrics, it will always be..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask will..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask would like to gather usage data to better understand how our users interact with MetaMask. This data will be used to provide the service, which includes improving the service based on your use." - }, "onboardingMetametricsDisagree": { "message": "No thanks" }, @@ -3463,19 +3451,9 @@ "message": "We’ll let you know if we decide to use this data for other purposes. You can review our $1 for more information. Remember, you can go to settings and opt out at any time.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* When you use Infura as your default RPC provider in MetaMask, Infura will collect your IP address and your Ethereum wallet address when you send a transaction. We don’t store this information in a way that allows our systems to associate those two pieces of data. For more information on how MetaMask and Infura interact from a data collection perspective, see our update $1. For more information on our privacy practices in general, see our $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Privacy Policy" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Privacy Policy here" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "here" - }, "onboardingMetametricsModalTitle": { "message": "Add custom network" }, @@ -3493,17 +3471,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "General:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 collect your full IP address*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 collect information we don’t need to provide the service (such as keys, addresses, transaction hashes, or balances)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Never" - }, "onboardingMetametricsNeverSellData": { "message": "$1 you decide if you want to share or delete your usage data via settings any time.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3511,13 +3478,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Optional:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 sell data. Ever!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Send anonymized click and pageview events" - }, "onboardingMetametricsTitle": { "message": "Help us improve MetaMask" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 02f7754a725b..f49efdbc7218 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "Acepto" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Siempre le permitirá excluirse a través de la Configuración" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Estos datos se agrupan y, por lo tanto, son anónimos a los efectos del Reglamento general de protección de datos (UE) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Nos gustaría recopilar datos básicos de uso y diagnóstico para mejorar MetaMask. Tenga presente que nunca venderemos los datos que nos proporcione aquí." }, "onboardingMetametricsDescription2": { "message": "Al recopilar métricas, siempre será..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask quisiera recopilar datos de uso para comprender mejor cómo nuestros usuarios interactúan con MetaMask. Estos datos se utilizarán para proporcionar el servicio, lo que incluye mejorarlo en función de su uso." - }, "onboardingMetametricsDisagree": { "message": "No, gracias" }, @@ -3231,19 +3219,9 @@ "message": "Le informaremos si decidimos usar estos datos para otros fines. Puede consultar $1 para obtener más información. Recuerde que puede acceder a la configuración y excluirse en cualquier momento.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "*Al utilizar Infura como su proveedor de RPC predeterminado en MetaMask, Infura recopilará su dirección IP y la dirección de su monedero de Ethereum cuando envíe una transacción. No almacenamos esta información de una manera que permita qie nuestros sistemas asocien esos dos datos. Para obtener más información sobre cómo interactúan MetaMask e Infura desde la perspectiva de la recopilación de datos, consulte nuestra actualización $1. Para obtener más información sobre nuestras prácticas de privacidad en general, consulte nuestra $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Política de privacidad" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Política de privacidad aquí" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "aquí" - }, "onboardingMetametricsModalTitle": { "message": "Agregar red personalizada" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Generales:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 recopilará su dirección IP completa*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 recopilará información que no necesitamos para brindar el servicio (como claves, direcciones, hashes de transacciones o saldos)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Nunca" - }, "onboardingMetametricsNeverSellData": { "message": "$1 usted decide si desea compartir o eliminar sus datos de uso a través de la configuración en cualquier momento.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Opcionales:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 venderá datos. ¡Jamás!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Enviará eventos de vistas de página y clics anónimos" - }, "onboardingMetametricsTitle": { "message": "Ayúdenos a mejorar MetaMask" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 0f6beff29ece..c44d687aeaf6 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "J’accepte" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Vous pourrez toujours vous désinscrire depuis les Paramètres" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Conformément au règlement général sur la protection des données (UE) 2016/679, ces données sont agrégées pour préserver l’anonymat des utilisateurs." - }, "onboardingMetametricsDescription": { "message": "Nous aimerions recueillir des données d’utilisation et de diagnostic de base afin d’améliorer MetaMask. Sachez que nous ne vendons jamais les données que vous nous fournissez ici." }, "onboardingMetametricsDescription2": { "message": "Lorsque nous recueillons des données, elles sont toujours…" }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask souhaite recueillir des données d’utilisation afin de mieux comprendre comment les utilisateurs interagissent avec MetaMask. Ces données seront utilisées pour améliorer les services que nous proposons ainsi que l’expérience utilisateur." - }, "onboardingMetametricsDisagree": { "message": "Non merci" }, @@ -3234,19 +3222,9 @@ "message": "Nous vous informerons si nous décidons d’utiliser ces données à d’autres fins. Pour plus d’informations, vous pouvez consulter notre $1. N’oubliez pas que vous pouvez aller dans les paramètres et vous désinscrire à tout moment.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Sachez que lorsque vous définissez Infura comme fournisseur de RPC par défaut dans MetaMask, votre adresse IP et l’adresse de votre portefeuille Ethereum seront communiquées à Infura pour valider les transactions. Ces données sont stockées séparément sur nos systèmes pour préserver l’anonymat des utilisateurs. Pour plus d’informations sur la façon dont MetaMask et Infura collectent les données, consultez nos dernières mises à jour $1. Pour plus d’informations, consultez notre $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Politique de confidentialité" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Politique de confidentialité ici" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "ici" - }, "onboardingMetametricsModalTitle": { "message": "Ajouter un réseau personnalisé" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Général :" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "Nous ne collectons $1 l’intégralité de votre adresse IP*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "Nous ne collectons $1 d’informations dont nous n’avons pas besoin pour fournir nos services (comme les clés, les adresses, les hachages de transactions ou les soldes)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Jamais" - }, "onboardingMetametricsNeverSellData": { "message": "$1 vous pouvez décider à tout moment de partager ou de supprimer vos données d’utilisation dans les paramètres.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Facultatif :" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "Nous ne vendons $1 vos données !", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Envoyer des données anonymisées sur les clics effectués et les pages vues" - }, "onboardingMetametricsTitle": { "message": "Aidez-nous à améliorer MetaMask" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index fcc67f89f409..7ebd3e958736 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "मैं सहमत हूं" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "सेटिंग के माध्यम से आपको हमेशा ऑप्ट-आउट करने की अनुमति देता है" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "यह डेटा एग्रीगेट किया गया है और इसलिए सामान्य डेटा संरक्षण विनियम (EU) 2016/679 के उद्देश्यों के लिए अज्ञात है।" - }, "onboardingMetametricsDescription": { "message": "हम MetaMask को बेहतर बनाने के लिए बुनियादी यूसेज और डाएगोनोस्टिक्स डेटा कलेक्ट करना चाहेंगे। जान लें कि हम आपके द्वारा यहां उपलब्ध कराया गया डेटा कभी नहीं बेचते हैं।" }, "onboardingMetametricsDescription2": { "message": "जब हम मेट्रिक्स इकट्ठा करते हैं, तो यह हमेशा... रहेगा" }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask करेगा..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask यह समझने के लिए इस्तेमाल डेटा एकत्र करना चाहता है कि हमारे यूज़र MetaMask से कैसे इंटरैक्ट करते हैं। इस डेटा का इस्तेमाल सर्विस प्रदान करने के लिए किया जाएगा, जिसमें आपके इस्तेमाल के आधार पर सर्विस में सुधार करना शामिल है।" - }, "onboardingMetametricsDisagree": { "message": "जी नहीं, धन्यवाद" }, @@ -3231,19 +3219,9 @@ "message": "यदि हम इस डेटा का उपयोग अन्य उद्देश्यों के लिए करने का निर्णय लेते हैं तो हम आपको बताएंगे। अधिक जानकारी के लिए आप हमारे $1 की समीक्षा कर सकते हैं। याद रखें, आप किसी भी समय सेटिंग्स में जाकर ऑप्ट आउट कर सकते हैं।", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* जब आप MetaMask में अपने डिफॉल्ट RPC प्रोवाइडर के रूप में Infura का इस्तेमाल करते हैं, तो जब आप ट्रांसेक्शन भेजते हैं तो Infura आपका IP एड्रेस और आपके Ethereum वॉलेट का एड्रेस एकत्र कर लेगा। हम इस जानकारी को इस तरह से स्टोर नहीं करते हैं जिससे हमारे सिस्टम डेटा के उन दो टुकड़ों को जोड़ सकें। डेटा कलेक्शन के नज़रिए से MetaMask और Infura कैसे इंटरैक्ट करते हैं, इस बारे में अधिक जानकारी के लिए, हमारा अपडेट $1 देखें। सामान्य तौर पर हमारी गोपनीयता की कार्यप्रणाली के बारे में अधिक जानकारी के लिए, हमारा $2 देखें।", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "गोपनीयता नीति" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "यहां गोपनीयता नीति" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "यहां" - }, "onboardingMetametricsModalTitle": { "message": "कस्टम नेटवर्क जोड़ें" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "सामान्य:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 आपका पूरा IP एड्रेस एकत्र करते हैं*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 ऐसी जानकारी एकत्र करते हैं जिसे हमें सर्विस प्रदान करने की आवश्यकता नहीं है (जैसे keys, एड्रेस, ट्रांसेक्शन हैशेज़ या बैलेंस)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "कभी नहीं" - }, "onboardingMetametricsNeverSellData": { "message": "$1 आप तय करते हैं कि आप किसी भी समय सेटिंग्स के माध्यम से अपना यूसेज डेटा शेयर करना चाहते हैं या हटाना चाहते हैं।", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "वैकल्पिक:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 डेटा बेचते हैं। कभी!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "अज्ञात क्लिक और पेजव्यू इवेंट भेजें" - }, "onboardingMetametricsTitle": { "message": "MetaMask को बेहतर बनाने में हमारी मदद करें" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 1bb94a0d3e04..45bc8912c676 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Saya setuju" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Selalu izinkan Anda untuk keluar melalui Pengaturan" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Data ini dikumpulkan sehingga bersifat anonim untuk tujuan Peraturan Perlindungan Data Umum (UE) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Kami ingin mengumpulkan data penggunaan dasar dan diagnostik untuk meningkatkan MetaMask. Pahami bahwa kami tidak pernah menjual data yang Anda berikan di sini." }, "onboardingMetametricsDescription2": { "message": "Saat kami mengumpulkan metrik, maka akan selalu..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask akan..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask ingin mengumpulkan data penggunaan untuk lebih memahami cara pengguna kami berinteraksi dengan MetaMask. Data ini akan digunakan untuk menyediakan layanan, termasuk meningkatkan layanan berdasarkan penggunaan Anda." - }, "onboardingMetametricsDisagree": { "message": "Tidak, terima kasih" }, @@ -3234,19 +3222,9 @@ "message": "Kami akan memberitahukan keputusan untuk menggunakan data ini dengan tujuan lain. Anda dapat meninjau $1 kami untuk informasi selengkapnya. Ingat, Anda dapat membuka pengaturan dan memilih keluar setiap saat.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Saat Anda menggunakan Infura sebagai penyedia RPC awal di MetaMask, Infura akan mengumpulkan alamat IP dan alamat dompet Ethereum Anda saat mengirim transaksi. Kami tidak menyimpan informasi ini dengan cara yang memungkinkan sistem kami menghubungkan kedua bagian data tersebut. Untuk informasi lebih lanjut seputar cara MetaMask dan Infura berinteraksi dari perspektif pengumpulan data, lihat pembaruan $1. Untuk informasi lebih lanjut seputar praktik privasi kami secara umum, lihat $2 kami.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Kebijakan Privasi" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Kebijakan Privasi di sini" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "di sini" - }, "onboardingMetametricsModalTitle": { "message": "Tambahkan jaringan khusus" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Umum:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 mengumpulkan alamat IP lengkap Anda*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 mengumpulkan informasi yang tidak kami perlukan untuk menyediakan layanan (seperti kunci, alamat, hash transaksi, atau saldo)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Jangan" - }, "onboardingMetametricsNeverSellData": { "message": "$1 memutuskan jika Anda ingin membagikan atau menghapus data penggunaan melalui pengaturan setiap saat.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Opsional:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 menjual data. Jangan pernah!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Kirim klik anonim dan acara tampilan laman" - }, "onboardingMetametricsTitle": { "message": "Bantu kami meningkatkan MetaMask" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index a5226fd27192..4ca1b2faaf7f 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "同意します" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "いつでも設定からオプトアウトできるようにします" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "一般データ保護規則 (EU) 2016/679 の目的に従い、このデータは集約され匿名化されます。" - }, "onboardingMetametricsDescription": { "message": "MetaMaskの改善を目的に、基本的な使用状況および診断データを収集したいと思います。ここで提供されるデータが販売されることはありません。" }, "onboardingMetametricsDescription2": { "message": "指標を収集する際、常に次の条件が適用されます..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMaskは..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMaskは、ユーザーによるMetaMaskの使用状況をより詳細に把握するため、使用データを収集したいと考えています。このデータは、使用状況に基づくサービスの改善を含め、サービスの提供を目的に使用されます。" - }, "onboardingMetametricsDisagree": { "message": "結構です" }, @@ -3231,19 +3219,9 @@ "message": "このデータを他の目的に使用する際は、お知らせします。詳細は当社の$1をご覧ください。設定でいつでもオプトアウトできます。", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* MetaMaskでInfuraをデフォルトのRPCプロバイダーとして使用する場合、Infuraはトランザクションの送信時にユーザーのIPアドレスおよびイーサリアムウォレットアドレスを収集します。当社がこれらの情報を、システムによりこれら2つのデータが関連付けられる形で保管することはありません。データ収集の観点から見たMetaMaskとInfuraとのやり取りに関する詳細は、当社の最新の$1をご覧ください。当社のプライバシー慣行全般に関する詳細は、$2をご覧ください。", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "プライバシー ポリシー" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "こちらのプライバシーポリシー" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "こちら" - }, "onboardingMetametricsModalTitle": { "message": "カスタムネットワークを追加" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "一般:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "ユーザーの完全なIPアドレスを収集することは、$1*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "サービスの提供に不要な情報 (キー、アドレス、トランザクションハッシュ、残高) を収集することは、$1", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "一切ありません" - }, "onboardingMetametricsNeverSellData": { "message": "$1 使用状況データを共有するか削除するかは、設定でいつでも指定できます。", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "任意:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "データを販売することは$1。絶対です!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "匿名のクリックおよびページ閲覧イベントを送信" - }, "onboardingMetametricsTitle": { "message": "MetaMaskの改善にご協力ください" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d9ccf970cbd6..af94a9026392 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "동의함" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "언제든 설정을 통해 옵트아웃할 수 있습니다." - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "이 데이터는 집계 처리된 정보이며 일반 데이터 보호 규정 (EU) 2016/679의 목적에 따라 익명으로 관리됩니다." - }, "onboardingMetametricsDescription": { "message": "MetaMask를 개선하기 위해 기본적인 사용 및 진단 데이터를 수집하고자 합니다. MetaMask는 제공받은 데이터를 절대 판매하지 않습니다." }, "onboardingMetametricsDescription2": { "message": "메트릭을 수집할 때는 항상..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask에서는..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask는 사용자가 MetaMask를 어떻게 사용하는지 이해하기 위해 기본적인 사용 데이터를 수집하고자 합니다. 이 데이터는 사용자 경험을 통한 서비스 개선에 사용됩니다." - }, "onboardingMetametricsDisagree": { "message": "괜찮습니다" }, @@ -3231,19 +3219,9 @@ "message": "이 데이터를 다른 목적으로 사용하기로 결정하면 알려드리겠습니다. 자세한 내용은 $1을(를) 참고하세요. 언제든지 설정으로 이동하여 해제할 수 있습니다.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* MetaMask에서 Infura를 기본 RPC 공급업체로 이용하는 경우, 트랜잭션 전송 시 Infura가 IP 주소와 이더리움 지갑 주소 정보를 수집합니다. MetaMask는 해당 두 정보를 연계할 수 있는 방식으로 정보를 저장하지 않습니다. 데이터 수집 관점에서 MetaMask와 Infura가 인터렉션하는 방법에 대한 자세한 내용은 $1 업데이트를 참조하세요. 일반적인 개인정보 처리방침에 대한 자세한 내용은 $2에서 확인하세요.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "개인정보 처리방침" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "개인정보 처리방침" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "여기" - }, "onboardingMetametricsModalTitle": { "message": "맞춤 네트워크 추가" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "일반:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1에서는 사용자의 IP 주소 전체를 수집합니다.", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1에서는 서비스 제공에 필요하지 않은 정보(예: 키, 주소, 트랜잭션 해시 또는 잔액)를 수집합니다.", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "절대로" - }, "onboardingMetametricsNeverSellData": { "message": "$1 언제든지 설정을 통해 사용 데이터를 공유할지 삭제할지 결정할 수 있습니다.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "선택 사항:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1에서는 데이터를 판매합니다. 항상요!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "익명 클릭 및 페이지뷰 이벤트 보내기" - }, "onboardingMetametricsTitle": { "message": "MetaMask 개선에 도움을 주세요" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 971129f0e027..fe80de66b981 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Concordo" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Permite que você cancele a inscrição a qualquer momento nas Configurações" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Esses dados são agregados e, portanto, anônimos para os fins do Regulamento Geral sobre a Proteção de Dados (UE) de 2016/679." - }, "onboardingMetametricsDescription": { "message": "Gostaríamos de coletar dados básicos de uso para melhorar a MetaMask. Saiba que nunca vendemos os dados que você fornece aqui." }, "onboardingMetametricsDescription2": { "message": "Quando coletamos as métricas, elas sempre são..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "A MetaMask..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "A MetaMask gostaria de reunir dados de uso para entender melhor como nossos usuários interagem com a MetaMask. Esses dados serão usados para prestar o serviço, o que inclui melhorá-lo com base em seu uso." - }, "onboardingMetametricsDisagree": { "message": "Não, obrigado" }, @@ -3234,19 +3222,9 @@ "message": "Informaremos a você se decidirmos usar esses dados para outras finalidades. Você pode analisar nossa $1 para obter mais informações. Lembre-se: você pode acessar as configurações e revogar a permissão a qualquer momento.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Quando você usa a Infura como seu provedor RPC padrão na MetaMask, a Infura coleta seu endereço IP e da carteira de Ethereum quando você envia uma transação. Não armazenamos essas informações de forma que permita aos nossos sistemas cruzarem os dois fragmentos de dados. Para obter mais informações sobre como a MetaMask e a Infura interagem da perspectiva da coleta de dados, veja nossa atualização $1. Para obter mais informações sobre nossas práticas de privacidade em geral, veja nossa $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Política de Privacidade" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Política de Privacidade aqui" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "aqui" - }, "onboardingMetametricsModalTitle": { "message": "Adicionar rede personalizada" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Gerais:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 coletará seu endereço IP completo*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 coletará informações de que não precisamos para prestar o serviço (tais como chaves, endereços, hashes de transações ou saldos)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Nunca" - }, "onboardingMetametricsNeverSellData": { "message": "$1 você decide se quer compartilhar ou excluir seus dados de uso nas configurações, a qualquer momento.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Opcionais:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 venderá dados. Jamais!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Enviará eventos de cliques e visualizações de páginas anonimizados" - }, "onboardingMetametricsTitle": { "message": "Ajude-nos a melhorar a MetaMask" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 2ebd1f371e74..3106cf4540f4 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Я согласен(-на)" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Всегда разрешать вам отказываться в Настройках" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Эти данные агрегированы и, следовательно, являются анонимными для целей Общего регламента по защите данных (ЕС) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Мы хотели бы собрать базовые данные об использовании и диагностике для улучшения MetaMask. Помните, что мы никогда не продаем данные, которые вы здесь предоставляете." }, "onboardingMetametricsDescription2": { "message": "Когда мы собираем показатели, они всегда будут..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask хотел бы собрать данные об использовании, чтобы лучше понять, как наши пользователи взаимодействуют с MetaMask. Эти данные будут использоваться для предоставления обслуживания, в том числе его улучшения услуги на основе вашего использования." - }, "onboardingMetametricsDisagree": { "message": "Нет, спасибо" }, @@ -3234,19 +3222,9 @@ "message": "Мы сообщим вам, если решим использовать эти данные для других целей. Вы можете ознакомиться с нашей $1 для получения дополнительной информации. Помните, что вы можете перейти в настройки и отказаться в любой момент.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Когда вы используете Infura в качестве поставщика RPC по умолчанию в MetaMask, Infura будет собирать ваш IP-адрес и адрес вашего кошелька Ethereum при отправке транзакции. Мы не храним эту информацию таким образом, чтобы наши системы могли связать эти две части данных. Для получения дополнительной информации о том, как MetaMask и Infura взаимодействуют с точки зрения сбора данных, см. нашу обновленную $1. Для получения дополнительной информации о нашей политике конфиденциальности в целом см. нашу $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Политикой конфиденциальности" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Политику конфиденциальности здесь" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "здесь" - }, "onboardingMetametricsModalTitle": { "message": "Добавить пользовательскую сеть" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Общедоступными:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 не сохраняет ваш полный IP-адрес*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 не будет собирать информацию, которая нам не нужна для предоставления услуги (например, ключи, адреса, хэши транзакций или остатки)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Никогда" - }, "onboardingMetametricsNeverSellData": { "message": "$1 вы в любое время решаете, хотите ли вы поделиться своими данными об использовании или удалить их в настройках.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Необязательными:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 не продает данные. Никогда!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Отправлять анонимизированные события кликов и просмотров страниц" - }, "onboardingMetametricsTitle": { "message": "Помогите нам улучшить MetaMask" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c9f382c3644d..4a570071faa5 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "Sumasang-ayon ako" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Palagi kang pinapayagan na mag-opt out sa pamamagitan ng Mga Setting" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Ang datos na ito ay pinagsama-sama at samakatuwid ay hindi nagpapakilala para sa mga layunin ng General Data Protection Regulation (EU) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Nais naming mangalap ng data para sa batayang paggamit upang mapahusay ang MetaMask. Dapat mong malaman na hindi namin ibebenta ang data na iyong ibibigay rito." }, "onboardingMetametricsDescription2": { "message": "Kapag kami ay nangangalap ng metrics, ito ay palaging..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "Ang MetaMask ay..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "Nais ng MetaMask na mangalap ng datos ng paggamit upang mas maunawaan kung paano nakikipag-ugnayan ang aming mga user sa MetaMask. Gagamitin ang datos na ito upang ibigay ang serbisyo, na kinabibilangan ng pagpapabuti ng serbisyo batay sa iyong paggamit." - }, "onboardingMetametricsDisagree": { "message": "Salamat nalang" }, @@ -3231,19 +3219,9 @@ "message": "Ipapaalam namin sa iyo kung magdesisyon kaming gamitin ang data na ito para sa ibang layunin. Maaari mong suriin ang aming $1 para sa karagdagang impormasyon. Tandaan, maaari kang magpunta sa mga setting at mag-opt out anumang oras.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Kapag ginamit mo ang Infura bilang iyong default na provider ng RPC sa MetaMask, kukunin ng Infura ang iyong IP address at ang iyong Ethereum wallet address kapag nagpadala ka ng transaksyon. Hindi namin iniimbak ang impormasyong ito sa paraan na nagpapahintulot sa aming mga system na iugnay ang dalawang piraso ng data na iyon. Para sa higit pang impormasyon kung paano nakikipag-ugnayan ang MetaMask at Infura mula sa pananaw ng pagkolekta ng data, tingnan ang aming update na $1. Para sa higit pang impormasyon sa aming mga kasanayan sa pagkapribado sa pangkalahatan, tingnan ang aming $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Patakaran sa Pagkapribado" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Patakaran sa Pagkapribado dito" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "dito" - }, "onboardingMetametricsModalTitle": { "message": "Magdagdag ng custom na network" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Pangkalahatan:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "Kinokolekta ng $1 ang iyong buong IP address*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "Kinokolekta ng $1 ang impormasyon na hindi namin kailangan para ibigay ang serbisyo (tulad ng mga key, address, hash ng transaksyon, o balanse)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Hindi kailanman" - }, "onboardingMetametricsNeverSellData": { "message": "$1 ikaw ang magdedesisyon kung nais mong ibahagi o burahin ang iyong data sa paggamit sa pamamagitan ng mga setting anumang oras.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Opsyonal:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "Ang $1 ay nagbebenta ng datos. Kailanman!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Magpapadala ng mga anonymous na kaganapang pag-click at pagtingin sa page" - }, "onboardingMetametricsTitle": { "message": "Tulungan kaming mapahusay ang MetaMask" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 1dbad4ab24e5..51aafd650946 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -3209,24 +3209,12 @@ "onboardingMetametricsAgree": { "message": "Kabul ediyorum" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Her zaman Ayarlar kısmından vazgeçebilmenize izin verir" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Bu veriler toplanmıştır ve bu nedenle 2016/679 sayılı Genel Veri Koruma Tüzüğü (AB) maksadıyla isimsizdir." - }, "onboardingMetametricsDescription": { "message": "MetaMask'i iyileştirmek için temel kullanım ve tanılama verilerini toplamak istiyoruz. Burada sunduğunuz verileri asla satmadığımızı bilmenizi isteriz." }, "onboardingMetametricsDescription2": { "message": "Ölçümleri toplarken bu her zaman aşağıdaki gibi olacaktır..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask kullanıcılarımızn MetaMask ile nasıl etkileşimde bulunduklarını daha iyi anlamak amacıyla kullanım verilerini toplamak ister. Bu veriler, hizmeti kullanımınıza göre geliştirmek de dahil olmak üzere hizmeti sunmak için kullanılacaktır." - }, "onboardingMetametricsDisagree": { "message": "Hayır, istemiyorum" }, @@ -3234,19 +3222,9 @@ "message": "Bu verileri başka amaçlar için kullanmaya karar vermemiz durumunda sizi bilgilendireceğiz. Daha fazla bilgi için $1 bölümümüzü inceleyebilirsiniz. Unutmayın, dilediğiniz zaman ayarlar kısmına giderek vazgeçebilirsiniz.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* MetaMask'te varsayılan RPC sağlayıcınız olarak Infura'yı kullandığınızda, siz bir işlem gönderdiğinizde Infura tarafından IP adresiniz ve Ethereum cüzdan adresiniz toplanır. Bu bilgileri sistemlerimizin bu iki veri parçasını ilişkilendirebilmesini sağlayan şekilde saklamayız. MetaMask ve Infura'nın veri toplama açısından nasıl etkileşimde bulundukları hakkında daha fazla bilgi için lütfen güncel $1 bölümümüze bakın. Genel olarak gizlilik uygulamalarımız hakkında daha fazla bilgi için $2 bölümümüze bakın.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Gizlilik Politikası" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "buradan Gizlilik Politikası" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "burada" - }, "onboardingMetametricsModalTitle": { "message": "Özel ağ ekle" }, @@ -3264,17 +3242,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Genel:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 tam IP adresinizi toplar*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 ihtiyacımız olmayan bilgileri (anahtarlar, adresler, işlem hash değerleri veya bakiyeler gibi) hizmeti sunmak için toplar", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Hiçbir Zaman" - }, "onboardingMetametricsNeverSellData": { "message": "$1 kullanım verilerinizi paylaşmak veya silmek isteyip istemediğinize ayarlar kısmından dilediğiniz zaman siz karar verirsiniz.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3282,13 +3249,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "İsteğe bağlı:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 verileri satmaz. Asla!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "İsimsiz tıklama ve sayfa görüntüleme etkinlikleri gönder" - }, "onboardingMetametricsTitle": { "message": "MetaMask'i iyileştirmemize yardımcı olun" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index bbbcffe561d6..6f2cba66d83b 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "Tôi đồng ý" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "Luôn cho phép bạn chọn không tham gia thông qua phần Cài đặt" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "Do đó, dữ liệu này được tổng hợp và ẩn danh theo các mục đích của Quy định bảo vệ dữ liệu chung (EU) 2016/679." - }, "onboardingMetametricsDescription": { "message": "Chúng tôi muốn thu thập dữ liệu sử dụng và chẩn đoán cơ bản để cải tiến MetaMask. Xin lưu ý, chúng tôi không bao giờ bán dữ liệu mà bạn cung cấp ở đây." }, "onboardingMetametricsDescription2": { "message": "Khi thu thập số liệu, chúng tôi sẽ luôn cam kết điều này..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask sẽ..." - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask muốn thu thập dữ liệu sử dụng để hiểu rõ hơn cách người dùng tương tác với MetaMask. Dữ liệu này sẽ được sử dụng để cung cấp dịch vụ, bao gồm cả cải thiện dịch vụ dựa trên quá trình sử dụng của bạn." - }, "onboardingMetametricsDisagree": { "message": "Không, cảm ơn" }, @@ -3231,19 +3219,9 @@ "message": "Chúng tôi sẽ thông báo cho bạn nếu chúng tôi quyết định sử dụng dữ liệu này cho các mục đích khác. Bạn có thể xem lại $1 của chúng tôi để biết thêm thông tin. Lưu ý, bạn có thể truy cập cài đặt và chọn không tham gia bất kỳ lúc nào.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* Khi bạn sử dụng Infura làm nhà cung cấp RPC mặc định trong MetaMask, Infura sẽ thu thập địa chỉ IP và địa chỉ ví Ethereum của bạn khi bạn gửi giao dịch. Chúng tôi không lưu trữ thông tin này để cho phép hệ thống của chúng tôi liên kết hai loại dữ liệu đó. Để biết thêm thông tin về cách MetaMask và Infura tương tác trong việc thu thập dữ liệu, hãy xem bản cập nhật $1 của chúng tôi. Để biết thêm thông tin về các phương pháp bảo vệ quyền riêng tư của chúng tôi nói chung, hãy xem $2.", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "Chính sách quyền riêng tư" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "Chính sách quyền riêng tư tại đây" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "tại đây" - }, "onboardingMetametricsModalTitle": { "message": "Thêm mạng tùy chỉnh" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "Chung:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1 thu thập địa chỉ IP đầy đủ của bạn*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 thu thập các thông tin mà chúng tôi không cần để cung cấp dịch vụ (chẳng hạn như khóa, địa chỉ, mã băm giao dịch hoặc số dư)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "Không bao giờ" - }, "onboardingMetametricsNeverSellData": { "message": "$1 bạn có thể chọn chia sẻ hoặc xóa dữ liệu sử dụng của mình trong phần cài đặt bất kỳ lúc nào.", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "Không bắt buộc:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 không bao giờ bán dữ liệu!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "Gửi các sự kiện nhấp chuột & lượt xem trang ẩn danh" - }, "onboardingMetametricsTitle": { "message": "Giúp chúng tôi cải thiện MetaMask" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 2b73be877bbe..ca5b01c48545 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -3206,24 +3206,12 @@ "onboardingMetametricsAgree": { "message": "我同意" }, - "onboardingMetametricsAllowOptOutLegacy": { - "message": "始终允许您通过设置选择退出" - }, - "onboardingMetametricsDataTermsLegacy": { - "message": "此数据是汇总数据,因而可以保持匿名,以遵守《通用数据保护条例》(欧盟)2016/679。" - }, "onboardingMetametricsDescription": { "message": "我们希望收集基本的使用和诊断数据,以改进 MetaMask。请注意,我们绝不会出卖您在此处提供的数据。" }, "onboardingMetametricsDescription2": { "message": "当我们收集指标时,总是..." }, - "onboardingMetametricsDescription2Legacy": { - "message": "MetaMask 将会......" - }, - "onboardingMetametricsDescriptionLegacy": { - "message": "MetaMask 希望收集使用数据,以更好地了解我们的用户如何与 MetaMask 交互。这些数据将用于提供服务,包括根据您的使用情况改进服务。" - }, "onboardingMetametricsDisagree": { "message": "不,谢谢" }, @@ -3231,19 +3219,9 @@ "message": "如果我们决定将这些数据用于其他目的,我们会通知您。您可以查看我们的 $1 以了解更多信息。请记住,您可以随时转到设置并选择退出。", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" }, - "onboardingMetametricsInfuraTermsLegacy": { - "message": "* 如您在 MetaMask中 使用 Infura 作为默认的 RPC 提供商,Infura 将在您发送交易时收集您的 IP 地址和以太坊钱包地址。我们不会以允许系统将这两项数据关联起来的方式存储这些信息。如需从数据收集角度进一步了解 MetaMask 和 Infura 如何进行交互,请参阅我们的更新版 $1。如需进一步了解我们的一般隐私准则,请参阅我们的 $2。", - "description": "$1 represents `onboardingMetametricsInfuraTermsPolicyLink`, $2 represents `onboardingMetametricsInfuraTermsPolicy`" - }, "onboardingMetametricsInfuraTermsPolicy": { "message": "隐私政策" }, - "onboardingMetametricsInfuraTermsPolicyLegacy": { - "message": "隐私政策在此处" - }, - "onboardingMetametricsInfuraTermsPolicyLinkLegacy": { - "message": "此处" - }, "onboardingMetametricsModalTitle": { "message": "添加自定义网络" }, @@ -3261,17 +3239,6 @@ "onboardingMetametricsNeverCollectIPEmphasis": { "message": "通用:" }, - "onboardingMetametricsNeverCollectIPLegacy": { - "message": "$1收集您的完整 IP 地址*", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverCollectLegacy": { - "message": "$1 收集我们不需要提供服务的信息(如私钥、地址、交易散列或余额)", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsNeverEmphasisLegacy": { - "message": "永远不会发生" - }, "onboardingMetametricsNeverSellData": { "message": "$1 您可以随时决定是否通过设置共享或删除您的使用数据。", "description": "$1 represents `onboardingMetametricsNeverSellDataEmphasis`" @@ -3279,13 +3246,6 @@ "onboardingMetametricsNeverSellDataEmphasis": { "message": "可选:" }, - "onboardingMetametricsNeverSellDataLegacy": { - "message": "$1 出售数据。永远不会!", - "description": "$1 represents `onboardingMetametricsNeverEmphasis`" - }, - "onboardingMetametricsSendAnonymizeLegacy": { - "message": "发送匿名的点击和页面浏览事件" - }, "onboardingMetametricsTitle": { "message": "请帮助我们改进 MetaMask" }, diff --git a/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap b/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap index c68e7759e419..516b27b2a38b 100644 --- a/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap +++ b/ui/pages/onboarding-flow/metametrics/__snapshots__/metametrics.test.js.snap @@ -146,7 +146,7 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy

- MetaMask would like to gather usage data to better understand how our users interact with MetaMask. This data will be used to provide the service, which includes improving the service based on your use. + We’d like to gather basic usage and diagnostics data to improve MetaMask. Know that we never sell the data you provide here.

- MetaMask will... + When we gather metrics, it will always be...

    -
  • - - Always allow you to opt-out via Settings -
  • -
  • - - Send anonymized click and pageview events -
  • @@ -192,9 +178,9 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy - Never + Private: - collect information we don’t need to provide the service (such as keys, addresses, transaction hashes, or balances) + clicks and views on the app are stored, but other details (like your public address) are not.
    @@ -204,8 +190,8 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy class="box box--flex-direction-row" > @@ -213,9 +199,9 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy - Never + General: - collect your full IP address* + we temporarily use your IP address to detect a general location (like your country or region), but it's never stored.

@@ -225,8 +211,8 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy class="box box--flex-direction-row" > @@ -234,42 +220,47 @@ exports[`Onboarding Metametrics Component should match snapshot after new policy - Never + Optional: - sell data. Ever! + you decide if you want to share or delete your usage data via settings any time.
-
- This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. -
+ + + + + We’ll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). + +
- * When you use Infura as your default RPC provider in MetaMask, Infura will collect your IP address and your Ethereum wallet address when you send a transaction. We don’t store this information in a way that allows our systems to associate those two pieces of data. For more information on how MetaMask and Infura interact from a data collection perspective, see our update - - here - - . For more information on our privacy practices in general, see our + We’ll let you know if we decide to use this data for other purposes. You can review our - Privacy Policy here + Privacy Policy - . + for more information. Remember, you can go to settings and opt out at any time.
diff --git a/ui/pages/onboarding-flow/metametrics/metametrics.js b/ui/pages/onboarding-flow/metametrics/metametrics.js index f874c99ca5e0..8fe4246ca332 100644 --- a/ui/pages/onboarding-flow/metametrics/metametrics.js +++ b/ui/pages/onboarding-flow/metametrics/metametrics.js @@ -35,7 +35,6 @@ import { IconName, IconSize, } from '../../../components/component-library'; -import { PRIVACY_POLICY_DATE } from '../../../helpers/constants/privacy-policy'; import Box from '../../../components/ui/box/box'; import { FirstTimeFlowType } from '../../../../shared/constants/onboarding'; @@ -45,9 +44,6 @@ export default function OnboardingMetametrics() { const dispatch = useDispatch(); const history = useHistory(); - const newPrivacyPolicyDate = new Date(PRIVACY_POLICY_DATE); - const currentDate = new Date(Date.now()); - const nextRoute = useSelector(getFirstTimeFlowTypeRouteAfterMetaMetricsOptIn); const firstTimeFlowType = useSelector(getFirstTimeFlowType); @@ -108,298 +104,135 @@ export default function OnboardingMetametrics() { history.push(nextRoute); }; - const renderLegacyOnboarding = () => { - return ( -
+ - - {t('onboardingMetametricsTitle')} - - - {t('onboardingMetametricsDescriptionLegacy')} - - - {t('onboardingMetametricsDescription2Legacy')} - -
    -
  • + {t('onboardingMetametricsTitle')} + + + {t('onboardingMetametricsDescription')} + + + {t('onboardingMetametricsDescription2')} + +
      +
    • + - {t('onboardingMetametricsAllowOptOutLegacy')} -
    • -
    • + {t('onboardingMetametricsNeverCollect', [ + + {t('onboardingMetametricsNeverCollectEmphasis')} + , + ])} + +
    • +
    • + - {t('onboardingMetametricsSendAnonymizeLegacy')} -
    • -
    • - - - {t('onboardingMetametricsNeverCollectLegacy', [ - - {t('onboardingMetametricsNeverEmphasisLegacy')} - , - ])} - -
    • -
    • - - - {t('onboardingMetametricsNeverCollectIPLegacy', [ - - {t('onboardingMetametricsNeverEmphasisLegacy')} - , - ])} - -
    • -
    • - - - {t('onboardingMetametricsNeverSellDataLegacy', [ - - {t('onboardingMetametricsNeverEmphasisLegacy')} - , - ])} - {' '} -
    • -
    - - {t('onboardingMetametricsDataTermsLegacy')} - - - {t('onboardingMetametricsInfuraTermsLegacy', [ - - {t('onboardingMetametricsInfuraTermsPolicyLinkLegacy')} - , - - {t('onboardingMetametricsInfuraTermsPolicyLegacy')} - , - ])} - - -
    - - -
    -
- ); - }; - - const renderOnboarding = () => { - return ( -
+ {t('onboardingMetametricsNeverCollectIPEmphasis')} + , + ])} + + +
  • + + + {t('onboardingMetametricsNeverSellData', [ + + {t('onboardingMetametricsNeverSellDataEmphasis')} + , + ])} + {' '} +
  • + + + dispatch(setDataCollectionForMarketing(!dataCollectionForMarketing)) + } + label={t('onboardingMetametricsUseDataCheckbox')} + paddingBottom={3} + /> + - - {t('onboardingMetametricsTitle')} - - - {t('onboardingMetametricsDescription')} - - + {t('onboardingMetametricsInfuraTermsPolicy')} + , + ])} + + +
    + - -
    + {t('onboardingMetametricsDisagree')} +
    - ); - }; - - return currentDate >= newPrivacyPolicyDate - ? renderOnboarding() - : renderLegacyOnboarding(); +
    + ); } diff --git a/ui/pages/onboarding-flow/metametrics/metametrics.test.js b/ui/pages/onboarding-flow/metametrics/metametrics.test.js index 487987a0f5f5..5318d0ca1821 100644 --- a/ui/pages/onboarding-flow/metametrics/metametrics.test.js +++ b/ui/pages/onboarding-flow/metametrics/metametrics.test.js @@ -140,13 +140,4 @@ describe('Onboarding Metametrics Component', () => { ); expect(queryByTestId('onboarding-metametrics')).toBeInTheDocument(); }); - - it('should render the Legacy Onboarding component when the current date is before the new privacy policy date', () => { - jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); - const { queryByTestId } = renderWithProvider( - , - mockStore, - ); - expect(queryByTestId('onboarding-legacy-metametrics')).toBeInTheDocument(); - }); }); From 52a65bc5b44d8663b02877ab9564889b217cf6d7 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Tue, 25 Jun 2024 15:06:19 +0200 Subject: [PATCH 16/22] fix: hide testnet amounts if user opt out in the settings (#25167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR aims to hide testnet fiat values if user opt out in the settings. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25167?quickstart=1) ## **Related issues** - Fixes: https://github.com/MetaMask/metamask-extension/issues/25169 ## **Manual testing steps** 1. Change current network to any testnet 2. Go to setting, turn on "Show conversion on test networks" 3. Try simple send - see fiat values displayed in simulations 4. Go to setting, turn off "Show conversion on test networks" 5. Try simple send - see no fiat values displayed in simulations 6. Change current network to mainnet or any other network which is not testnet 7. Try simple send - see fiat values displayed in simulations ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/constants/network.ts | 7 + ui/hooks/useHideFiatForTestnet.test.ts | 69 ++++++++++ ui/hooks/useHideFiatForTestnet.ts | 17 +++ .../simulation-details/fiat-display.test.tsx | 121 +++++++++++++----- .../simulation-details/fiat-display.tsx | 12 ++ 5 files changed, 194 insertions(+), 32 deletions(-) create mode 100644 ui/hooks/useHideFiatForTestnet.test.ts create mode 100644 ui/hooks/useHideFiatForTestnet.ts diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 62fe072bf0c0..452f0584ffaa 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1116,3 +1116,10 @@ export const TEST_NETWORKS = [ LINEA_GOERLI_DISPLAY_NAME, LINEA_SEPOLIA_DISPLAY_NAME, ]; + +export const TEST_NETWORK_IDS = [ + CHAIN_IDS.GOERLI, + CHAIN_IDS.SEPOLIA, + CHAIN_IDS.LINEA_GOERLI, + CHAIN_IDS.LINEA_SEPOLIA, +]; diff --git a/ui/hooks/useHideFiatForTestnet.test.ts b/ui/hooks/useHideFiatForTestnet.test.ts new file mode 100644 index 000000000000..cdc4905c74c4 --- /dev/null +++ b/ui/hooks/useHideFiatForTestnet.test.ts @@ -0,0 +1,69 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { getShowFiatInTestnets, getCurrentChainId } from '../selectors'; +import { TEST_NETWORK_IDS, CHAIN_IDS } from '../../shared/constants/network'; +import { useHideFiatForTestnet } from './useHideFiatForTestnet'; + +jest.mock('react-redux', () => ({ + useSelector: jest.fn().mockImplementation((selector) => selector()), +})); + +jest.mock('../selectors', () => ({ + getShowFiatInTestnets: jest.fn(), + getCurrentChainId: jest.fn(), +})); + +describe('useHideFiatForTestnet', () => { + const mockGetShowFiatInTestnets = jest.mocked(getShowFiatInTestnets); + const mockGetCurrentChainId = jest.mocked(getCurrentChainId); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('utilizes the specified chain id', () => { + mockGetShowFiatInTestnets.mockReturnValue(false); + mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]); + + const { result } = renderHook(() => + useHideFiatForTestnet(CHAIN_IDS.MAINNET), + ); + + expect(result.current).toBe(false); + }); + + it('returns true if current network is a testnet and showFiatInTestnets is false', () => { + mockGetShowFiatInTestnets.mockReturnValue(false); + mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]); + + const { result } = renderHook(() => useHideFiatForTestnet()); + + expect(result.current).toBe(true); + }); + + it('returns false if current network is a testnet and showFiatInTestnets is true', () => { + mockGetShowFiatInTestnets.mockReturnValue(true); + mockGetCurrentChainId.mockReturnValue(TEST_NETWORK_IDS[0]); + + const { result } = renderHook(() => useHideFiatForTestnet()); + + expect(result.current).toBe(false); + }); + + it('returns false if current network is not a testnet', () => { + mockGetShowFiatInTestnets.mockReturnValue(false); + mockGetCurrentChainId.mockReturnValue('1'); + + const { result } = renderHook(() => useHideFiatForTestnet()); + + expect(result.current).toBe(false); + }); + + it('returns false if current network is not a testnet but showFiatInTestnets is true', () => { + mockGetShowFiatInTestnets.mockReturnValue(true); + mockGetCurrentChainId.mockReturnValue('1'); + + const { result } = renderHook(() => useHideFiatForTestnet()); + + expect(result.current).toBe(false); + }); +}); diff --git a/ui/hooks/useHideFiatForTestnet.ts b/ui/hooks/useHideFiatForTestnet.ts new file mode 100644 index 000000000000..b01b20513550 --- /dev/null +++ b/ui/hooks/useHideFiatForTestnet.ts @@ -0,0 +1,17 @@ +import { useSelector } from 'react-redux'; +import type { Hex } from '@metamask/utils'; +import { getShowFiatInTestnets, getCurrentChainId } from '../selectors'; +import { TEST_NETWORK_IDS } from '../../shared/constants/network'; + +/** + * Returns true if the fiat value should be hidden for testnet networks. + * + * @param providedChainId + * @returns boolean + */ +export const useHideFiatForTestnet = (providedChainId?: Hex): boolean => { + const showFiatInTestnets = useSelector(getShowFiatInTestnets); + const currentChainId = useSelector(getCurrentChainId); + const chainId = providedChainId ?? currentChainId; + return TEST_NETWORK_IDS.includes(chainId) && !showFiatInTestnets; +}; diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx index 5716e22e5747..0eee6aee8990 100644 --- a/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx +++ b/ui/pages/confirmations/components/simulation-details/fiat-display.test.tsx @@ -1,47 +1,104 @@ import React from 'react'; import { screen } from '@testing-library/react'; import configureStore from 'redux-mock-store'; +import { merge } from 'lodash'; import { useFiatFormatter } from '../../../../hooks/useFiatFormatter'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import mockState from '../../../../../test/data/mock-state.json'; +import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { IndividualFiatDisplay, TotalFiatDisplay } from './fiat-display'; import { FIAT_UNAVAILABLE } from './types'; -const store = configureStore()(mockState); +const mockStateWithTestnet = merge({}, mockState, { + metamask: { + providerConfig: { + chainId: CHAIN_IDS.SEPOLIA, + }, + }, +}); -jest.mock('../../../../hooks/useFiatFormatter'); -(useFiatFormatter as jest.Mock).mockReturnValue((value: number) => `$${value}`); - -describe('IndividualFiatDisplay', () => { - // @ts-expect-error This is missing from the Mocha type definitions - it.each([ - [FIAT_UNAVAILABLE, 'Not Available'], - [100, '$100'], - [-100, '$100'], - ])( - 'when fiatAmount is %s it renders %s', - (fiatAmount: number | null, expected: string) => { - renderWithProvider( - , - store, - ); - expect(screen.getByText(expected)).toBeInTheDocument(); +const mockStateWithShowingFiatOnTestnets = merge({}, mockStateWithTestnet, { + metamask: { + preferences: { + showFiatInTestnets: true, }, - ); + }, }); +const mockStoreWithShowingFiatOnTestnets = configureStore()( + mockStateWithShowingFiatOnTestnets, +); -describe('TotalFiatDisplay', () => { - // @ts-expect-error This is missing from the Mocha type definitions - it.each([ - [[FIAT_UNAVAILABLE, FIAT_UNAVAILABLE], 'Not Available'], - [[], 'Not Available'], - [[100, 200, FIAT_UNAVAILABLE, 300], 'Total = $600'], - [[-100, -200, FIAT_UNAVAILABLE, -300], 'Total = $600'], - ])( - 'when fiatAmounts is %s it renders %s', - (fiatAmounts: (number | null)[], expected: string) => { - renderWithProvider(, store); - expect(screen.getByText(expected)).toBeInTheDocument(); +const mockStateWithHidingFiatOnTestnets = merge({}, mockStateWithTestnet, { + metamask: { + preferences: { + showFiatInTestnets: false, }, - ); + }, +}); +const mockStoreWithHidingFiatOnTestnets = configureStore()( + mockStateWithHidingFiatOnTestnets, +); + +jest.mock('../../../../hooks/useFiatFormatter'); + +describe('FiatDisplay', () => { + const mockUseFiatFormatter = jest.mocked(useFiatFormatter); + + beforeEach(() => { + jest.resetAllMocks(); + mockUseFiatFormatter.mockReturnValue((value: number) => `$${value}`); + }); + + describe('IndividualFiatDisplay', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + [FIAT_UNAVAILABLE, 'Not Available'], + [100, '$100'], + [-100, '$100'], + ])( + 'when fiatAmount is %s it renders %s', + (fiatAmount: number | null, expected: string) => { + renderWithProvider( + , + mockStoreWithShowingFiatOnTestnets, + ); + expect(screen.getByText(expected)).toBeInTheDocument(); + }, + ); + + it('does not render anything if user opted out to show fiat values on testnet', () => { + const { queryByText } = renderWithProvider( + , + mockStoreWithHidingFiatOnTestnets, + ); + expect(queryByText('100')).toBe(null); + }); + }); + + describe('TotalFiatDisplay', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + [[FIAT_UNAVAILABLE, FIAT_UNAVAILABLE], 'Not Available'], + [[], 'Not Available'], + [[100, 200, FIAT_UNAVAILABLE, 300], 'Total = $600'], + [[-100, -200, FIAT_UNAVAILABLE, -300], 'Total = $600'], + ])( + 'when fiatAmounts is %s it renders %s', + (fiatAmounts: (number | null)[], expected: string) => { + renderWithProvider( + , + mockStoreWithShowingFiatOnTestnets, + ); + expect(screen.getByText(expected)).toBeInTheDocument(); + }, + ); + + it('does not render anything if user opted out to show fiat values on testnet', () => { + const { queryByText } = renderWithProvider( + , + mockStoreWithHidingFiatOnTestnets, + ); + expect(queryByText('600')).toBe(null); + }); + }); }); diff --git a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx index 6286141441af..5143cd5ed06b 100644 --- a/ui/pages/confirmations/components/simulation-details/fiat-display.tsx +++ b/ui/pages/confirmations/components/simulation-details/fiat-display.tsx @@ -8,6 +8,7 @@ import { useI18nContext } from '../../../../hooks/useI18nContext'; import { Text } from '../../../../components/component-library'; import { SizeNumber } from '../../../../components/component-library/box/box.types'; import { useFiatFormatter } from '../../../../hooks/useFiatFormatter'; +import { useHideFiatForTestnet } from '../../../../hooks/useHideFiatForTestnet'; import { FIAT_UNAVAILABLE, FiatAmount } from './types'; const textStyle = { @@ -37,7 +38,13 @@ export function calculateTotalFiat(fiatAmounts: FiatAmount[]): number { export const IndividualFiatDisplay: React.FC<{ fiatAmount: FiatAmount }> = ({ fiatAmount, }) => { + const hideFiatForTestnet = useHideFiatForTestnet(); const fiatFormatter = useFiatFormatter(); + + if (hideFiatForTestnet) { + return null; + } + if (fiatAmount === FIAT_UNAVAILABLE) { return ; } @@ -55,10 +62,15 @@ export const IndividualFiatDisplay: React.FC<{ fiatAmount: FiatAmount }> = ({ export const TotalFiatDisplay: React.FC<{ fiatAmounts: FiatAmount[]; }> = ({ fiatAmounts }) => { + const hideFiatForTestnet = useHideFiatForTestnet(); const t = useI18nContext(); const fiatFormatter = useFiatFormatter(); const totalFiat = calculateTotalFiat(fiatAmounts); + if (hideFiatForTestnet) { + return null; + } + return totalFiat === 0 ? ( ) : ( From 765370f91f5ba9df6e00545ba4a0c1e8e3cf5b20 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 25 Jun 2024 18:58:30 +0530 Subject: [PATCH 17/22] fix: formatting of deadline in permit signature page (#25321) --- .../confirmations/signatures/permit.spec.ts | 2 +- .../app/confirm/info/row/date.stories.tsx | 26 + .../app/confirm/info/row/date.test.tsx | 11 + ui/components/app/confirm/info/row/date.tsx | 27 + ui/components/app/confirm/info/row/index.ts | 1 + ui/helpers/utils/util.js | 10 + ui/helpers/utils/util.test.js | 12 + .../personal-sign/siwe-sign/siwe-sign.tsx | 7 +- .../__snapshots__/typed-sign.test.tsx.snap | 461 ++++++++++++++++++ .../info/typed-sign/typed-sign.test.tsx | 6 +- .../confirm/info/typed-sign/typed-sign.tsx | 1 + .../components/confirm/row/dataTree.tsx | 55 ++- .../row/typed-sign-data/typedSignData.tsx | 10 +- 13 files changed, 603 insertions(+), 26 deletions(-) create mode 100644 ui/components/app/confirm/info/row/date.stories.tsx create mode 100644 ui/components/app/confirm/info/row/date.test.tsx create mode 100644 ui/components/app/confirm/info/row/date.tsx diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 5afe288f869b..8320fd748f63 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -85,7 +85,7 @@ async function assertInfoValues(driver: Driver) { }); const value = driver.findElement({ text: '3000' }); const nonce = driver.findElement({ text: '0' }); - const deadline = driver.findElement({ text: '50000000000' }); + const deadline = driver.findElement({ text: '02 August 1971, 16:53' }); assert.ok(await origin, 'origin'); assert.ok(await contractPetName, 'contractPetName'); diff --git a/ui/components/app/confirm/info/row/date.stories.tsx b/ui/components/app/confirm/info/row/date.stories.tsx new file mode 100644 index 000000000000..971ba7624f3e --- /dev/null +++ b/ui/components/app/confirm/info/row/date.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { ConfirmInfoRow } from './row'; +import { ConfirmInfoRowDate } from './date'; + +const ConfirmInfoRowDateStory = { + title: 'Components/App/Confirm/InfoRowDate', + component: ConfirmInfoRowDate, + + decorators: [ + (story) => {story()}, + ], + + argTypes: { + url: { + control: 'date', + }, + }, +}; + +export const DefaultStory = ({ date }) => ; +DefaultStory.args = { + date: 1633019124000, +}; + +export default ConfirmInfoRowDateStory; diff --git a/ui/components/app/confirm/info/row/date.test.tsx b/ui/components/app/confirm/info/row/date.test.tsx new file mode 100644 index 000000000000..eda8510e4e7c --- /dev/null +++ b/ui/components/app/confirm/info/row/date.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { ConfirmInfoRowDate } from './date'; + +describe('ConfirmInfoRowDate', () => { + it('should match snapshot', () => { + const { getByText } = render(); + expect(getByText('30 September 2021, 16:25')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/confirm/info/row/date.tsx b/ui/components/app/confirm/info/row/date.tsx new file mode 100644 index 000000000000..47fa8e4ea56a --- /dev/null +++ b/ui/components/app/confirm/info/row/date.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { + AlignItems, + Display, + FlexWrap, + TextColor, +} from '../../../../../helpers/constants/design-system'; +import { formatUTCDate } from '../../../../../helpers/utils/util'; +import { Box, Text } from '../../../../component-library'; + +export type ConfirmInfoRowDateProps = { + date: number; +}; + +export const ConfirmInfoRowDate = ({ date }: ConfirmInfoRowDateProps) => ( + + + {formatUTCDate(date)} + + +); diff --git a/ui/components/app/confirm/info/row/index.ts b/ui/components/app/confirm/info/row/index.ts index ef2102b280a1..0fc08d5887bb 100644 --- a/ui/components/app/confirm/info/row/index.ts +++ b/ui/components/app/confirm/info/row/index.ts @@ -1,4 +1,5 @@ export * from './address'; +export * from './date'; export * from './divider'; export * from './row'; export * from './text'; diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 909c7c2c2d8a..114e7cb4c369 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -43,6 +43,16 @@ export function formatDate(date, format = "M/d/y 'at' T") { return DateTime.fromMillis(date).toFormat(format); } +export const formatUTCDate = (dateInMillis) => { + if (!dateInMillis) { + return dateInMillis; + } + + return DateTime.fromMillis(dateInMillis) + .setZone('utc') + .toFormat('dd LLLL yyyy, HH:mm'); +}; + export function formatDateWithYearContext( date, formatThisYear = 'MMM d', diff --git a/ui/helpers/utils/util.test.js b/ui/helpers/utils/util.test.js index fdcfe4e48972..76811f432f81 100644 --- a/ui/helpers/utils/util.test.js +++ b/ui/helpers/utils/util.test.js @@ -1043,6 +1043,18 @@ describe('util', () => { }); }); + describe('formatUTCDate', () => { + it('formats passed date string', () => { + expect(util.formatUTCDate(1633019124000)).toStrictEqual( + '30 September 2021, 16:25', + ); + }); + + it('returns empty string if empty string is passed', () => { + expect(util.formatUTCDate('')).toStrictEqual(''); + }); + }); + describe('shortenAddress', () => { it('should return the same address if it is shorter than TRUNCATED_NAME_CHAR_LIMIT', () => { expect(util.shortenAddress('0x123')).toStrictEqual('0x123'); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx index 8c1260c3ede5..94389b253450 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/siwe-sign/siwe-sign.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { DateTime } from 'luxon'; import { useSelector } from 'react-redux'; import { toHex } from '@metamask/controller-utils'; @@ -9,9 +10,9 @@ import { SignatureRequestType } from '../../../../../types/confirm'; import { ConfirmInfoRow, ConfirmInfoRowAddress, + ConfirmInfoRowDate, ConfirmInfoRowText, } from '../../../../../../../components/app/confirm/info/row'; -import { formatDate } from '../../../../../utils/date'; const SIWESignInfo: React.FC = () => { const t = useI18nContext(); @@ -64,7 +65,9 @@ const SIWESignInfo: React.FC = () => { - + {requestId && ( diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/__snapshots__/typed-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/__snapshots__/typed-sign.test.tsx.snap index cf3c57f86829..328fe4209390 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/__snapshots__/typed-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/__snapshots__/typed-sign.test.tsx.snap @@ -1,5 +1,466 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TypedSignInfo correctly renders permit sign type 1`] = ` +
    +
    +
    +
    +

    + Spender +

    +
    +
    +
    + +

    + 0x5B38D...eddC4 +

    +
    +
    +
    +
    +
    +
    +

    + Request from +

    +
    +
    + +
    +
    +
    +
    +

    + metamask.github.io +

    +
    +
    +
    +
    +

    + Interacting with +

    +
    +
    +
    + +

    + 0xCcCCc...ccccC +

    +
    +
    +
    +
    +
    +
    +
    +

    + Message +

    +
    +
    +
    +
    +

    + Primary type: +

    +
    +
    +

    + Permit +

    +
    +
    +
    +
    +
    +
    +

    + Owner: +

    +
    +
    +
    + +

    + 0x935E7...05477 +

    +
    +
    +
    +
    +
    +

    + Spender: +

    +
    +
    +
    + +

    + 0x5B38D...eddC4 +

    +
    +
    +
    +
    +
    +

    + Value: +

    +
    +
    +

    + 3000 +

    +
    +
    +
    +
    +

    + Nonce: +

    +
    +
    +

    + 0 +

    +
    +
    +
    +
    +

    + Deadline: +

    +
    +
    +

    + 02 August 1971, 16:53 +

    +
    +
    +
    +
    +
    +
    +
    +
    +`; + exports[`TypedSignInfo does not render if required data is not present in the transaction 1`] = `
    `; exports[`TypedSignInfo renders origin for typed sign data request 1`] = ` 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 4d64d3a38f28..332b3f79a6ff 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 @@ -80,7 +80,7 @@ describe('TypedSignInfo', () => { expect(getByText('Estimated changes')).toBeDefined(); }); - it('displays "Spender" for permit signature type', () => { + it('correctly renders permit sign type', () => { const state = { ...mockState, confirm: { @@ -88,7 +88,7 @@ describe('TypedSignInfo', () => { }, }; const mockStore = configureMockStore([])(state); - const { getByText } = renderWithProvider(, mockStore); - expect(getByText('Spender')).toBeDefined(); + const { container } = renderWithProvider(, mockStore); + expect(container).toMatchSnapshot(); }); }); 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 f6efca236f3a..019dd138f401 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 @@ -63,6 +63,7 @@ const TypedSignInfo: React.FC = () => { diff --git a/ui/pages/confirmations/components/confirm/row/dataTree.tsx b/ui/pages/confirmations/components/confirm/row/dataTree.tsx index c72fc6113292..307b608488d9 100644 --- a/ui/pages/confirmations/components/confirm/row/dataTree.tsx +++ b/ui/pages/confirmations/components/confirm/row/dataTree.tsx @@ -8,6 +8,7 @@ import { BlockSize } from '../../../../../helpers/constants/design-system'; import { ConfirmInfoRow, ConfirmInfoRowAddress, + ConfirmInfoRowDate, ConfirmInfoRowText, } from '../../../../../components/app/confirm/info/row'; @@ -20,32 +21,50 @@ export type TreeData = { export const DataTree = ({ data, + isPermit = false, }: { data: Record | TreeData[]; + isPermit?: boolean; }) => ( - {Object.entries(data).map(([label, { value, type }], i) => { - return ( - - { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - - } - - ); - })} + {Object.entries(data).map(([label, { value, type }], i) => ( + + { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + + } + + ))} ); -const DataField = ({ value, type }: { value: ValueType; type: string }) => { +const DataField = ({ + label, + isPermit, + type, + value, +}: { + label: string; + isPermit: boolean; + type: string; + value: ValueType; +}) => { if (typeof value === 'object' && value !== null) { - return ; + return ; + } + if (isPermit && label === 'deadline') { + return ; } if ( type === 'address' && diff --git a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx index 3b3d1db72ca4..9b3934cef651 100644 --- a/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx +++ b/ui/pages/confirmations/components/confirm/row/typed-sign-data/typedSignData.tsx @@ -11,7 +11,13 @@ import { import { DataTree } from '../dataTree'; import { parseSanitizeTypedDataMessage } from '../../../../utils'; -export const ConfirmInfoRowTypedSignData = ({ data }: { data: string }) => { +export const ConfirmInfoRowTypedSignData = ({ + data, + isPermit, +}: { + data: string; + isPermit?: boolean; +}) => { const t = useI18nContext(); if (!data) { @@ -29,7 +35,7 @@ export const ConfirmInfoRowTypedSignData = ({ data }: { data: string }) => { - + ); From bda7cdd57205f8e35bb98d0a44f976e44a715b53 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 25 Jun 2024 11:02:53 -0230 Subject: [PATCH 18/22] chore: Remove dead code/config related to Mocha unit tests (#25492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Our test scripts and configuration have been updated to remove things that were used for our Mocha unit tests, which have been converted to Jest. This unblocks the creation of a test coverage quality gate (previously this was blocked by a bug in our "merge coverage" step, which merged Mocha and Jest coverage). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25492?quickstart=1) ## **Related issues** None ## **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/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- .circleci/config.yml | 9 +- .depcheckrc.yml | 2 - .eslintrc.js | 34 +-- .mocharc.js | 17 -- coverage-targets.js | 20 -- .../files-to-convert.json | 1 - lavamoat/build-system/policy.json | 226 +++++++-------- nyc.config.js | 17 -- package.json | 7 +- test/lib/wait-until-called.js | 66 ----- test/merge-coverage.js | 268 ------------------ test/run-unit-tests.js | 38 +-- yarn.lock | 6 +- 13 files changed, 123 insertions(+), 588 deletions(-) delete mode 100644 coverage-targets.js delete mode 100644 nyc.config.js delete mode 100644 test/lib/wait-until-called.js delete mode 100644 test/merge-coverage.js diff --git a/.circleci/config.yml b/.circleci/config.yml index caa86668657b..5ab0af8fb0cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -236,7 +236,7 @@ workflows: - test-unit-jest-development: requires: - prep-deps - - upload-and-validate-coverage: + - upload-coverage: requires: - test-unit-jest-main - test-unit-jest-development @@ -286,7 +286,7 @@ workflows: - test-unit-jest-main - test-unit-jest-development - test-unit-global - - upload-and-validate-coverage + - upload-coverage - validate-source-maps - validate-source-maps-beta - validate-source-maps-flask @@ -1677,7 +1677,7 @@ jobs: - store_test_results: path: test/test-results/junit.xml - upload-and-validate-coverage: + upload-coverage: executor: node-browsers-small steps: - run: *shallow-git-clone @@ -1685,9 +1685,6 @@ jobs: - attach_workspace: at: . - codecov/upload - - run: - name: test:coverage:validate - command: yarn test:coverage:validate - persist_to_workspace: root: . paths: diff --git a/.depcheckrc.yml b/.depcheckrc.yml index f2f9b37fd563..e4169c5436f0 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -39,8 +39,6 @@ ignores: - 'wait-on' - 'tsx' # used in .devcontainer - 'prettier-eslint' # used by the Prettier ESLint VSCode extension - # development tool - - 'nyc' # storybook - '@storybook/cli' - '@storybook/core' diff --git a/.eslintrc.js b/.eslintrc.js index 2fa4b8cf9846..9f7fed5928ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,9 +46,7 @@ module.exports = { 'development/**/*.js', 'test/e2e/**/*.js', 'test/helpers/*.js', - 'test/lib/wait-until-called.js', 'test/run-unit-tests.js', - 'test/merge-coverage.js', ], extends: [ path.resolve(__dirname, '.eslintrc.base.js'), @@ -94,8 +92,6 @@ module.exports = { 'test/stub/**/*.js', 'test/unit-global/**/*.js', ], - // TODO: Convert these files to modern JS - excludedFiles: ['test/lib/wait-until-called.js'], extends: [ path.resolve(__dirname, '.eslintrc.base.js'), path.resolve(__dirname, '.eslintrc.node.js'), @@ -261,30 +257,7 @@ module.exports = { * Mocha library. */ { - files: [ - '**/*.test.js', - 'test/lib/wait-until-called.js', - 'test/e2e/**/*.spec.js', - ], - excludedFiles: [ - 'app/scripts/controllers/app-state.test.js', - 'app/scripts/controllers/mmi-controller.test.js', - 'app/scripts/metamask-controller.actions.test.js', - 'app/scripts/detect-multiple-instances.test.js', - 'app/scripts/controllers/swaps.test.js', - 'app/scripts/controllers/metametrics.test.js', - 'app/scripts/controllers/permissions/**/*.test.js', - 'app/scripts/controllers/preferences.test.js', - 'app/scripts/lib/**/*.test.js', - 'app/scripts/metamask-controller.test.js', - 'app/scripts/migrations/*.test.js', - 'app/scripts/platforms/*.test.js', - 'development/**/*.test.js', - 'shared/**/*.test.js', - 'ui/**/*.test.js', - 'ui/__mocks__/*.js', - 'test/e2e/helpers.test.js', - ], + files: ['test/e2e/**/*.spec.js', 'test/unit-global/*.test.js'], extends: ['@metamask/eslint-config-mocha'], rules: { // In Mocha tests, it is common to use `this` to store values or do @@ -297,7 +270,9 @@ module.exports = { * Jest tests * * These are files that make use of globals and syntax introduced by the - * Jest library. The files in this section should match the Mocha excludedFiles section. + * Jest library. + * TODO: This list of files is incomplete, and should be replaced with globs that match the + * Jest config. */ { files: [ @@ -391,7 +366,6 @@ module.exports = { 'test/e2e/benchmark.js', 'test/helpers/setup-helper.js', 'test/run-unit-tests.js', - 'test/merge-coverage.js', ], rules: { 'node/no-process-exit': 'off', diff --git a/.mocharc.js b/.mocharc.js index 890fca78e6b9..ff6200513b96 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,23 +1,6 @@ module.exports = { // TODO: Remove the `exit` setting, it can hide broken tests. exit: true, - ignore: [ - './app/scripts/lib/**/*.test.js', - './app/scripts/migrations/*.test.js', - './app/scripts/platforms/*.test.js', - './app/scripts/controllers/app-state.test.js', - './app/scripts/controllers/permissions/**/*.test.js', - './app/scripts/controllers/mmi-controller.test.ts', - './app/scripts/metamask-controller.actions.test.js', - './app/scripts/detect-multiple-instances.test.js', - './app/scripts/controllers/swaps.test.js', - './app/scripts/controllers/metametrics.test.js', - './app/scripts/controllers/preferences.test.js', - './app/scripts/constants/error-utils.test.js', - './app/scripts/metamask-controller.test.js', - './development/fitness-functions/**/*.test.ts', - './test/e2e/helpers.test.js', - ], recursive: true, require: ['test/env.js', 'test/setup.js'], }; diff --git a/coverage-targets.js b/coverage-targets.js deleted file mode 100644 index 631a8b6e737d..000000000000 --- a/coverage-targets.js +++ /dev/null @@ -1,20 +0,0 @@ -// Codecov uses a yaml file for its configuration and it targets line coverage. -// To keep our policy in place we have thile file separate from our -// codecov.yml file that specifies coverage targets for each project in the -// codecov.yml file. These targets are read by the test/merge-coverage.js -// script, and the paths from the codecov.yml file are used to figure out which -// subset of files to check against these targets. -module.exports = { - global: { - lines: 70.85, - branches: 59.07, - statements: 70.3, - functions: 63.52, - }, - transforms: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, - }, -}; diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index df0feeb26e47..ed5615e8c9d0 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -316,7 +316,6 @@ "test/lib/mock-encryptor.js", "test/lib/render-helpers.js", "test/lib/tick.js", - "test/lib/wait-until-called.js", "test/mocks/permissions.js", "test/stub/provider.js", "test/stub/tx-meta-stub.js", diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 635158958f06..02078b5d39fc 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1161,7 +1161,7 @@ "@lavamoat/lavapack>combine-source-map>inline-source-map": true, "@lavamoat/lavapack>combine-source-map>lodash.memoize": true, "@lavamoat/lavapack>combine-source-map>source-map": true, - "nyc>convert-source-map": true + "gulp-sourcemaps>convert-source-map": true } }, "@lavamoat/lavapack>combine-source-map>inline-source-map": { @@ -1670,6 +1670,30 @@ "browserify>duplexer2>readable-stream>safe-buffer": true } }, + "browserify>glob": { + "builtin": { + "assert": true, + "events.EventEmitter": true, + "fs": true, + "path.join": true, + "path.resolve": true, + "util": true + }, + "globals": { + "console.error": true, + "process.cwd": true, + "process.nextTick": true, + "process.platform": true + }, + "packages": { + "@metamask/object-multiplex>once": true, + "eslint>minimatch": true, + "gulp-watch>path-is-absolute": true, + "mocha>glob>fs.realpath": true, + "mocha>glob>inflight": true, + "pumpify>inherits": true + } + }, "browserify>has": { "packages": { "browserify>has>function-bind": true @@ -2313,7 +2337,7 @@ "setTimeout": true }, "packages": { - "nyc>glob": true + "browserify>glob": true } }, "depcheck>@babel/traverse": { @@ -2552,11 +2576,11 @@ "process.cwd": true }, "packages": { + "browserify>glob": true, "del>is-glob": true, "depcheck>resolve": true, "eslint-plugin-import>tsconfig-paths": true, - "nock>debug": true, - "nyc>glob": true + "nock>debug": true } }, "eslint-plugin-import": { @@ -3910,13 +3934,13 @@ "gulp-sourcemaps>@gulp-sourcemaps/identity-map": true, "gulp-sourcemaps>@gulp-sourcemaps/map-sources": true, "gulp-sourcemaps>acorn": true, + "gulp-sourcemaps>convert-source-map": true, "gulp-sourcemaps>css": true, "gulp-sourcemaps>debug-fabulous": true, "gulp-sourcemaps>detect-newline": true, "gulp-sourcemaps>source-map": true, "gulp-sourcemaps>strip-bom-string": true, - "gulp-sourcemaps>through2": true, - "nyc>convert-source-map": true + "gulp-sourcemaps>through2": true } }, "gulp-sourcemaps>@gulp-sourcemaps/identity-map": { @@ -4033,6 +4057,15 @@ "define": true } }, + "gulp-sourcemaps>convert-source-map": { + "builtin": { + "fs.readFileSync": true, + "path.join": true + }, + "globals": { + "Buffer.from": true + } + }, "gulp-sourcemaps>css": { "builtin": { "fs.readFileSync": true, @@ -5719,7 +5752,7 @@ "gulp>gulp-cli>matchdep>micromatch>fragment-cache": true, "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": true, "gulp>gulp-cli>matchdep>micromatch>regex-not": true, - "nyc>spawn-wrap>is-windows": true + "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true } }, "gulp>gulp-cli>matchdep>micromatch>nanomatch>define-property": { @@ -5737,7 +5770,7 @@ "gulp>gulp-cli>replace-homedir>is-absolute": { "packages": { "gulp>gulp-cli>replace-homedir>is-absolute>is-relative": true, - "nyc>spawn-wrap>is-windows": true + "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": true } }, "gulp>gulp-cli>replace-homedir>is-absolute>is-relative": { @@ -5750,6 +5783,13 @@ "gulp>gulp-cli>replace-homedir>is-absolute>is-relative>is-unc-path>unc-path-regex": true } }, + "gulp>gulp-cli>replace-homedir>is-absolute>is-windows": { + "globals": { + "define": true, + "isWindows": "write", + "process": true + } + }, "gulp>undertaker": { "builtin": { "assert": true, @@ -5970,6 +6010,7 @@ "process.nextTick": true }, "packages": { + "browserify>glob": true, "eslint>glob-parent": true, "gulp>glob-watcher>is-negated-glob": true, "gulp>vinyl-fs>glob-stream>ordered-read-streams": true, @@ -5977,7 +6018,6 @@ "gulp>vinyl-fs>glob-stream>readable-stream": true, "gulp>vinyl-fs>glob-stream>to-absolute-glob": true, "gulp>vinyl-fs>glob-stream>unique-stream": true, - "nyc>glob": true, "react-markdown>unified>extend": true, "vinyl>remove-trailing-separator": true } @@ -6464,11 +6504,11 @@ }, "packages": { "del>graceful-fs": true, + "gulp-sourcemaps>convert-source-map": true, "gulp>vinyl-fs>remove-bom-buffer": true, "gulp>vinyl-fs>vinyl-sourcemap>append-buffer": true, "gulp>vinyl-fs>vinyl-sourcemap>normalize-path": true, "gulp>vinyl-fs>vinyl-sourcemap>now-and-later": true, - "nyc>convert-source-map": true, "vinyl": true } }, @@ -6769,7 +6809,7 @@ }, "packages": { "mocha>find-up>locate-path": true, - "nyc>find-up>path-exists": true + "mocha>find-up>path-exists": true } }, "mocha>find-up>locate-path": { @@ -6793,6 +6833,47 @@ "@storybook/test-runner>jest-circus>p-limit": true } }, + "mocha>find-up>path-exists": { + "builtin": { + "fs.access": true, + "fs.accessSync": true, + "util.promisify": true + } + }, + "mocha>glob>fs.realpath": { + "builtin": { + "fs.lstat": true, + "fs.lstatSync": true, + "fs.readlink": true, + "fs.readlinkSync": true, + "fs.realpath": true, + "fs.realpathSync": true, + "fs.stat": true, + "fs.statSync": true, + "path.normalize": true, + "path.resolve": true + }, + "globals": { + "console.error": true, + "console.trace": true, + "process.env.NODE_DEBUG": true, + "process.nextTick": true, + "process.noDeprecation": true, + "process.platform": true, + "process.throwDeprecation": true, + "process.traceDeprecation": true, + "process.version": true + } + }, + "mocha>glob>inflight": { + "globals": { + "process.nextTick": true + }, + "packages": { + "@metamask/object-multiplex>once": true, + "@metamask/object-multiplex>once>wrappy": true + } + }, "mocha>log-symbols": { "packages": { "chalk": true, @@ -6855,105 +6936,6 @@ "node-sass": { "native": true }, - "nyc>convert-source-map": { - "builtin": { - "fs.readFileSync": true, - "path.join": true - }, - "globals": { - "Buffer.from": true - } - }, - "nyc>find-up>path-exists": { - "builtin": { - "fs.access": true, - "fs.accessSync": true, - "util.promisify": true - } - }, - "nyc>glob": { - "builtin": { - "assert": true, - "events.EventEmitter": true, - "fs": true, - "path.join": true, - "path.resolve": true, - "util": true - }, - "globals": { - "console.error": true, - "process.cwd": true, - "process.nextTick": true, - "process.platform": true - }, - "packages": { - "@metamask/object-multiplex>once": true, - "eslint>minimatch": true, - "gulp-watch>path-is-absolute": true, - "nyc>glob>fs.realpath": true, - "nyc>glob>inflight": true, - "pumpify>inherits": true - } - }, - "nyc>glob>fs.realpath": { - "builtin": { - "fs.lstat": true, - "fs.lstatSync": true, - "fs.readlink": true, - "fs.readlinkSync": true, - "fs.realpath": true, - "fs.realpathSync": true, - "fs.stat": true, - "fs.statSync": true, - "path.normalize": true, - "path.resolve": true - }, - "globals": { - "console.error": true, - "console.trace": true, - "process.env.NODE_DEBUG": true, - "process.nextTick": true, - "process.noDeprecation": true, - "process.platform": true, - "process.throwDeprecation": true, - "process.traceDeprecation": true, - "process.version": true - } - }, - "nyc>glob>inflight": { - "globals": { - "process.nextTick": true - }, - "packages": { - "@metamask/object-multiplex>once": true, - "@metamask/object-multiplex>once>wrappy": true - } - }, - "nyc>resolve-from": { - "builtin": { - "fs.realpathSync": true, - "module._nodeModulePaths": true, - "module._resolveFilename": true, - "path.join": true, - "path.resolve": true - } - }, - "nyc>signal-exit": { - "builtin": { - "assert.equal": true, - "events": true - }, - "globals": { - "process": true - } - }, - "nyc>spawn-wrap>is-windows": { - "globals": { - "define": true, - "isWindows": "write", - "process": true - } - }, "nyc>yargs>set-blocking": { "globals": { "process.stderr": true, @@ -8291,7 +8273,6 @@ "lodash": true, "mocha>log-symbols": true, "nock>debug": true, - "nyc>resolve-from": true, "stylelint>@stylelint/postcss-css-in-js": true, "stylelint>@stylelint/postcss-markdown": true, "stylelint>autoprefixer": true, @@ -8319,6 +8300,7 @@ "stylelint>postcss-selector-parser": true, "stylelint>postcss-syntax": true, "stylelint>postcss-value-parser": true, + "stylelint>resolve-from": true, "stylelint>specificity": true, "stylelint>style-search": true, "stylelint>sugarss": true, @@ -8519,7 +8501,7 @@ "setTimeout": true }, "packages": { - "nyc>glob": true + "browserify>glob": true } }, "stylelint>file-entry-cache>flat-cache>write": { @@ -8821,6 +8803,15 @@ "process.platform": true } }, + "stylelint>resolve-from": { + "builtin": { + "fs.realpathSync": true, + "module._nodeModulePaths": true, + "module._resolveFilename": true, + "path.join": true, + "path.resolve": true + } + }, "stylelint>specificity": { "globals": { "define": true @@ -8932,11 +8923,20 @@ }, "packages": { "eslint>imurmurhash": true, - "nyc>signal-exit": true, "stylelint>write-file-atomic>is-typedarray": true, + "stylelint>write-file-atomic>signal-exit": true, "stylelint>write-file-atomic>typedarray-to-buffer": true } }, + "stylelint>write-file-atomic>signal-exit": { + "builtin": { + "assert.equal": true, + "events": true + }, + "globals": { + "process": true + } + }, "stylelint>write-file-atomic>typedarray-to-buffer": { "globals": { "Buffer.from": true diff --git a/nyc.config.js b/nyc.config.js deleted file mode 100644 index 408ea27b427b..000000000000 --- a/nyc.config.js +++ /dev/null @@ -1,17 +0,0 @@ -// nyc is our coverage reporter for mocha, and currently is collecting -// coverage for .yarn folder. all of these are default excludes except the -// .yarn/** entry. This entire file should be removable once we complete the -// migration from mocha to jest in the app folder. -module.exports = { - exclude: [ - 'coverage/**', - 'packages/*/test/**', - 'test/**', - 'test{,-*}.js', - '**/*{.,-}test.js', - '**/__tests__/**', - '**/node_modules/**', - '**/babel.config.js', - '.yarn/**', - ], -}; diff --git a/package.json b/package.json index 0bde909457fd..ad044a2f4ae5 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,7 @@ "test:e2e:single": "node test/e2e/run-e2e-test.js", "test:coverage:jest": "node ./test/run-unit-tests.js --jestGlobal --coverage", "test:coverage:jest:dev": "node ./test/run-unit-tests.js --jestDev --coverage", - "test:coverage:validate": "node ./test/merge-coverage.js", - "test:coverage": "node ./test/run-unit-tests.js --jestGlobal --jestDev --coverage && yarn test:coverage:validate", + "test:coverage": "node ./test/run-unit-tests.js --jestGlobal --jestDev --coverage", "test:coverage:html": "yarn test:coverage --html", "ganache:start": "./development/run-ganache.sh", "sentry:publish": "node ./development/sentry-publish.js", @@ -576,9 +575,6 @@ "history": "^5.0.0", "husky": "^8.0.3", "ini": "^3.0.0", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-reports": "^3.1.5", "jest": "^29.7.0", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.7.0", @@ -594,7 +590,6 @@ "mockttp": "^3.10.1", "nock": "patch:nock@npm%3A13.5.4#~/.yarn/patches/nock-npm-13.5.4-2c4f77b249.patch", "node-fetch": "^2.6.1", - "nyc": "^15.1.0", "postcss": "^8.4.32", "postcss-rtlcss": "^4.0.9", "prettier": "^2.7.1", diff --git a/test/lib/wait-until-called.js b/test/lib/wait-until-called.js deleted file mode 100644 index c59e51aa4c69..000000000000 --- a/test/lib/wait-until-called.js +++ /dev/null @@ -1,66 +0,0 @@ -const DEFAULT_TIMEOUT = 10000; - -/** - * A function that wraps a sinon stub and returns an asynchronous function - * that resolves if the stubbed function was called enough times, or throws - * if the timeout is exceeded. - * - * The stub that has been passed in will be setup to call the wrapped function - * directly. - * - * WARNING: Any existing `callsFake` behavior will be overwritten. - * - * @param {import('sinon').stub} stub - A sinon stub of a function - * @param {unknown} [wrappedThis] - The object the stubbed function was called - * on, if any (i.e. the `this` value) - * @param {object} [options] - Optional configuration - * @param {number} [options.callCount] - The number of calls to wait for. - * @param {number|null} [options.timeout] - The timeout, in milliseconds. Pass - * in `null` to disable the timeout. - * @returns {Function} An asynchronous function that resolves when the stub is - * called enough times, or throws if the timeout is reached. - */ -function waitUntilCalled( - stub, - wrappedThis = null, - { callCount = 1, timeout = DEFAULT_TIMEOUT } = {}, -) { - let numCalls = 0; - let resolve; - let timeoutHandle; - const stubHasBeenCalled = new Promise((_resolve) => { - resolve = _resolve; - if (timeout !== null) { - timeoutHandle = setTimeout( - () => resolve(new Error('Timeout exceeded')), - timeout, - ); - } - }); - stub.callsFake((...args) => { - try { - if (stub.wrappedMethod) { - stub.wrappedMethod.call(wrappedThis, ...args); - } - } finally { - if (numCalls < callCount) { - numCalls += 1; - if (numCalls === callCount) { - if (timeoutHandle) { - clearTimeout(timeoutHandle); - } - resolve(); - } - } - } - }); - - return async () => { - const error = await stubHasBeenCalled; - if (error) { - throw error; - } - }; -} - -module.exports = waitUntilCalled; diff --git a/test/merge-coverage.js b/test/merge-coverage.js deleted file mode 100644 index 1a6617a4e90b..000000000000 --- a/test/merge-coverage.js +++ /dev/null @@ -1,268 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const libCoverage = require('istanbul-lib-coverage'); -const libReport = require('istanbul-lib-report'); -const reports = require('istanbul-reports'); -const glob = require('fast-glob'); -const yargs = require('yargs/yargs'); -const { hideBin } = require('yargs/helpers'); -// Temporarily commented out as we can't rely on the commented yaml file -// Can be restored when the codecov checks are restored -// const yaml = require('yaml'); -const codecovTargets = require('../coverage-targets'); - -// Temporarily commented out as we can't rely on the commented yaml file -// Can be restored when the codecov checks are restored. In the meantime -// the important parts of the yaml file are copied below in normal js object -// format. -// const codecovConfig = yaml.parse(fs.readFileSync('codecov.yml', 'utf8')); - -const codecovConfig = { - coverage: { - status: { - global: {}, - project: { - transforms: { - paths: ['development/build/transforms/**/*.js'], - }, - }, - }, - }, -}; - -const COVERAGE_DIR = './coverage/'; - -const COVERAGE_THRESHOLD_FOR_BUMP = 1; - -/** - * Load .json file at path and parse it into a javascript object - * - * @param {string} filePath - path to the file to load - * @returns {object} the JavaScript object parsed from the file - */ -function loadData(filePath) { - const json = fs.readFileSync(filePath); - return JSON.parse(json); -} - -/** - * Loads an array of json coverage files and merges them into a final coverage - * report. - * - * @param {string[]} files - array of strings that are paths to files - * @returns {libCoverage.CoverageMap} CoverageMap - */ -function mergeCoverageMaps(files) { - const coverageMap = libCoverage.createCoverageMap({}); - - files.forEach((covergeFinalFile) => { - coverageMap.merge(loadData(covergeFinalFile)); - }); - - return coverageMap; -} - -/** - * Given a target directory and a coverageMap generates a finalized coverage - * summary report and saves it to the directory. - * - * @param {string} dir - target directory - * @param {libCoverage.CoverageMap} coverageMap - CoverageMap to report on - * @param reportType - * @param reportOptions - */ -function generateSummaryReport(dir, coverageMap, reportType, reportOptions) { - const context = libReport.createContext({ - dir, - coverageMap, - }); - - reports.create(reportType, reportOptions ?? {}).execute(context); -} - -/** - * Generates a multiline string with coverage data - * - * @param {CoverageTarget} target - Target coverage threshold - * @param {import('istanbul-lib-coverage').CoverageSummaryData} actual - - * istanbul coverage summary detailing actual summary - * @returns {string} multiline report of coverage - */ -function generateConsoleReport(target, actual) { - const { lines, branches, functions, statements } = actual.data; - const breakdown = - `Lines: ${lines.covered}/${lines.total} (${lines.pct}%). Target: ${target.lines}%\n` + - `Branches: ${branches.covered}/${branches.total} (${branches.pct}%). Target: ${target.branches}%\n` + - `Statements: ${statements.covered}/${statements.total} (${statements.pct}%). Target: ${target.statements}%\n` + - `Functions: ${functions.covered}/${functions.total} (${functions.pct}%). Target: ${target.functions}%`; - return breakdown; -} - -/** - * @typedef {object} CoverageTarget - * @property {number} lines - percentage of lines that must be covered - * @property {number} statements - percentage of statements that must be covered - * @property {number} branches - percentage of branches that must be covered - * @property {number} functions - percentage of functions that must be covered - */ - -/** - * Checks if the coverage meets target - * - * @param {CoverageTarget} target - * @param {import('istanbul-lib-coverage').CoverageSummaryData} actual - * @returns {boolean} - */ -function isCoverageInsufficient(target, actual) { - const lineCoverageNotMet = actual.lines.pct < target.lines; - const branchCoverageNotMet = actual.branches.pct < target.branches; - const functionCoverageNotMet = actual.functions.pct < target.functions; - const statementCoverageNotMet = actual.statements.pct < target.statements; - return ( - lineCoverageNotMet || - branchCoverageNotMet || - functionCoverageNotMet || - statementCoverageNotMet - ); -} - -/** - * Checks if the coverage should be bumped up - * - * @param {CoverageTarget} target - * @param {import('istanbul-lib-coverage').CoverageSummaryData} actual - * @returns {boolean} - */ -function shouldCoverageBeBumped(target, actual) { - const lineCoverageNeedsBumped = - actual.lines.pct > target.lines + COVERAGE_THRESHOLD_FOR_BUMP; - const branchCoverageNeedsBumped = - actual.branches.pct > target.branches + COVERAGE_THRESHOLD_FOR_BUMP; - const functionCoverageNeedsBumped = - actual.functions.pct > target.functions + COVERAGE_THRESHOLD_FOR_BUMP; - const statementCoverageNeedsBumped = - actual.statements.pct > target.statements + COVERAGE_THRESHOLD_FOR_BUMP; - return ( - lineCoverageNeedsBumped || - branchCoverageNeedsBumped || - functionCoverageNeedsBumped || - statementCoverageNeedsBumped - ); -} - -/** - * Creates and returns a combined coverage summary report of every file in the - * provided array. - * - * @param {string[]} files - array of files generated by fast-glob - * @param {libCoverage.CoverageMap} coverageMap - * @returns {import('istanbul-lib-coverage').CoverageSummaryData} - */ -function getFileCoverage(files, coverageMap) { - const subCoverageMap = libCoverage.createCoverageMap({}); - - files.forEach((file) => { - try { - subCoverageMap.merge( - coverageMap.fileCoverageFor(`${process.cwd()}/${file}`), - ); - } catch { - // If the coverage doesn't exist, it means that it was excluded from - // coverage or had no coverage to report, which is fine. Glob is a lot - // wider of a net then what the test file runners match against. - } - }); - - const summary = subCoverageMap.getCoverageSummary(); - return summary; -} - -/** - * Checks coverage and reports to console - * Throws an error if coverage isn't met - * - * @param {string} name - The target's name from coverageThresholds in jest - * config - * @param {CoverageTarget} target - the target coverage threshold - * @param {import('istanbul-lib-coverage').CoverageSummaryData} actual - - * istanbul coverage summary representing actual coverage - */ -function checkCoverage(name, target, actual) { - const breakdown = generateConsoleReport(target, actual); - if (isCoverageInsufficient(target, actual)) { - const errorMsg = `Coverage thresholds for ${name} NOT met\n${breakdown}`; - throw new Error(errorMsg); - } else if (shouldCoverageBeBumped(target, actual)) { - const errorMsg = `Coverage EXCEEDS threshold for ${name} and must be bumped\n${breakdown}`; - throw new Error(errorMsg); - } - console.log(`Coverage thresholds for ${name} met\n${breakdown}\n\n`); -} - -/** - * Primary script function - */ -async function start() { - const { - argv: { html }, - } = yargs(hideBin(process.argv)).usage( - '$0 [options]', - 'Run unit tests on the application code.', - (yargsInstance) => - yargsInstance - .option('html', { - alias: ['h'], - default: false, - description: 'Generate HTML report', - type: 'boolean', - }) - .strict(), - ); - // First get all of the files matching the pattern coverage-final-${n}.json - // from the coverage directory - const files = fs.readdirSync(COVERAGE_DIR); - const filePaths = files - .filter( - (file) => - path.basename(file).startsWith('coverage-final') && - path.extname(file) === '.json', - ) - .map((file) => path.join(COVERAGE_DIR, file)); - - // Next, generate a coverageMap - const coverageMap = mergeCoverageMaps(filePaths, true); - - // Persist this to file, which may eventually be used in more steps - generateSummaryReport(COVERAGE_DIR, coverageMap, 'json-summary'); - if (html) { - generateSummaryReport(COVERAGE_DIR, coverageMap, 'html'); - } - - // Use the keys in coverageThreshold in jest config to determine targets - const coverageTargets = Object.keys(codecovConfig.coverage.status.project); - - // Check coverage totals for each target - coverageTargets.forEach((target) => { - const summary = - target === 'global' - ? coverageMap.getCoverageSummary() - : getFileCoverage( - glob.sync([ - ...codecovConfig.coverage.status.project[target].paths, - // checking test file coverage is redundant. - '!**/*.test.js', - '!**/__mocks__/**/*.js', - '!**/*.stories.*', - ]), - coverageMap, - ); - // Check and validate the coverage - checkCoverage(target, codecovTargets[target], summary); - }); -} - -start().catch((error) => { - // Report the errored coverage check - console.error(error); - process.exit(1); -}); diff --git a/test/run-unit-tests.js b/test/run-unit-tests.js index a58a858c09c8..645bcfc02e1b 100644 --- a/test/run-unit-tests.js +++ b/test/run-unit-tests.js @@ -64,45 +64,14 @@ async function runJest( } } -/** - * Run mocha tests on the app directory. Mocha tests do not yet support - * parallelism / test-splitting. - * - * @param {boolean} coverage - Use nyc to collect coverage - */ -async function runMocha({ coverage }) { - const options = ['mocha', './app/**/*.test.js']; - // If coverage is true, then we need to run nyc as the first command - // and mocha after, so we use unshift to add three options to the beginning - // of the options array. - if (coverage) { - options.unshift('nyc', '--reporter=json', 'yarn'); - } - await runInShell('yarn', options); - if (coverage) { - // Once done we rename the coverage file so that it is unique among test - // runners - await runCommand('mv', [ - './coverage/coverage-final.json', - `./coverage/coverage-final-mocha.json`, - ]); - } -} - async function start() { const { - argv: { mocha, jestGlobal, jestDev, coverage, fakeParallelism, maxWorkers }, + argv: { jestGlobal, jestDev, coverage, fakeParallelism, maxWorkers }, } = yargs(hideBin(process.argv)).usage( '$0 [options]', 'Run unit tests on the application code.', (yargsInstance) => yargsInstance - .option('mocha', { - alias: ['m'], - default: false, - description: 'Run Mocha tests', - type: 'boolean', - }) .option('jestDev', { alias: ['d'], default: false, @@ -156,8 +125,6 @@ async function start() { throw new Error( 'Do not try to run both jest test configs with fakeParallelism, bad things could happen.', ); - } else if (mocha) { - throw new Error('Test splitting is not supported for mocha yet.'); } else { const processes = []; for (let x = 0; x < fakeParallelism; x++) { @@ -179,9 +146,6 @@ async function start() { totalShards: maxProcesses, maxWorkers, }; - if (mocha) { - await runMocha(options); - } if (jestDev) { await runJest({ target: 'dev', ...options }); } diff --git a/yarn.lock b/yarn.lock index 0748c38f545a..176e4ee08108 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22620,7 +22620,7 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.0.2, istanbul-reports@npm:^3.1.3, istanbul-reports@npm:^3.1.5": +"istanbul-reports@npm:^3.0.2, istanbul-reports@npm:^3.1.3": version: 3.1.5 resolution: "istanbul-reports@npm:3.1.5" dependencies: @@ -25685,9 +25685,6 @@ __metadata: immer: "npm:^9.0.6" ini: "npm:^3.0.0" is-retry-allowed: "npm:^2.2.0" - istanbul-lib-coverage: "npm:^3.2.0" - istanbul-lib-report: "npm:^3.0.0" - istanbul-reports: "npm:^3.1.5" jest: "npm:^29.7.0" jest-canvas-mock: "npm:^2.3.1" jest-environment-jsdom: "npm:^29.7.0" @@ -25712,7 +25709,6 @@ __metadata: nanoid: "npm:^2.1.6" nock: "patch:nock@npm%3A13.5.4#~/.yarn/patches/nock-npm-13.5.4-2c4f77b249.patch" node-fetch: "npm:^2.6.1" - nyc: "npm:^15.1.0" pify: "npm:^5.0.0" postcss: "npm:^8.4.32" postcss-rtlcss: "npm:^4.0.9" From 215f06ce426e3ec03a956ec81fe25b7a0e2d46da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Tue, 25 Jun 2024 15:38:04 +0100 Subject: [PATCH 19/22] chore: updates MMI packages to latest versions (#25502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Updates packages from MMI to their latest versions. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot --- lavamoat/browserify/mmi/policy.json | 203 +----------------- package.json | 12 +- ...odian-connection-info-mmi-visual-linux.png | Bin 77916 -> 77321 bytes yarn.lock | 112 +++++----- 4 files changed, 62 insertions(+), 265 deletions(-) diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index da30479b362c..3863648c3f02 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -789,56 +789,8 @@ "@metamask-institutional/custody-controller": { "packages": { "@ethereumjs/tx>@ethereumjs/util": true, - "@metamask-institutional/custody-controller>@metamask/obs-store": true, - "@metamask-institutional/custody-keyring": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store": { - "packages": { - "@metamask-institutional/custody-controller>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2": { - "packages": { - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream": true, - "browserify>process": true, - "browserify>util": true, - "watchify>xtend": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>isarray": true, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>process": true, - "browserify>timers-browserify": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@metamask-institutional/custody-controller>@metamask/obs-store>through2>readable-stream>safe-buffer": true + "@metamask-institutional/custody-keyring": true, + "@metamask/obs-store": true } }, "@metamask-institutional/custody-keyring": { @@ -850,9 +802,9 @@ "packages": { "@ethereumjs/tx>@ethereumjs/util": true, "@metamask-institutional/custody-keyring>@metamask-institutional/configuration-client": true, - "@metamask-institutional/custody-keyring>@metamask/obs-store": true, "@metamask-institutional/sdk": true, "@metamask-institutional/sdk>@metamask-institutional/types": true, + "@metamask/obs-store": true, "browserify>crypto-browserify": true, "gulp-sass>lodash.clonedeep": true, "webpack>events": true @@ -864,54 +816,6 @@ "fetch": true } }, - "@metamask-institutional/custody-keyring>@metamask/obs-store": { - "packages": { - "@metamask-institutional/custody-keyring>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@metamask-institutional/custody-keyring>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2": { - "packages": { - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream": true, - "browserify>process": true, - "browserify>util": true, - "watchify>xtend": true - } - }, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>isarray": true, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>process": true, - "browserify>timers-browserify": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@metamask-institutional/custody-keyring>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@metamask-institutional/extension": { "globals": { "console.log": true @@ -926,55 +830,7 @@ "@metamask-institutional/institutional-features": { "packages": { "@metamask-institutional/custody-keyring": true, - "@metamask-institutional/institutional-features>@metamask/obs-store": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store": { - "packages": { - "@metamask-institutional/institutional-features>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2": { - "packages": { - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream": true, - "browserify>process": true, - "browserify>util": true, - "watchify>xtend": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>isarray": true, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>process": true, - "browserify>timers-browserify": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@metamask-institutional/institutional-features>@metamask/obs-store>through2>readable-stream>safe-buffer": true + "@metamask/obs-store": true } }, "@metamask-institutional/rpc-allowlist": { @@ -988,7 +844,6 @@ "console.debug": true, "console.error": true, "console.log": true, - "console.warn": true, "fetch": true }, "packages": { @@ -1008,7 +863,7 @@ "@ethereumjs/tx>@ethereumjs/util": true, "@metamask-institutional/sdk": true, "@metamask-institutional/transaction-update>@metamask-institutional/websocket-client": true, - "@metamask-institutional/transaction-update>@metamask/obs-store": true, + "@metamask/obs-store": true, "webpack>events": true } }, @@ -1023,54 +878,6 @@ "webpack>events": true } }, - "@metamask-institutional/transaction-update>@metamask/obs-store": { - "packages": { - "@metamask-institutional/transaction-update>@metamask/obs-store>@metamask/safe-event-emitter": true, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2": true, - "stream-browserify": true - } - }, - "@metamask-institutional/transaction-update>@metamask/obs-store>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "webpack>events": true - } - }, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2": { - "packages": { - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream": true, - "browserify>process": true, - "browserify>util": true, - "watchify>xtend": true - } - }, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream": { - "packages": { - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>isarray": true, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>safe-buffer": true, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>string_decoder": true, - "browserify>browser-resolve": true, - "browserify>process": true, - "browserify>timers-browserify": true, - "pumpify>inherits": true, - "readable-stream-2>core-util-is": true, - "readable-stream-2>process-nextick-args": true, - "readable-stream>util-deprecate": true, - "webpack>events": true - } - }, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>safe-buffer": { - "packages": { - "browserify>buffer": true - } - }, - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>string_decoder": { - "packages": { - "@metamask-institutional/transaction-update>@metamask/obs-store>through2>readable-stream>safe-buffer": true - } - }, "@metamask/abi-utils": { "packages": { "@metamask/utils": true, diff --git a/package.json b/package.json index ad044a2f4ae5..afb8a17584f6 100644 --- a/package.json +++ b/package.json @@ -275,14 +275,14 @@ "@lavamoat/lavadome-react": "0.0.17", "@lavamoat/snow": "^2.0.1", "@material-ui/core": "^4.11.0", - "@metamask-institutional/custody-controller": "^0.2.27", - "@metamask-institutional/custody-keyring": "^2.0.0", - "@metamask-institutional/extension": "^0.3.24", - "@metamask-institutional/institutional-features": "^1.3.2", + "@metamask-institutional/custody-controller": "^0.2.30", + "@metamask-institutional/custody-keyring": "^2.0.3", + "@metamask-institutional/extension": "^0.3.27", + "@metamask-institutional/institutional-features": "^1.3.5", "@metamask-institutional/portfolio-dashboard": "^1.4.1", "@metamask-institutional/rpc-allowlist": "^1.0.3", - "@metamask-institutional/sdk": "^0.1.27", - "@metamask-institutional/transaction-update": "^0.2.2", + "@metamask-institutional/sdk": "^0.1.30", + "@metamask-institutional/transaction-update": "^0.2.5", "@metamask/abi-utils": "^2.0.2", "@metamask/accounts-controller": "^16.0.0", "@metamask/address-book-controller": "^4.0.1", diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png b/test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png index a722d3cc1bc71a8740e538a583a75f1e541c0fae..8b628e6b61cc93f765939fffbfecfc6f8235232d 100644 GIT binary patch literal 77321 zcmb@u1yohh`z?9^rAxXIq@)F;OAw?Hq+6sLDanI+=$4WOkuK@xfRu3PZUm%Lx_JwK z|NnjWjyK*N?~QTSV8G#=z4zIB?X}i7zd7f(Ltd-A!oj4#gg_uT@^UZLA&`gQCGrqD zGWa7&tAGUlAi1c&l7f^DP;EjWk0J6eUub+t-I@3JKs3I9cEDR%`8;L8JBI;=n-#Pm zqQ3Jf*ob)hr1}gse}kB~k$(J>T+EP$7Y05$UGrJ`4%4)e#exLt1S19=B>_Lf;-E{$ z)y_a_(#`I&!e;hxey8 zN6!>1pFO^Lf-0a*`q96}T0cVT)hM5g8j*~f&PND!$UFVTiJCGrNz%*-D@N0p*lemz zQm|hGYT(;cs#Vi+xsu_jMiPG&93-*^>JlyH_^AnNY_P%5g?tV{iHhYuD@6GD1CtGR zC5zbC!7aw%qYWDv_B)=WUBicxd4Z7ojX|47M&|y{D_1P^*8jX3%Apc^|9yJ(fBf%u zuvt5e9sb|@7{c84?2|t}^1tt02qcruD)|E?M_a$izkk91WC9Kb2%I65|J`?RJcgkB z+g`HP|Gy7Bk8JS~6};)snIrl3SIPL(DyFoU(Ig#S#B}2~=rcaXMTY^D6IOdKLuwxpA_|Tz;?p&Xl&8mqWBm7a8OeXIKek}; zPDCC0uWxSMjaZda2v{=>k##iPH4L-Iw%{*NidD;@+~C??;mD5pgd|J`I!Dr@&6$c0 z_RZ%$l{zQp7zlg>LX944a^EMdy9x!oyGmMf z6PuIYB(oOdzrD6mKIMFn$tI1*E!n76G@hrLG0@-72!jO#L{JH-GsR9fvfN*{qNORG z#n|{}-zn$!d6w$j&q*gP-wYU}T;CwMqWe@sW`bn1r_0}=W%yyx)z{ZUum1*V6;*o} zLhin?V6J2!Wg<+3O2cxDjnW2Oxr-uKh4)2`Ahlp1_@6p&jTOi`YnNzc2)p(A5hL++ zIa38aVTEk4e(9|uX3cIc+Q28q>H5x-Dvh>*5AOYLW>eGL$eAq%GjkqzMMDfO&DC7^ zNs3NkjR!F`OSwkL^92iDJ(M{Ta2Fg%x}CctsT&t&-gQT24dQ@t;hV;#wfHIJCa^A4 z8~Z5qw7fl>MwU16^;|rmxPP+=0lR;S+r^=Ij=E=awL%V$kWjtjVoPmJzKYp)js0g- z+^zY>#Ad9`Wj$#6LrXKr3~J(#2MsD=ids&kThVI)XJg}}qBuILr?%Woarri5X zeXNE`nV*ASK#3*2r68zvPbngp=;*Rm`PTKGeK*iNup$M$1k zVnxx2@%!i~4W^4>&?Ry*8Y(C(Yd^zY+1-8qVaSA()KbwqO(iioSx>{DSiOKO(AnJ` z0}m@K7(I`Lm6!KO>3X{04U@OGw}4kX42x9#&FG#EH!ZK;EU7x1|NQ)cZ7|FS5ywwv zpZ0`x&X9-hXUM8tPA?1LNLwkrG4rG`fvsyUF0BRC7E{t#`=;wh)_Q|?qk$HYZ1&us z<3x)2{P)oF;vf#aNxYWfU6g&TZ}})xGsNfD5{GQux}&HaU4LsD8!zr)>u0};pc1qB z<9~ZIW^=MoS9f{o<>x6Q(^)#-$je*lm2 zW3s7g;c}wlbEx?^m=lc;H_w==m(q|W5n$pExv#FSPESvlSSQ8D6H`;~Zf;HoYR4!r z62}vhaxvjj@I@P&@+3u4iLnxehIiR^Ij9j~U}3G$p6yJ#te4J{axz6yQWmKf+zRJY-s_7mStz z9YquxJ_}km-_OB(}gM|f_l`7tR^vUeMgr!2$x(6eRIBuR9S6vb8`Y` zWMo84OiWZ%6rIki`Swr28;QHa?FoAcTA}w<6@@k{x&4eeIk4!TKfgt!@Ka*nrRUt9 z)b955^h7Ak)L9=%1VtU#3OLN9rp1od;C&CPk57}S!{hd+&a$kdwzkWl5h{^VL{D0n zH8+L1M2AgX)((xnsCzuqugNM`I+UxuHTS)5heg{%cg2W4%RilVmtu*tr)1oib^J9E zIqe?NCdcZeO1KlvBlbM>o|55BB|8b@>0&u;BpE~2@jq>=|Yijrz8FRp}w*OGFA?{mPSg4XJDC9EHH}YjoKbD07eeU2>Z6zFu>($ptR?%IW)GPH!V6>5KE z%nnIj+=koU_Tu423xz&xd_z2LLrC}MeZ9K_ZQ@r+PuU#KUou2nOq?1NxaJdoNty9* z;&}NU4UJ9HYv!jgbZ8pnb5D8UIL$6Rg;Zf9k!5nY3eVTOtlQ0R&DFasEiKun;Mk7+ zit%1qzP-JojaL}2d=>TM2lqCO_V!P7I^i^NUf%57T#KLQ&Q#r-$6jdl?KsBttUNqi zToNK8jUM|8u;{8PJ~XtT%*@A9a_`@V8aaLy@QBo4WrRIAze1Q*9AS{!d0`nD8Ts_n z(Cpb?;WwZj-$5XF>)##9>2tE=bhlZ}y}T5@`I^1)uHRA68r$xt!NsQg)AE95BEm4` z7;+^Nf=&mb6(xrU{_yPpy_B4KK^M>S$$ENCG&m*1>IsNKHCFv|+REhEft8knU0q!@ zOT;qt!*%8FR!(<4+?>gUUF9n61tYI7XWehhU;o0UB_YJemqd>V?zB0H*k|0>+uK{+ zOiE85ZuD@>QP})FiI8P1bRdt#YtdtufDclNrCw43nt-QO-vR1Y39k_fI@!brqMxAriu#-!D3%&KbxDE_exf_#-5*DTYFON$*T_^KA=DNv|XJfYE4fE zdkZTq<-C@&kdl*>lzj2x1-#|vkMiZmahqx-rb;R|8Ce+`F|Xpfy1F0v!Ee6?2M0q_ zjPA-r;MJ>Sk>D~9UUDtw>^KoY z21Y|I7Gg24WBibp*YAyuGs2ItOSEh^9{|Wxz4MAS{*lzy)>czf6FME3^pjY2M9Lnu zfRhDw;IR=@~L z{pI+TcvF~bruy|;?h1qGFge!mMdP{QtR*KZ1fk<}rOV}SRhIU?pf*qIbGpVpQx6nR zeUqVGG*~tsR2M>e;!>nRo|Y4yhw`R`KS}60-FLlre3jqJqLf&_$Lm!pYam}@VKw9S4mZM(yE%Gy5ZY5dLhQu z<<2k%htem3c(upGZ-_db8AOkT;g-)&&jfQ{pQZ~@A0eUX$;m`0oKGevC$Ewn;bE$e z?4CFl_+WN8i}Xejayk;YPu)_;VS>?9QvNpoQFlzSO2T!-XS7Qbw|N3`ekED z7<4u$`uRg5isVqsn3Qm}t-ib?&1=_w5Jcw;)eS83HOTwf6AiNod8;yFhY&B!_*~@8 zlh2DLFV~(Pqw=nAK0bCQdL%W%R{hFlTbcb8`qEW`76$~oBBcX=uyP&%*n>1m=Bv@%F3d$v$M_V+PQfx`vjrlbW1+S zh;ni9mX+>^S?KS_@Pyn=jFs;0?hG;KWtb8ZB?<42l8;ZN`LET(x9z51zI@@~tu*Zp zcW^h|Ds9VU@kR)HJDZ`HaM{-lks7vf9Jml+vh>;^qVkefITP}^68I#Z$VPOH?d*CU z5nnp4uyLqX%bw~u_*Q!V7JOf??0llXF3Yt9>>QNEx{F(+fgb zWx=_sHDc3vp0T@EaPl!h;`3wiA)1>Ig<1aIOS!275Gp|@X ziH0E5kVQ5K<@v=$HKJ~HW5ddCjdg=ZFP7D@Wirt=vLt0u2?^p9YuvuQ`{Q}P!nPmHzkCcxv@~xW!!N%Vi8Hf7OoF%upQaH}gpg(u+8lQ_y=CdF8l(=Q|DKq~ z(5LKEu92pf)f^%FGVFot_gb^ejL)zAq0avLNJwOmOb$Jj@4LjdvjS(!6ej+-7q#~DYI2bJWeSO8$g z31Sz}f)0UDP9=k^xjhg^`tqSaB_=vL8NZ!QWzoz50uT}XAX|i3p-qaQ;^5(d40(%# z3k~9+_$E&U2OZtYo%jEN&k;@su zEkcJhJO-II$wbX=5I9@*`wfIKapjRRaSleo9?th~I27YFa|*&9`t#eiJ7W2{Kp=W3 z;D-Kd4G%%o_UD&65T_!H2gn}Uq}8(eMX!^UEW8s@ zYCl@wNtQu1LbEj?bx(6vi6=QTlkH{I`q>@b>0HsUpF}G#IFxcCBVE=)^gcnb=VK%v zJH3w+5$fGFM4v;oQ-W`lBKfDRuCLhNR0Sx7VPiu~*iz|{AyxtrBV0eZAyr6z@(_qH zF1QQ-+J6YG8f$Y$Y+-vyX8(Z*3S{WTeJ_?Gaz7*Xd>9HfSS7Blc3~m0(o*eDyiU{k z%x8jVOG@(dtvYjSV_`6?GqE34misJl|I9Y&cj?fjoaJRxZ_;r+rdaEvgpwnYx#4VW zv)6J{`On_nT|oz#^S)0tmO=~0ZB!kNRUH@2@x;yc^^$9E&ZzgBu8a`&3x4bKyYPLh zv&1gPM-WNxHU=FC#OX~Wtwgy`XFF!V&x$`IIf{v#hL@%{iN-!H4r}4$?j@zwEjn4)Hyj%q}Jya)vB_wvtK&52QGT-OjX)iYG|0*8qq&{_V;%d9G&iH zy?+I{E#8Ca$H&-GzhYI3<6P&x zqmz|#-7FR~^%pmRML}UEwtIm6o>IcFQ@6w0>0y_yqHLgC*Flm;t73)L2JYnCBNCuTFN^ zJmM_P%rvwo@ukFX|3CpQIGHqRPD-4rTpvtpbSIRe@%$h|irebJscWFEeYRy==0bJW zmOLIYA%6EWOv2%ObKYa2#ieK2-BT!XrFh-N;g|>l=%xFKH1o@jU*h6;)aQ~~z2l2|ou3eVe{3Zy%al>1+PDuTlX$Bx`C2Z@C`K`n1D={{H?6sNiz3$S`z|oz z!{(~?<;$}pEc9eix61%5(u?YPPtWV|tg|`q#T&)M0GP~!02qg1P1CS>7aHEpbH_Gn zx~wIn7j$oOl@|dkbOP5_Q5jfa%h(~D=i(N0?VEDW@V$LVJ5S10j~Jb;Ei6pN|MDfd z7e2Hn=3RWo9us^K*?)L=c(!1ysixb*-8X_p14c>$KS;u(uGfcu1gAd|^Om?68&#D! zVuu%1RUIF#6N)zb-tAwIUL@eLdk>AydbsZBNd5X05C-1ALHW#ag@KE6bbOj7WE*+H z%q#4^W0?URuJjli`nm**z7stagdass_=(z`%YnH|_pH6<=2~$SnjIJzXg}-61mng& z8V#hqI95b|gc%SeaXRI_-*U7oU`#tlyZ$&fEhK9F>|(QF@SNYc*m#gj$m^JluET`8 z_~Lha*0Xl-{b)ELy3K1SpW6<=R^XH__KcNbH8@6CW?ZW|1pc%+c^CwOHaPT~{cw`;RtgrpfCg3%1U>8~woIjk@o;qr@vKXNqEzi-S#4l-+lx&(oK&)NxX+IpZ00&!=UT87*2`FFg`dH*(Z=nO-n6KEW7wE zeG8K4rC3!lW%T;(0mccO0cHlW2wACL;hUo=Mnz4_S*s;wOXHIZ=g#e@x6W#6!!uD5 z7$9P2`O28D0xt3B@aQ5Brj`PeN~dW8;_e z+~lIq)f_a#cdLh4ue`vXdxsp7IiH0FAeQGmb9ardy*V-}Vls|l2TGr#hTX=yXM zX}n!;o9eGw=HgYwb2L~>^i`5XHz$p2IP@FhGF+@~w2cL3#tGSI?{+6MUPY3%onh}^ zxNu4Al4SU;_or1_o(iMNOqmNUn6Mey+vh5 zn*xAW)i(C@T$mG@m_6NiRBk>hd#n6ND&6z4033q9Ik@VfSF<<^%Rxwd6qmt=pp(Oc zgM$c4ziX3zRzDIp#q|;ydu7|Z2nQC2MRBQMIQ*_OP2~3IS37Ol&BlWFUOKPkX@W9k zknv4j%f%%dLd177NBjJE77o2*&d}+pEy&ORaJgv-sQB2}N#9c;x|gi+;+Gk(((mBw zKEA#s#txm*+H0Q{syun`Ob`hrKH8@hIsL|YzHDN5&_S`ogHSN>o4`;7QJ@oR@Ghg7 ztpU}q1p+BW6KjP)1d3*9Y#SOIkMJ-tG4&mKqJ9#Gyu3QyTWZ94LQgM#ZDnt0IKNQm zdp74wx~N1S<8w2f_`co;P;dIR_M_V;o7S5XS0UuWaT%rZ3xs#JMXVsBt4mJzXMj;rT>nf$>Q`?!t^QG45Q5ggA}L&fo#c@e z)oSFMTqTE74v=bsP7IguKFy0rSzbN`7((h77+xZ-8$%#B{k!zTW8bLJh%D*ZGc(?8HMt!u2lC#oc6?HcfsZGPr=-r zL9>zLyvWwx@C-DY^6>PqFh3tmKhL|HaYAO5uiSoAbPRk9;O9^6?(79WVONxkyZc%Y z+dm5RrR?bf?HO}BH&u7P@s9(*KQpVQ^>VRHwcL0lm}Ago-0l%A(TG3;`(BQ1jwWD*y9paw_r(7p_jc>3 zC@R|C(%hdFCg$ySZZ|cZfZpo9S6>FFe-UtnN0nb({73Z@HZ>1S6%PBCUiuCC5dAU! zX~NBKk55E4#dpJhBPkg9ra)r?ClH&a5AV@pEflI?W0Tzvzr0ML7P+-@MeSA(H*)-8 zbUPs8C?qUQAhkT0?h75?dX(Lz(^Vy)X=F68Z>iJA?U&9S`b{>1qNj|5la-T;Yn?WV z%rECmkU?;2aB$FzIjUHvZL-pQyGB{R<`9cRto`()Er`~)VXt!i_G$;LJ!_+G8M@PQ;EjotqDdZduchlUh&F<&+f4u;^Jb|5GT@XrPVqqyN zmYEI5=BfO}D=vFt4ig|tbZvf`$l-ZD7V=J$*!=nqcW+T!Q|+`D!F6F2q1mmP`kNMz z>+sV{N&p%X#D<8J(F@=+j&mA%dY5$=gsK@r?PsW5x>do(3qwP{H#d9E5NWD%KU__^ zy1NBtQ0@pKj5?=#l4y7ORX5_3lFaMJe>{)T8FPJ$MH>tfKlpa3$VFn^-N&4SHv|CK zg`I30Ls$A=(fHbp{3)=B2rTQkjC-lj`~0KlpWsJ%DpXe{{e>WkjqsI>+kmw=jm>d) z%O1A9UDouRmX_9U2~g`XQEC z6%z-84raGd;np1~flh~licQ92=7ey(AUSuPx4g%rv=`UQSc6CJvQi=?&>;RQ=zt7? z%)F_kz~#V47r#1&x~%n2Zl82VWsr%w>fGJl=p-mBE05ZgxuOGUBMra8tY=wKULJ(t z%f4x>eVx}@MlHTq`!IZv_a*V>F8VOKtoP~Ot!l%fFFe>A&e9vxQd4oM1V3yYZw6_L z9(Qe*MbJDSnMD+Rt9U;<2-ncm1n}9>(ZImVt4mv((CqvPQ1#7%qNU~2vLo;N_{sr5(i0&!1HPK3O61mQNv`D!XEDqtBBclvDt?&BvB z66G`=p{GJgaICoJ8AX$IrG{6|($b&9Dv!Vv@jedzO)7r4@h+B0r7Aqx;Z<47^+`EEj{_OO$w6JjDXgy=% zO33HWuiki7PUG9(hKdQ_b8?7@h=3$0>$tmVnUN;q?Jg%LXSefpb2N81B1}Ut!+oaNbjwJJVHW3&}V)#bv*>}oyJ$?KbW=-~DQ5W3PR&Oxtv|a77UEeV+MK*AGjG|7if2LPL7>w(tm)YA24V6^l(;PV#b#avEP5Y))g=shB_k^f3o=$vpogW3c??we{EBY7hA9;- zc<*0dz>B~oN z8(rMo=Ko%uq9t&y1O(Q#i@ z;s%yMzWukG=H@!=#3o|xh-<>Q?7VTm!DW2w`E{KGm+GL9i$(3WDt#y~1I!HKKOqeX+yg0D(Q`duYT-%JAEs(3$aOxuMEY83GnysiCi|`{ zJW|5b0|6*AksOHeX)d?9^{WED}l67fDhOD^(!hWbJRCC)Uwlk zPdEL1uWGc(%bG70z)WqqIlV$8a;504F{MSAL7wNu@$kKGN3}6cO1EyK-q)rS!P|`^v&1L8}M{ z7uWCi?Nx$AT{!$FKR5U9jU~9T=SemEPDn{@tjhXL0QP9Uh&DUPU6UKad%sNl{U13u z<%pmlG>L-4+z84AudB4aK6Wmyvb=Pk<&xS(2iF%T{aD!8f*(v42jSNZH)o5nv2Uy#czPY`H~+n~74ts52FqhWKtNVjmTIxjbqI5YjYhozXNH(96I8z-N3pE_?_tT= z?CGMh@9qg0FNT#~nxoaU#k$IBV?|}nduwYo8#6r(_L_P|);Gs9cC$W1_7wh)LK-Op zAIUKipd~3iZY7R{aAYNLdJio;hg3c61{!BMJ6r4w|4r_+U8x!iqiMMY>!XE@L!pfU zJzD>gL;;&4&@moLZouG#sAvz2>f>U;#|#J|C1hWk=JcDr^#-Ev2~KH>*Ghlw<>_wY zLF7Y$BPm3pu?y5+1AP5SgtKmYS|*~ef6nsq-(S9 z%~|EK*>ej4fJYl1fTXo$NkdmjiSVV?2GZUwg8Wjq>L8NReYb`x%8@ZVY|jCP{lqnH zk}zH`yX^(8Bo?=@#<+Drc8{<`=e(>(azi8Mrh0yLAt zZ+#1zzz>)Bi3X4f_if|evICjH;T%NqcMqZ^fdDS=|8`;huMFD#_Wx~~0tfk@?OJ>H z7B44nMI|NC4;x%kzpg!9ypK2D?!O1S2y+2C65>Cr|38sAc=tyJ7ySTGNcZg@WeI_p z@=+`rR|_{R`e#P}5Fz0(I-6bxnKbxW^u0q_^f#Zzk+|WrJ{$v|=WnzCPD52msmji; zy&8PpevT1~{7K5|IMkrQ?qcf1pTT4P60{O6-CK^5ET|0I^lOM z59WXK+1&i4_JcZKVixTBYT9+TgVpZDt2w7FFoXXPV`6^ZcZnJ+uD4;a#!(F^ zVvS1N2g+yVsS1u-oAT8&afT&?7#j;lUy%H8O;Oi-ksJV1?$6)btGPy6pc9$>flmfq zW|S!F{+A3(XfDgo^`%1!>G%<{4Gx?Gg4sqVh}BM_u^%H0J>aKU<1IQm`eWGf_T<^$ zc-7n4-;>4|blB}3yYqx36bk6%&rIujteUR&8evjz5w^AVS4sHv1Rbh^z_T^ zQ4x$~@eY~r z;;35eCtVO>ipy+>odtJA-eCMX$(Vkgap>|W*Eq|~mm(ZNDPqm7H%HIYY#x(9@Il6e zz@YnxgybX12n#0@|Jm@uTK*{=L+fJ3U@a`#d#`2tBibG*W`KZ!zPg?iItDIo&5jSi zj@O?7DUMM5F&Cfhe&bd;wmaWQde_?B9Rz(1ED}Fcyw=ldsi>(j=>A#`w!m#pg4N!! zxBozDAp4v&n^-FPCbMC8q%2^eBmL>?n|MvjjSk;Qh#|_o)~8{!Qs82K!uq5>*}Lpo zLgrEOH|x-fW5_69R;jh*utsyBa?d1%()khUP{;>wZ$hci&@hnExaM>jH`9Dw-(G)( z{*?1+4`OgV5m-tb^=ohJc#<6ICKd-X@YQv6tfnt|-sGL5`7pSAqkWq^DZr;g5=r%J8RSDCVm-H?(MZm%*4s^SgpW?BqGukdZz+ky66c&4 zzKyI!4)D_sl`42X-H7jBihVDXW{xHeFl%k$u*<0Zv1z&yxKSS)*YL?f`#Pk>vB9Qj zNS$+6>5EnEHVv%e>+w8j53w#TCts(8N|i%69CF!_bi+S>Zz!vM^H zZ#Uw+{wgp2civPmp(c2ereNP$#cys!YLe*hP9gdpr$(DdYg||@xnOOPQWG-oqVc1D zY2rU%(kYENyS`Wl*%_ey7rddGqo{xsalh^eK|#aS(w;0fzGH;HevA+Bgmbyh+~Lgs z0=4gHA-EO`kskoteyGL7Y&62&dsK90K$5X9f`VlvKmXU*SQUVyiN>DQ+GXjnOv(ng zL+v;?I7M1z<$R5IE#9*=Q2&9D)jG}uKTkK|(YE3S-55^3fb0kUr$H`?kdpYMB8>v& zh(JDl*%yQj!|!Ko*pK1O9Yo5SR?G%+1w9lqnP9;;*hQr!L>?rEQ)ky*J%NG}oY=Z>4JYpFs5c1IP0t zBqdM&T0RiO8Gq_*z&mNZ==63JbrE{!i>84FF`PbJMNo2N6?-8Uta&}uhyOqfkxyrE zjk~VRQqMDF`Rk1=8rob*ZhWc0gc>`h@DE$L@%gT9bW9|oY!C@jGPSm62}l;1txh~D zw7zT>zTo?!eWRS}4nxzBO6|4A26t5UpUi)XHf!bae#C|+ zC|U&sJ|W5(!j~ZRhBrH?7LDK5+?<6FG6Qa8x{(9|seAT0q(6UTx&avU>dMOcG<)$Q zrd3Tgfq#B)%0uvRyM`YxF5RuH+08O%@0BCU-Q=DG@+J0+G8z8iJE)qOgi zWO=vd5Lzr{{celoZ@=J^Q)9M5XljaybNORi+1-)K$$~Mtvv*Vn7cdyGV%{fgSLtQ1 zvWKs41|<~C4V_+_$jPD0umniO0Ldpl>AByvpohm9|IVlxkNy1V5#RvZ+S;gJ^8huP zvr{2u|B`a|2_tMr`AwVv*~NlMbI3p6Ffkhr5Xg72kjQ>_@#$5b5W^=s43iC9M1B_` z6p3@8mLFUEE*W7P;Z$@S4^0X*^mNsdqMhDu4AD{F8HoVr`5l!w5F9NAzJhGRLU#cU zg|`PqrHcDnto4z9{Rs4ZJ+16=&kY`E^N-aossCU0U-A&;|4;T`Hj(3d^E1HJ4t-Xp zcQ~~r+ekIZcS$X|%CW_^O~cL6scC7_mnUL`(24Q>=k%gY)BJ#f!64;~OH3T)+g;jw z=JXpPZZt7Awi7Wc;BIFJ)(l-;A(=-|=v|g{ATS9W9)^An!u^}#I9HW~Utx0FG^5uE zXy$!`3LS<99`>ur#hO<#r;(A7Ofb9o8V*+0!j{sOy6-Q4aktU60YkkRS?(%DF>q#& zPfP%N^{iFA5>pDFHNC_`^S5u+u%7?<^T$XI{@M95;G5QClk1ynR6H7nSi_{wmt~U~s&Xu>tQNWxgOdV31lod=eMn2gTZ--IVs+P!~o6UxYwkNvd z4c~;D&9STVK0CR`70sBaln#x+jt`>yBHzy^8O*ZyU_Ly)w$d*5A1EB&47UqT`QTF* z`&u$b(+Y!*@JXule8-8fi$lbKOz-hg{Dn}6dmamCyiID(Cwy&Z`Q|J=bM-AdtSb8t z_g3xudBK}pZ!uaH7CU6MT;)|Gf7v`DWS2y=-HScae{xKr=gj*X7|kapCenon_m_8e zmX?OCN~E%aXKN)`Se)jj%86vmEzEakp(Yc3 zAnE{i3!L=gmxMR!Z*FdGqWysV1h1rRP3P;IY~HWabV!HX9?oAI>s~mDCoR7I&WA50 z8zj7?yi=<2P@=>YV9vJ^wOJnzZ^$QI$8rD9n4o`mStVNtmG)c@#;FVjUMf}#*8mc> zTS-U=y@UQzA+OY2s+cY0>xOd2)I^rjvuDo~%;#gCys{ifrHfZ6(FQUu9{K?-uNU9l zSaB~Cx6gq6IY34x-2d<4p^Z6t zdV0DvDjq#dP)O)xZ=6klD-o|%p!j3n_|_j>T>EQu^kJY5;yXHWss3wzSw%%*+43sr zph9GMF&GphNO<$JvW9ZS;}eijGoG^yRUKYo}vD?_xe2H(W51KdHI3@ zYw192CRiNs`%SRsDpSv_B_lyHV}b2FG_JZ-9r%wKW?u1ECn8GCWSSRtmRj+p_9$KZ zFU+-dS3a9TnIF-asc5VmWdR50qV7v{dDd~A+%;kC?_0k;P$2do%XfNbS}vIk5GS>wkh#%gwRFz6N*7Ft@wfxlpNdz+GgfQp1-W~ajhSOS=03tn}=+dHk3 zaj|fe8+?-R)%CR2_SY?a3Fz3_`w09WBEK-xFjH4oUu+pREf*AG?3_g)2x>ql@kWPQ z#kUZnjn$tZbQ-%VKI;U&m*jO-%7*);U&N zPNoixKvF~+6bwBp51yc$V$!}-=L4drH4c@zuJju(!l(Jq*!T>7puN!@EOg{kGv9XH zXO5Qx`FG=8>&Nr+O4Gm3YgM);1%w&+#=J0;ZyJp!eEQXpZ7ou$!~5^t*7FJ*M!uMz zT-ttW)G>ShI*A^3?M*iH9;d0*-~ZF6pP+0bGqY8;=-8`S^05h7ZU6;KXtJoax~{G+ zkiVxZ*DES3fY9gD+*6*SlxyzRF|r(^P?jzh%f*~$X$JiJiHX`use)NK6M&9E^uTH! znuO9zBmV)e>7^YnzctH{gWg5%=8%%{ucB0B*-3A#lwtS<(KLVdomzC}t(bF8y2Ka4 zD>b5Zb5_oT*yYl7b9gHaSb{DJ~!50{l%WQhMjyI~ifh~m-75E1cj_CqI=J&FyZ z68?|}aVg=?zSe~+aM1mE>DOK?H|aTC8i4sZX7!W7OLaQK5`J?H>k^MIuZwY=iKB2_ zFg+6aHu@LdXQhP4@Ek9tUN-6Z`jv=xC^zM2=;!oqu$zlgf3>OZ=_|mX8>h%lV)XSj zQ>QB#OSW$ym}>5kvxC&he?me*`-kOM1XWn7b{(ZZE3E!uskJ5yr5{&#A~On{arPk{ zpsqAdN3K!i8LsKf3#CdJnNnN}vG(<)pc+Ri@jy>c77GX$eH9(!*`LsDdZ^R(8Dyew9PqrFXhlw|dZ6zS+M``o zbE{C+0s*R?678)%7=HCf4vh^^W&p4JrQF5hwl;cEm`Xtn_5SYUA@7`)rEbnwY2Y5A z(l_L%BE^LkGu+1*Z@pY*w4IVSMA?^`_jzbm4J_kjV>JCG0_9W^@wrmAhpt4IA|c@x zHN(_j_4q+q2)p%swYv86{IMFEbu#5#=->v*T!O>g+jk!;6(9m@%M1kEe25#7fs0;R zjG02TuiuU>)W=(mlJ4qP)oQfWh+@L*Z{P8LIQXp>P?4J(2;Ev+VOIPVejAW#s2CG<*Lgd@;)B z+e|_4eFiuXP`XA@GBG`EJvXI;05(h62w@kd+kN=mCa4NcNzs4#688CXduNBDr>7^f z-tYB&fS8Lve@1~4ANP^-4ger3n9=&aI4WxY+wg$RcIJSId>mc#Hgv)LA6g#}XT$alJ0Oz~4yZ{>CC;p0jw zC2@wu1scgLQpKZXOP97cIhEg5GqnpON`M+{|S<{P~`ILBtNV33x{QmGPDW+T)%5|VpvI`KXdtRgfQf~QIP3C zl((xIHa0dcLKmBny)(XBb+#^Zh=~oCCF&~-2z7SRjTyw1g--h9m@q7WFRP?mV)m3- zp)cAAkK%I%kXszSd=5^ZK*+Fc2tqUj3GT3`M$q+B#lko zI)Z-=C+8>{a}(fqBO?MTMu;mP$a(k=oxjaf%jr@VH3_5S^eEYk!|+uk37<)avoBe; zezRVt&%QRece6nzXSS)wrFNdIXR zIY6Uu7Jg!DDxyY1G86~PVwCFD?1k0Ke=?LgEk)1tegQz4E}x9S_qZ^kr{EC?nQzeD ziRpBZB`dk<2|>4k_FpOzc=aDy>;An={d=?iee-v?FM0Ua{!eb$KrP!!7QqDU@!-%y z1OG2}dhl6hnsv2s!AsNs@k9nHGxanhi4x1G^z6Q^NKuhXdsD`-O zgg0YB>c$@+e{3uY)5|W=WKm|SY({)k309PWGucY*$Es8!@7s2)PxH}#Q_yD($#as1!2T^Q4d4+35dYHOEsRGOkko`E-`nL? zZ<4|6r=j@Evu6Bhv-7KC;bneP6>BA=tGIRJuWa1{@k%UTZlrLD%_k?MW$A&NTX?To zHhg+3je3B`r#F8QmKcObE;(BFD<=&+%D~#~eg@j!7g`t^xJ4{SNu1?bt|lw5{eFDs z$(sj5MdP34;T3T_Yk`L<^mdr4l?DG08fEtMs?*il4HeNa(BmZH@%1|UDq&j~R!Ioo z0vDYm=&m7S?xx_CtHVGsCgHWv*YnQ^dwbQ7jI_J5>bW*2^SN#6iKyA%Yh8!Zf$Kow zG{3=HIG2IK#O6uMf>CUD6!{casBx$BTnO*_osfHBuGZ!s7FuwuwYa~gu3SZw8O_i@pdTHTyJAb_O8)c5zo##zmd|f7R?fE+c)`75zm>6y6 za5wAw`bhf1wPcd=R`lTPB1~ILnMPvfFml1TvBmP`ub=h2=Xu?#8M7_Cd!x@C@ue_B z6~xbvx3Bh6p@&LRZ=DOmDp$TcXLpB_DWF@QN{hJMF1JA=C&yTu#l+SIlL@BTz6IRD z1S3WKF6Wk3@@#G5Bi{#so2--}8VQYp;e%4lcZ0Xz<6I~DPWQncehH3bmswS4js}B0@B?r-O_aq+{N$ve{t`8-^{%;cMs!?a@=R_ zwby#%d7tN9f;RnsWn17!F!6*l)R$F@D7q{3Ljngt6=GS%h}Sw-=Ip8SVT6d8-@jY z2_1eTnD-s6m8Xfh+UmKSNcXy$kD>Q!Y8p&!somZb3Vcbx?G87CLVQn+;0dJZ3mB`oU7A6kl z|9<)dcg@i~*&&-l4h}#Gu?{c0n)1XOT5YJPf!FWCJF|mV#R8r_o1}|PxIZ^u5D3}y@N`#WW~O_4W4FMyLjK``DTx^ad60%GP0+QiqPnrd1B~eO;!K_2 zHW!eH+}^IA*m5npZo>8bo`Goe<&-yQb(1TNzq-AWrYrZ;@ZvTzy22B8fP*lMtmsCg zluJ5=dlIxmp{>!|wQZ1+>^n?kYvQ<@Z$V(XcBh(49X3@864%?D&>(XMKux2E{cB-y=nADxbB`|v<{qACcTOm?XM>aaHm7*j~8 z?nw)?d2*VN9=Ub53XBJ@8_DRup<8zj$9!`;rN~jgb^X zBl3z#-zoLo3nSsM>RPi8o#=aj)N1|w-e%IzReRT@M_jN)2>do}|AM%-x;5UM+;6!4 zcwTnUH@bb=axl-gayQd(SjJi6q`zz`)e5!+%Ygi`3daz)R#n)*L;q3IRG}n!m zIXF95NVAf1sN9}0s#H&ywts+EhQ>MI#L{%3L;ZM%a^RwSx`N^TZ^r4rx1?$OqdcZ2 zi#+Wg4pZ4ov9Miin(7Ty)9n< zqT-U#mV0?thvm(=!lUSnwg;pKOfD2e{=;=eTER|#d>7%&7RbC*!$Wgp+H^7G{_iRr zaujB6YjkimQ1Xj^83Y}olZJ#NpV&Djv5m+6y7aGo<1g3Sd)d2l{e7oC^7Y*|l#_)! z6yq3>){pH{ta(3wP?YkNS6>hDB-6!OlZ^vj0)8-b=lZtg4TrhAi3$)vE#6J`sXP=E zHj{{K8J4s=?!V#@sp8i-d4PXTGi`K#ISk2En+X;IF6O@w4b zb@nn9)TSV^US53W3ivp!UqOGOS>F1D&X4#@b>IW!3G^NwH({DCPG|W_JL`R!Xw=-{ zwlih3Dn<(^F|CP5;)mw2qvNErzf1_v$M0Gvfp)Ra;dV$At?OwPh}~f zKPr+@X4X<#uEz=w;qli!{k_m=dK#k`G@0Uuf##2*hJkE>K^((Y7Dv5d3u2jg(s(bs!|x%B--{PD_!1}wb+_c7{&Y0qw=UOR|8tc zh?Aq^_sAUVHBX*h6Kq6M`hR`swQ5L#DGa`;?Te36npki!GGN1?4OlyN1!Fw_+jRS3 zI@l_|ByDhNj&gWuGi#^?(Veh_6Q{D{#LIS5WNU%>*FA6z8#I)mJw7_lx3$O#?0$r3 zg6$<>5mk*x%P~ewdLea9=Fpsr^f`2E_8He^68 zVz8qr@PYr(r{kYanmZ1m+I#(WG|_&PARPV@Sypnb^~T)e;htwysS zM0D4!{H^qwCt4d(u z-6ZfriXO0kGB{~lYKya8kiS|xRGgY-fYu5^be6_7lr+rq-wj&z(ODuxnSL(?)${6o z!zNeqw5~r(mFAVn8$&~nczk`sf4n@IaDHIEyLCW>pKLw%+E;gGO-b7@B*Vzasj;b& zmb*2NTN3ZqcQ@g|2YU?>!uV3LMi-Eb5VV2Rbz45+< zv%P@St_@D!FwS?pF6Y7!`>QxwvYBlP7xu;PkUY&7#gJ4%E~cnxs-1l@ey2Sp8Er)^ z>Du438n2hrxxFED(AN?bg?Wd=Bd6u|Juim%yVmXUampSdyubp0X`h|HRGH$i(MtJ> zL04RqyW*yFiB%v8Q_}9I4~+~Wg$_iROSc<*N10e)&_f0<5AOQ$f*P2GW+8QaLvBf~VLTiu%;s z%!pxsFJPk@{p)bCF7#uEJ+tdnXi;ilE+;RPlPT$Uf3H9SbaQieeP?fNeOpCGJ;k%m zACf2AgOjtaEFPY|VK=Q${snj$XP;a#>&7J_+ty&A*z{5O!rPjU3;t z()gof;v%m4QX-mYl1eeF?Mdf)FiQEu+liUq%2jA|ammSX)}!W~>&kb$my{8GfEJg7 zGvST!OPR`h7xuI%Oh|=Kb7VSkt^X>-LjXuBcTzN(g}mk$A*Wj+b9TfRPL54lS~^M^ zJ$fwJ*cGXLoliuKH5khoXxdeb3>!sOk!|TTTvbik9wd6bajE7!3uAN+8vrA~)F)TU zxYAj-uS+-%c^VivW91^@uI!^v@BstGUY>KZecS$)9rR?9mJDU)9B^CdITl9B9m9I; zGT_zyu(depCXnXQZ(<}QJ#uV5p2nN5=GE<_ARlR( zbFA_t3qdH7P^mzJ3-|OKg+G=7rRZck*yqvTEuKP2iMKB1V;ZcB0&1}FLpr^7M?s1aVrk0h~W3#bwefhK9q>CFHbj^vXiAhk}LPqrOw7X`Ox&hOhIV6~+d2F!f zAM#p`jve#fRSc%JWXrwT-}^YnRVt%F8iZl+j==Wz&y^yw< ze_oqu9S17l+auYki;G=uFY~VIAzR9T$IS$uuQZ=0T!{uo`v~Ek=5^X8+kAbo&(CCb zEpA2gc^kN`2G1K++WYd9S1B*gw7|vML5%63@6rB;mt33OuGV*>e*%htYngW~%S-by zX*NV!R*9sNh|bn2O&7d<)ViNyZ=KEaB9H6xdbjjY*r_!&75*p7+|(Cw&MYnVx(&2? zOJQ%GjEr-<`nBzNh&a?7fBYpKKVxkF4zcw05u9|X3Xvyt4XpsWt{5!*2Txt&gkkOW z#A2V@yhFz|hqvg!zlB6W@?z*?CF8!o?l1O!j?31Et+%Sqgr?Yi&_u!x2@0_^-{&JgN~@@A-nhn{|eB4z6`t%`y? z2UC*U_RJ06UEM!s`{vg{Dm)W}kWD&=X0|opP4;DOn!00xf!!MDd{q192i^TzBi>E;y)kbJSNVshGBNo|0c5?u{AS5JQ z(x*`L-4rA}QmWSbzu#ZDPd)+Q;NLEW=aK*FJBfs(_UiS6f5V7?<9+k~;XN30f9c78 ze*7=Q2&pv_3FW?@fSsS8_}DFc1n-2T6H#1LVX{PYQ}tN?iUyAfr?KuXVtf}Yo)*OJ zMQ-j|yzM=bqV_N?5IFW1>Z-C!u>QT01D&tK?)448{O|4V^DKQx+9AWQk5n8U9~I=G z-aM%Vh9MZm{==={2?<^zmlnGC>*ZDUTIrV{HbD0iV!V$H(9j}$F1NW9U{)Q;Yt}?l zzj1nfz0Pd6{rjV>BXUeZ$DD^B7>}&HJe>adj@-q-;CPk!*7|3ll(VYEPDC1i13#0+#ULZM&sp+b;Dkh7f)qL# zAI{n#HYp+h66^hh=&k%St+)(KK{C;6{?cO9txkk8ds}+>`(*gM4#(2srY}PVz6T=% zlS$0&aO@|vf4H@C^>@rRpYvvMIs)T*fUgwzP_Lu?wifs~33`x>aub+w6>6Fy?J6wt z(;dDR?S$mgzQAB9?%rvs1$}ghc76H4TS%sUbwup9&G$-M6p_aS3N1Bs)MOr4rf32WBz?TbK zRBZtFcoj!jCv0v@D=Ga=n}EzuKkCJfsJwWDugs zRV3)d;JqFFV?Sk5A>9GwjTtK=^<5^oF$8r^J?Ao(_^4f00xWZ->(jMsY91zrQbZEb zHcW#=Dzr}^DF+~DZmZjyzwkYg>f_o5?`*B`(C?A=#h%Y+NEU%=(sqw*^KF_~2b6d( zM3SC^usA(pvxAQ_#LB<&$^H8Fd3L|Y7}Uz;rI|$6ioKc~B z;=}FZ;byc~N9#(jT%edVqrKpLR4)JJR&eRgu6?FQU)kUx2*J=#Z-des>bQj(4Y6a| z^79LBcDr>2fI0j-{MsLbW3~}&hPhl0hq^z)kS`y3SJw0*Ff?lDj$D1OYBLL%cqvYI z%r{=y@$9YJ%XjG~ZcF<|#=pL|E|PH;`vr05F4a(Nf$q_F9@kGvJG*cG*5#`XPrSWwf%Y(QZAC)jh zV%2^Ny_z8^GI%xdiRZ&*^wqK%+otRG&c@8InH5E;eISbDt0~D76O2gZUOm`>AHR^5 zpAr73_Sf_dE-T}WT@@FRAg*lvD5mZ7=(y~!S~$9kQgtXsCf4%*ng8>~JXYi63BahR zO5z)V4y=vGejYkGIr?Qk&!VnjL}^s6rt;=zabZurY3t{WXvxE z6ap}gom|&XY5_^Fs#;ZLYTwgE*6s(H@Eh*op{cnsBMvL(mo?9-i$sulre|oFBj{oI znOJ%!0(yJPBkcczQT%rP14d!ne78vh0^p!<=xnw9z|PwG7*~2FZ4zo%rKgL$BGG?h zKYAFKK2y{qKO+jMB!zRRmHl<{S~{ORIe5w&UQWQB60YvJwMbDNgyz^x`B2aPi)a~A ztutX*NwqycUrTAdzL(iY9%|ZgLIL5#w2y?0Z&k4X_6y$ov{0|*=($e7Ss+q)D`@2Lu3)~2X{M5D&16!nVq!auPfNn`1ZP)%}s53n3&Vdsfy=8#p^o_1oga zLVf{4+&xl9~NOiaw&>Km?aJd8~? z&8udmu0%mnMa(((6|rQ86eCerqZ@!>;)R?fpDwqt=k+4u(_(GTQC{c*zTbKaq=tp8 ztDlCS$yDa}dt)Dv1S?m9wurPcKF+ZC$nW#Mr&I+5O2j3Eg`{$3&KF~}`6Jtf&t3%; zc3C{dxlk)WZ*6}J{0Wy#)&yQWdbM*;Np5o@`9vp1xITBdKA2V(Vv#ArNaS4tl_^)c zgDR;Jy$6;S=0mlShHfFNVD27&VA*m430h_aeV4kV#1-1rqwv)(cbMFp_s9fd>br!R zr`LQeF#YZgXAo693(roPw@mh|TT}Ig-^3+9^^Y8u)Z?$$w2WOxM-Kx=J~r+Mq&)7r zwh>=5-xZ_=uGU+w{3py4!^l*)c2o<}ES-ZA2AZ+UK;<|MZRI&V;{NOLY_O1kL7Jd* zwlMhG^R|B`2?U5BMeUMJFR-;>q$~cJ-OD}4Tv$Zz#Jh9!TR1~cngVchl$2$>vy2g8 zX50|%mP8$#<)Xc8>x7q@L2$kh{Fcz&#D863qr8=C6jW69XWuGsxDFeyyWWtvxs=b$mi zOMrSk{9A2y<^_Q59%pE_@CNrr0iZJ=Cr(A>VSw;kjK2ZGg~z=hsKw2Q`_Ytj)BO__ z3fVl|6&?6E(BF%8dUE9CniI}Pp2F~kNmlwB&fn5o&B6j}IoSBvep|BEzjFcdI(d0! z9eMUn5{{+l3)Y^m|K4)*HtZ`u?f?4c7^g2&m6o}iMOtbLY>jA-CKajF-u{EF9dZeW zj$|Xo5AY+~bSI}X4U|hi5lS$&+h&~)x6Q~Iz*+_7s(A8^02oolXm$O-1E5TXjzmx~ zA_5XVPx8E4cyCO^o)aeDJA5b3lsF$wDd4j7)x=$ge9_ZWqx-J-)AV!b`7~3myW)Wr z`sF|Utt@gIVb#pm#J|u>ZwW%t;qBA6b(udKcYYs@5u@`lPz`Iv1!<}qJNxLr^=pfi zf#4))^SEDo{W<2|8Pp+7@O;~1re~HoV3H84x{eo*`8Wy#&)ATWed&rD(h#egJ+_VS z#4C)S;ONahH>kqS!X29w7lbitR>g596BLoND=|8@5U1fVv!-~kPgYc7IbX|ZJV?rN zK>t6ZM&pY!W0NCQG{)4zzb~t(Q`|i3=cbWglJEtv^bd8P1X3pN_l)*h5Qd8fFlP|b z#}$OQ2LJ>*bFP*HaSjxaNwN4Z4TiXXSi7&ZQz~QT5z&aP&t9Rgp(o01*HaPV(LYVG z-!)WJQE`j67GJXamKIX$Jr`2h$Q53YuK#3v(txHPz&m8@$xn6FSgOFU)OrYh!iig;-9P z&%?qPtwC&4lZ2#r)iXN$17!wmS`lMs?Yrg3m@I_I9A_?7m!fW`78GhnxbI-^ZhwVC z`|Rd-4mqPW2>{E+?D&V5YD&tlKpwJF-xc|~g(PKiWSiY$(ZSMF6<{cfw{2jVKJn5~ z`e+6`D#XX$SeYbc!0Wm^2$hQwK#jb7Jh%zkJxyApb6jQb$R{OR6J#I+%x4Tyke{wD zhYKfvLr>BNTxrm{4YLw@X=uUHdKISv0emJYnfVGqoDLR8Wp)kVsiisNH089ET#Owg zo1Is8oMFM^m{3efQBYI7M;- zlNhyx&$i3VYyB!2{6k4Q1iI(^K+^5Fa61t|LHUb-h;9wBf1*`lleVapg&}({`A>Go zi{>r-=?0mGTC5GlgRV4>>!b#P=SZ$EB%-M#cFsIjF;S&X(NOEZL2XvPJOY?i-L$+wG7QAj^Izc(96saZ-Y^@Gvy{Lh7c!+meY8|*(&%SD*uIU{e0#my zfI)I8fyujYyL89M-FkSWJ-5k+TlQvep;?iCB?Gxep} zfaB&buJ$w(k9p#hCG5y}dBWGua6_d(o z3}YbKje&5yl|m9{WUiFOsBsFnp}jN$X?1Eth=?geM;Fa9muD28lY-LuV%LHMi2)aM zmx6aTek;Rx#y58r{shSmDp}Yv%A9`q<{*xiCKvgT$c0xR1RxdwEa= z8ybJj@~-H!G3v_n#2#MD;(R3Hc)B))vDp0+7u6V_W{2||qyueGKRIO9g&u;lFLHZ5 z)`i+u!}+b%Ea3qG+KN?v-y6Icur)~37wL6ZsLOsZA}!MZkfwDW8R}#A(L53E#SziO zTN{G9D@2r#wM(z3?MaGN&ns!w*?DE9MM2Ao8uKRV#H-}zLk!_hFeOr+YeS12Iy}?O z`=>x^ooDxNHvi~csp%f9mSIqW78L%8odw2tm_itX?MmCqm*xHuXSd66bVZm4$rTHr zN&VJN(#FTNl&hF}9naP91;>zC0z*}M+U=|)!`29+d$XN+sit0&I!=n1rt#6HN88T+ zjOsKXNC5rB*g0zWD?F!U?QdO^A$gYO=KEWFk)N`W$pzC7S5`>zG4ve5FDiC+lEHP~ zAFTP&1EkNL`MwH;>E_D9Y+hC~g6VHlG=y=->{qIoKIjB5paE`rOyscXM-HW3%AUeZfxz)Ln5<#yE&uZG*dq zhXHcTCn{&>GsXeNw{Lr@t7{xms6Su;Wt*g=*7nMZJszMJA=%k($6468#85+s{fD=o z_AF<|6QZeLugOLt7ISrAPqFln4j2F|mzW|9V}sb(67PwM?F#8KA0}XQ4LwL>yb(o) z{iG7!z|unxV&iAm($T@h#`eA#Iy<0{cz5%MIzYJGeD!J6C!;sI_3V3pI;B7xpA=4O zf75%gj>T5meoQd|b`ya15_bn*m1s)CajV?M%alGajY@p&qkE~BTkctox)c9w+$OaF zt68EFtdKzIXSfbDI86V|uPCCu-I%wUkF_r*?jC{vpnPdPE9$d+BrNzhRYi zT9Wn_sg?hq2Z$fm9rCeFyyG83F58BiT0BnlpeZv4(fjD&83R>@+!g^YI6B0Dvf}9M zD5*S5nufkKAD@^Qe5-@K2(UBf|cj2$()j$o^2>&{e!Z|xV-G3xc{t-roz^VBGyBORw&g`mQGb7lEmaf zeCP3cDcDu3G(EI_frn}a9hobPhIDZ%hi?sXF4b7g)H))u$YXctPqE64=(KMVdy_?8 zVmm^M6V08Y3w#11MNWR|pY^b3Df%lpJEH6}rvAqZR1qIcL}+NT{(KA@-^we#v*ITV zEt{#-^^ffO8uZ}i<|ZN{0uKjA^F_eDPP_^$c&&d1)z!6TEt?9pBUw{p(8S1Q@u&Qo zR|;s)pECt9qk)bL3NR(r5+y*3_t>3eq=*{0cD5u+Nfr|qS7p$Jpm9)Av64y1$XLtC z4R6R08CUeczdj^{fdBWC>ZzoxEpz&itE+2zv#FE~QoLl&lLY1Vwl+v{9t|}rTE}|p#!c^wQ$7ADtEKYJ|yaXd&e;YF$ zeC65H>r($Tdp}OI_{lt66R)ww(x)Hj%#~qo8aY zxph!d##;enxt#lkgA0mV9R_3P_5Mk}1bUQO_a!UsKIf(N%qH|7e$M~ zgC}Q`Jw4m=S0{0Cs?jkq`O$xK^npMXnVkNMp0cEahKI*`T!FEDIXXJ}f`EV%;R5(& zJImhwepVLXIH;(ofc^;kB>PGa`Atz@@07UMy23Yv+%90Hj`9>5-jG zx?PJFbYMHA^XHc~o}PDw3WehCo|}{ZaN2L866QHOnHH)BHC9YvO+T5^pyR~akjP++ zf&*3j_zk-Kr20P7M@Y4z083!u!VSq)*v*YVvUJMN5${ z`eAbNYmUdGJCVun9XNPkY3`e-$)c>w+-b3?PS>YNQan7nlO-InyH52{QS!vy;FKk^ zH{-jfT3P&K17^SCAbDWirxzDX8miwqA%Rm~g1StqYWR-=I`ktV!j@(;Fol6~)u4Q= z?ga0bL3tiPAQPrSU-+R#gkn5i+4S_T2XggA9i0gd4&;p6FAwlcO;nVDCyh#Q)9n+^hPQ9PhNT|Lv|x~>+mf|;&Y3ywT!i)VoYI3-}15?2z(ri^T?*)i+Ep3(wae81irv^&cnwu=*qB+ zt6i&P$HaGmSb{r%$V1OWt80L|6+bXj`^ts?x>JRq>X(Npc{qX|nU7Svkdj*R zA^9); zOp-9p3the)+p+*FYwOhnMoV++{k^?RNi+83rY3FQyU(A8YOs*=(=#&2c@fvY9V{(V zR;n}rgs^R2&lwqFn5g56xTQc33c*NDO#HEUes&gv%WpOkjsFJ7Jt?B{%ku5a+@@=_ zGUR|BKQ1Z?)OeHdffP@PmmD`FfiXl%Mw^5G9YvfZT3v~bZ&i|ClK>QAYi`Z4r&~JZ z>g2)`(mDRx#aFjQ0meRFT~kB!tV56G;iE@{L@5OYh1Q}J7W6NIp{dt5w$nVd9O=G} z%W587UgE5Gpul|xze5z%RT+9r{GPrfxb?*+BrpCDzs@Ra0OUj^}LNolt9a;g>s^lk8$a9QsoO~jpQB3@3 zA2`wIF@>+7G2DUHx&|>j^wOu*vjlX{UDl+1FXh}zwMs^4-8HR|f9kAIyXghL84qqy z>d%>sJs?(X&}1WfjtyL^wJwFmPQNo2i!)U)2BET=*hU2WzM`XvwE6K!;s;auack4h0*`E2O?SqJ4e0+RnW+vF&latkzrfhboFLdi;3$Qda z0k*`+RfCF(it(5nYXbhe{5WW8HjzzauzPE(}~U zGC?4Le; zx^o7s_bqjXlnI|PRz`L8`Tfs%p%4}fG*|WSSTRcDTLQ$9sVP-uWn+oMg~RZFE8Gsb zzB-XEp4{^^^fh)~2(79a6Qz>4J-HzR;8s_F#@EXo-Jn>yIEmG{^kPVK)0BKUzYXEUQ{554G|6FpYcBXuqTS~@y93oQx~BBHFt?;kTklo*$g zpxdnV4dZPEW=@GDtB%iGR(oShOHCagK?w>~yoAI=GYgoJg>ybQ0Ok0*ga2@Kv1Xmt z0cbt==T9^o?!}Zqgh!;Rq-7=X$+1hXb=`r>W_oyZbart>Uf%r)dGrw8eH+=QPvu^G zSmhSfME&sPi$-yEb+t-{((?cn5FQ6sWhF4{>L@9t#>J7~XZ6|JdJ8XJ1_fJw;5YKyAbg#o96YRrnV z{NYP)HMQyJ$VlhiNtG#}L!7i2Cv-!_s`bVXT;aYwYkApn{D&TAly>(oJPU%$OYRFS}uB$%(-V4g@ybFwrEGVr4ztvlR=qC<-<&k{KE1*6@Te`;v zot7q0#9$Llt5M9nt+>37rOr`F_hek<_z<0M@?M^e<%D+#ishVV6kqk;B*-u^1ZUL* zs96*5ORtU{tVYZR=u#-CeVj=-8Ou3f?F-;`Z**VmW$LJ8Aes9k34 z8hU!~_4Hb;&`d%ebd=@j>q~XC=i2_+S}wER#a+cJS7S&sK7SX|nVE)T_4ij=ci56- z&M0`x>nKN+qWHMDgt)kpO`K`e5CAc^efu67`aLw&dM+Tlu6VUWwk1R7(9=*zc#G?> zQLKrClz`9S4OjR1Mz*nNKaeGB8RV)0|I7=ag;{2ej* z$(nqlgF0K=M1Xwxpm`Bg!>ens2?_wkYH3dBx8AVGLxhW!j_D`3j`wB_OrkF+P<)p# zMP1rtZIpVBt5}}}A35WHC~%huRsiKI&}b_wO%#k08W(M^J$gFoB3WeVKD&fFRJMMH#UGq zOof5E(=RJTqw7!!kSA<{>LjShl9DUQ-4oufraR`C*Kv}5&Q}?pyrhNro-M4X>Uso< zWu3i5CE-sd#lsm~uK>hMlli*ev&lWFS41wRu7^j8oAvdcJj5U3GkeEjYRPujej=JXTxM#<2X4dY)f4f_n2UD25ZI-wNFx1E%(W!++?W@ZH!D zB0+j1%?sjG8+@JRiI3G~~Zrs8=xtYotA<0y!hTnl&sMq{BWv7Ln3-@+YwJ-L5q0L z8dDh5MVCk~Sp{hf#_C_`+Lrbf%=S^Q1G3*wBHFp2z{l4fp0EAC_mu$l$p>HEQb14p z3inRYm<2xiRW+4d>n2W|vJYUUycbFb0O+lt4&V|JsPl|qW)PzjkpswlZgi61)$$GC zC3-dTL(=Dz+wi`k#EC?{`yya$+9Pt3}8PO$D$2qFCVAkJ) zk)qCj=6Hz99tdq-10q z$o_SCC(3r~+jHFhq$7MNpG;w~AOEj+V4$HTA^`OTE;cJG4|U7TA&Mw5rXj?{d9M02bN#*0_9ls zO`K4!F@LSKh=DM}1jF#q&B79bx}}W4c)eo#Min+Ll*-*i$EpvwD@qdoowlPWt042g zYCBZBPn!7W^ECieyS8pmjib!RscZc&b%$h}>;Id!s75;NvR*c(J7qGNwLJYpq4)|NTXgkNYHc^@(0E zcF-fK_mAxG03iA~XMRsTMzTpk#DF_6()?V?J0?XT8+qE&%Y zPqDVo_%7zPI+c6^FfzM@s7Fzv!pdMd-ol($8>P2KX0WN4#~-AB(*UdW#4C*)z@q9e ziaASU>H-GM%`DK+PqQj?C8a-3e<6c_sBj=2iPHa`SlaYbFh&N_1aBWx%H&STUbo+e z3IH8tMG@P15f~x%Mh^orZsM1xNveSavQm9 zgsW}@R)NKq9%qk4&)SLWb9@*-vTH)@R$_l>YWh!qezc2yhZp|ivdBAVB;b={w zph6?@5i#wx(aM<3gr@9WcHLPfUpXPMbY<)BJ}Q!(4C~|Yc#*usRW$w6`GGL6mlV?S z!RI6#r1{5BNlsi%%JDDJB0kplFjp|}9+!9MAG4{k*b&#*1?bDV;?cy6j4lH5U8YY_ zz}LVH6(-XS&vCHFrWsT|*wJhlNRj>Z=-qiM~WY#`odv-jeahS2y#JWWwc5 zpyTNv$9t)VL~ zs@Nzc^R2T{&ivg~-%xYsWO80H{af6{=IG++dqgmNr=i0#Isw!*!zHpeD^9qHIjbsO z`9W%M)bU=$ow$SaZg<0>Ct1AUQd4d53}&pP1K^oOXCADEW?Us>zO1GBnFaBk1_6(+1+JGTfzyTi$+oDMGbFeD05q-yWfh^DUp6Nv^_ar6 zko1;V-Jb^Pr<5;kn)*y%9?()SdAru`*KGu6NbGxcJ2Ytd_bWH6L?RyAsoq1OGFfAh zE!f_2xn%K4@tWmPE4=LClsmHc;pD~WZ`rAWzRWBkZhRhG26ui8ta5fDksbPoyzjef zd=iF@Z2%+$d2E#-l5~mExh~s25nl@klIZAmH zAWC|OsYr-&_(5KxpL!njEO1WNIOo0jZ$$_i9S^l51R4hhl)YF>S=jhwTQ^1h1FUpv zddq%|V>h^x!ddspbP;^+qNx%Otwu#b^&AY~R!Qs}Yy(*L>wOYBkgN8w z2vVUq_v?94rXXk{MuWZebB_9hHNATwglrnmU1!ZRf%#N2VC(vPN#0rgGxv_p$KRXV zG7iy6Oy3b4-W|>v&<43KO}p|7oB+}%FLbbDVf`#!NNNFLnN}Er9n$0EoOE?$qQ`1D zb_6h!0m_2Ww6{6u&8g@E`pY9FaHMCP_r}7eYkX&Z6bWY z(^!bY|H;eFl92$>`~n;aE{XZsH;o4`x}!Mo94o$Zq#x3x($k{)UcJ z7704!f?6=CpAmU+c+p(Qvy=$ z4L&==WEk0Q*I~5TY`3xkbq2+!I2GBg!wj#)Ktn6uK@&Yr;`*9z`}!TJhu@IYwEiJ# zuHb`POltiGr!69V_WT-zhK(hK)EcTMMg#77e|;8x0F3^>u8@ZS{rT7R|AT0Mf2;rN zFYpr$f@E1%{qhnAV+BRWAaj(jkh}rcS4i{sRqhb+OfF4L)MK2nvbOc47ZdSx;R#-T;-run@PE!dHE+#*}81Ky?jdTsmH8mwu%v z=OjB}sO?@vla=t&9MT zK#P`^&$Ea=BbK5|FPa$pXBXz=OhJ?wf9>5K{}#vvY<4_b#G${HLst#BS*t7xUGZzoXe7Vxm-@e_!?xT@+XqP_m>uxG*mVe!)#~4BfYI_my3pKUS9j&-42p=s&xgV z)+Qon#OZa_aP0zs_3##cM1qC^MnLiKA9}eo`2!#1jIIq85ea$o9IcXkm@Q}QI9K&I zDDl0L;Z+u_;qT*V;H_vWy{6zlycXM7 zQr@XyrmUoaNec9o6v&x5#ccNO#1lIIP14`i#mDf5j?W8z>e=G+kJwtWx3KwcZ96?~#T(pBQ9cZ&s-LUbT`Bl*_dUDP0H0;)nF;c~QJEaQC(gu1}LCY>Z;2n#eJ zfS3cQ!IzqEcqep9r|xzl$S!Z(_Gg+pFy4a|#dXJX+&87;;Os(35(D~EjZBZ0P~Pj@ z05)=VY?qacSSd{heTf8Xttrnq|5H@4YIAi%duL^R_z~IrY(d{&PRr>MyMgSr#5?dA z1D%!WTBX*_PSGG+wV3`NL-0H=z{~#~%titg0KpEh^gvFIoB>uvp2gDjL^_7>-N{BD zK+0iaSsY4;Kb?69@5Xz;W>LtTBa=-rCT#;#OE!eHxatt#{-rfP5R6Evcqhp5 z76e9@tx+*#aC2YqyP$L*k*bVL#Q!c)*_i^el=IRh&A7xemkCM#VW7@-(Y8>~z0-5n zUOkZ&XkWDlC)1q>G`PNc73X?!u1RzT4AE!Nhxb-MtqjP-b_i1>qJOazWjB8BUf-K7 zzv??gmyzkY2dX_2;$I6tL|_Pmj`twoUv=OCVjnB&Wv;592mlFN=6%? zd=~&E{`0<)aj>Q7C(b}hf}HVvhZpe7GveO!k~hq7PY~{xpH>2e0WrxQTGDyFGCkRJ zg^hz`R8rgtFbIu^oe)&srvFxr5fIRTIB$)(XQoZ%)_x-aF&)I%1gKs1XR7?_@vgz> zoH6+XfWR=J4^~ugbJ=%zzPE@;Muf9#WNhHAVb_em0iT`@TY&{tk9|` zKxrjF>WcisEfA>ytp?Z>btxeyKsv%o5c<8KzQqSVQ7OBZ%Os#?FcmI?9ivF*dsI5G ztBCOCe*z@?TQ?oFy@eL4wA6sP0s`>#mc*&)`Sa_c^c-!e)i7`0%U|Cww>Wy}Ep}4S z5s6F{xFC~kskXKCG#vtPn$G+UchEpZ6=pLWt_ZM|0BW3@v^f!Nt_)5g?*jt>kq!Bt zQ-vD>*1zvn3$w1z?OP$Tp)YMKzGS@3?%ozZ(p}9(ogp7sf>9zGl!ID_x-GjmssNW5MAD$Y0`L(@5z_;r@Y&`jo+s4M1p!&X3T$$! zpMNgShel%56Q0LEzPa1$StwyL2zGi%?zDCZh=x-_XIq0i^Q zBmYak2cYRPAs~$)+S=5XS0=(EDUqWC>FB<-=(=(hSVtCBxy|Ly0HdhErTu^ru{STd zF8@5|xcg@)*lVApzvWnfWOoU%pnLqHPeFwVfU}{X%|IVBe}F!|@EZz-%SzwdyxeB< z!3B`P#wn{1b>)Br3oOFh+a4iR>aK=wP;^omY(Cz`YFlRKivN%^M1{%yOSRE<+K{|z zNC}|0*f_=aVe8;NXQyb5&4xhF3X~A0wA}#u+|WoRUm_ErAPCe>`-@5z+NjLbB)hQ3PFx;TrD4PLa z1;o(OPsff^`T-q3-->|zBX#TB;G=Qn|Hax{KvmVf{i0h%N+gvI=~7xcbkilF(%mWD z-O^IhAPv&p-QC^Y-F+wi&-a}(#yxkOJMOsKFB*%DL@ziDQW_E$J53C)kyEF38blHq8BJgJjJ)q@H@3hS_8yr!HH#KDZjb3 zm$7mOFear=gxSN6l&|TzWB5B@#7)<<#~^<5me6s@AWk-;MpkuDvy{+P%ULV|q9ySWK3 zoo$l}E3+!IYL(JJJ^-sfGcRjuQCVtM<>Kt<{7OhQSd0VR2bHy0bYT2>8mZ4wB#*hXt1( zQ(%;IuZ;A_UxL7k4~asvMHJW8dbT#I+5qVXBi)(sQjbqd90u+xE)wR-5l?N_-vo*0 z6f|+6{fL(kr(#N5A_Pu8CUDU{gJ?9bm`^1g6o6n1*&K+xt1B<4Ud%N zD1^yFS0uzEm#rDq99RnRbGp~BIn7z}YYK|@#3Th2fIlrX%3WPi70g&0hr8rUr#~3K zu=cWF3&8U{j*;3{u+53}=#^$;QKVVwVOB9se+ccocY8c%j$nQM%&cQx$v_t>p(l5c zahE#ut0{BhBAM^fJH<#TVNzWidjzk(^3LPaXeXJdfL3YS5BBn0yoEKWMn|sC*VgSZ zS9AQwYbz<1@-nbny*FPuTMq|a)SNrkR?~FW-vF^T*kun~VG@^FNLtz(NdHs$hVRYw zKDiu>GAq}K7@fGu>Csh#L2;bx=)gAJFOq&W9i(_c#9qqJTAdY_Mx9Mk$R^)P&u{*4*7} zVSQv{B4q0)Fu$a>M1x&8sOJQH{1(F8dFEAPB!tABvWU@*Q~sn>NA+ql|F^3HM@G~k zioEvx^89!2+cAQ_V<^zWGbYiz>E zf{jC9cDNVI+hR!gx>NE*88>9tI+IdC=MA4|c(gbTnDD&ifVq&3of{nt&@L}!^_kkN z{l`+f#be2Y%hjf2ycy+_h1rg&jw$zPjZ!uk=sT{IbkT@R!?+5=Q`4H0-O6iL(@_dT zSYpF2Ei&)duqMBQFPof}>!VE+m9uIWiQrnMK<`PRj)yw+J7ONtENJr!(}?ik-yAa| zLw}X=sup#NPWPhX)Mwqqh}0Rz4wal zCW9xQy?;-an|^qtHUC}YkDG$XyIV7Fo^ensbDM+x7sS2$J@@;V5~Xv;*1cGnik9k9 zy^AxWgzsXT@%eeV=$Nly?iw2#H;sh8D7K%McFXfV+{M!xDte({;}OVwoqQJ|27LM; zqeM?=oQ+l4kdsp@eU$696cwt8Vj~UX_DSU;5j{aVDvfcmw}qj+U@=FZEvmNK z_1&1Lu;aVqroB|s6uoY^_d_A%n4ZqpyR@;gp`**EqN2;q{)^+hb|rX9Mf+Vvsn}>5 zU3ocemX)=<2<|=j7}s~ZhXr~Y2Xi%HlDVmAnbGoc$?OpzTJCsS-fxDh#y2RIKb$6m z`y)?Q-ijmtS8)Xq4Hc8KtlNuZPP^jlvK1|{8$RBvvDW@}R!3WJ71abrB(8YpjMTrr z^|?QO3}6PXo18?Q{56_w8P4A)czoDutey-wL3|2a>;1b^lc>x_;!T&K*KoR!6nVBc zbJLM<2MvEWl{ENxH@M*DcD7g&u)BAFRhV8|&FSX@gM9slcx-6wr19};aQN${(GG6O zDWZg4X>RGLh?op8>Cb1!vFQkG3Zi@w%ZEmKFeh&_#EsT8ttb*c6MTY*f@0v+Oq(~{!E-3 z8Wha$lZD)FXJgfASpzu16qHebeTzB{^zF(c$?~%hVPfU<+z;8^-QQlxvy`O~NcA9) z6gEA+<3Oz5)6ueY)T4{;eT{H(J3i>Tw~_~icW`p3d+0g!3rU)}+}8vp3m(Dskni{+ zJ||~*EAQ=9t7vqAnrJU)Sj_vVpCA>BEl;iE%ii9`&fTnfo&D8S?EPZ026jj9=zaUV z)QplGTY+MM+~vXj$)DQz-rYiOzNI3qu%XUg{@l^QpH-%P`*?-*SFOiw_lrc8O8E`E zNsac~!WF}fr+R~X<7XM^y5y=2xF20+cGkteP*9-YoJMd}xZd6u1HRUByM-Ni%qVS5 zfGoJqp8Wo-I#AZ%{$MZGSpGeMCE}K5cI3&v@==wfD>`E&9l0t}n_ z%mcqwfA|zOxBC(a8i$AJ&I=;mhwI5jYMl`RWwxSvJVJy1{QVbVKMwDB1+p%Qv-Uh4 z>N4MilfgJQh)|Ki8!SH8V4oTt<9EL_JE0 zz^jS~qini=1t&iRB9_?&;3$59oAL2-08Z+_nTn3p5oVkSiSKbgnXOQ@T4#A)|2dxQ zNJg$Va0e8tx-r%)ABjV=0lPrKfW!>Fdj|O$(yW1BBnJn91bxwu25_~(?Qa|YFJ9aq!(fi1 z+1wnmvLNmjRq8A=dl8tvuT$})2fk37JmZ$~A67eS4=T}8mi~x?a zH=0hP*D&tk{;>fMee1C;kbhHt$&SMYDO2_;HymlKPmS$;e;)gkqpk}IFnIOSQ~cxAWi6D#?Fp6zU7rrlTm7 z17}8MK+7VvHzm6`1t6#EgCsSO4hj~C8ur|&2-eCn0#3)jnH3(LoBc^R>jxVr{V8dO z59gA|q9ZIWU_#EDdtTp+y#@-AiPr-(x{vW`+oP^Uo8obq>W&AZvEQ^Fb;yF0r$`VeO6jVyxK2WOX zsn-KHqZA1msdodXIoYPt@Co^;i%T6RXYHVBm8_h_Jr@1_&v)<<-FFv_yij;kwMtpy zh~MAU9=A#hr&m|m8%SK2!Bja>_(i%)*JSxRka{%B2Wh}nSU;we)LTODboS7d>w~O)S&Pub8__qX&1`FM#Sgd(!6lj4{JzyOX^7XqIbXCk6P*aF1G62h# z!(>1spDpJH=1E{-JeOC?Ha569WLvCe&jlPk8Z>8n8qSrynnrV&A5ZSw@EpuG59h<1 zAPmZf_;7PQHx>>#SpgNR)^HvXq=Y`m{;kQH>|dYAIfq>Z(Xq*M#owux4yP+0y?Abs zghmHUfoeRVf!F?Q)CJDsPKYClMe*cXwg2X%1?*09TII%Yn$vi1QTip%=s6(J^zWb}D9n za$=12wXE39&3NxrQ=F0#k%Phg(pMO`8xWxGj;e&#c+?Wb5;0w8H7kVQshaRTT9z46 zya!YZ=di7podYJbWqP0j*;CVOer8s8{zXj5_Aa&h#X(lH2DjzOUci-4{A9ikt+24q zX>UIsU1>>g6DUZW$z0Nmj|R9$%yAiUGRK0Rg;&G#%^GdgfI zedA}&u98fiOlOlROVch6ofg9?+v?>CV&?G~RrO)lX@&$bUr9gH*c-wW*hMqd!A2zZ zUM6d~`iV-6^f1))Mpns!2UP0)u8QD>JE)PDb~jTHS!(2aFjFx9WMaLx7enRw4pyF) zx-jymUa_Ot~^&HctQR%o`JfXst_8lDIDSD<^oGvO003f^pf#>S-JBN2c) za&|6Cmb&}VhO6R-MydMj`Yig=ON(NhX@^FqU81nj){TJxfF_iEzug#;k+Pdql`^T^ z=c1s8i;?Y<+fWfSEA80Z`Xn?cFuZLugBh%A_$N~Q6FO28yXWCLBZCb#oOy=`zV$TY2uUVL^n{3=|6sir-zwf4{ za&Q}LSQp2dnn{?<58|_W7-=T@@?)_iglBju`h~mZ8Fo(TdYl^R`qrn8Pl+?uWeW8q z3N%b%x?tgw(waBGO*G zTyl-UL|0Ie8ygWH`0#J-S6QeD#%XK<*yck+?kDGx7 z!h*eYoktsM+t@xm*6wIgaw{!8GW8nA186{eAjWKS)nl{pz+h)Y@A@~n$(b)?(V}9s zf4!eSEZuG&uC}jQ-l9x=jFSMns(nt<*&4#7&4{GLMZWF+zG^+EywPnNuz8Nb8RV(A zn(w)yl`l1k*Aw{SWN{gR!*Tt1zu|O2f#S*mslp%?O#X0~h(ArA+xaOme+c^nih}SD z2;sYpo4 zJoWYU<=+wNBIw?-#QMy%QL;&KNu-$m0wkLMy~{29slK+lK0G}Rrr_z;e%UG8sDP+Bi0jg}iMvP&--AZO?dHn>@qO_Z`!Ir766bds(rXZmsER(=<~{Ju|7$1Y;kpuOl4zOx!H&G$a!5 zmeeH&K|MV_HrG?I-Z~H=5Z;y$gJVjNi1n4p^5oS^=Y@+8yCWqh8*P2&wl&4yP@;RO zKNPN5R)g$Eg3QhnhLjR!B_9wRd~L62X#C@|g}J+%wa(R%KBcJE7ZYp!${SfDAxF8S znk;?6mX+7J7$%HA1zS2cFw=(5u1J-q^p+ps4&7ucT*VwKpcLh={RU{MGO3Kd>;bw0 zV8kE?N)DvCP~(v6ekc4fFv#05|Jw?=Dj5h034aveOkYPA>H(zYeH@=K4Vv*y*{c;T zG$@!2CXILG+7=Zwa_ZA)xeCn%@1S(iUZ^8GXVG7oH#*yC)|g_yF_CI1Svag`LFoR#cg%gz5xqYglw3`mlrP!AVlJ%G?U)`a%|r3}jS_)+6YMbLr@ z{xAknejb4Y6$&8r$ISd{fE^Z^m20!S+e;>3jbRv1qfJR|=FLD4H4x-GPkTw4!0K=W%Nj7>o|-yu@#P2F1Ri48QRN4k z6IRgZs%4sc-dUk&8<<!Lzv}zCHhXWZPLSP=(NWE8wM= zC-Eii;2{~yUDQm>j`8_YQj8;x3rcDX9;qt=?*#Zq$g<96|A{ehs1a~v&0ObFqdMAaP-VhNCz*Bp&3 zkfqO>nOi5wwOo9+v<_hhP#<>zB%|%pH5S8KYYfMWya41?d^5CY@5e$2w)m56QY@$e zF18=3f56d46DlvkZtqO*E9w<9@ZNWX7@rtc6Y~ZCxu(kauTK2;$9vNi6}A_Q$(^Ui z=dm~h{j2j991+D@47i2q={Gk(;t&QT2Y04`Z(L(c@qmLGtOvDI;=4%3ba{! zORFbWzkX9bl1=tzs&8JSY|;IH02VjPUPAF zc(9WXis&G$b^8@;j`7a>%kRI+x9?4ii~O|?_%ED7ah>{}PFDp$AT%Pey-8_(4ZIoJ z-od6F%moLOyxZ+B4>R!o<-#m+^wQJa9dc1z8T!BJ$>9#;p1aeBY9uh^KuQ}tk^G-pIO2$q{ncq+}Q7`JWhC}v`*kbjL@&i z5ow}D&SkvcFq;uc((bKqEg~5=)GRZ%Hn>`nGcanJz%I>hid+4dTirst@Y-5&uu2WU zKKB2vFt%`$226<-+i&ijT_{U1tbPzCPuii-S--9a8r<@Qr1E^SYzitO3=fZyx3r}t z)0ba})pccg@5$JyC@6eWv+wTi;%1a0y5EP1kYq}sAR%onhTe><)55DFAi*mXY4Ad` z%gaBq(W~blP{5Cjwvw{43Q@fw@di!0X)E-nQw|%1;roN*frH=bXvhxXSiHWj4w!_$ zByrZ;52uEYj1&|M2-_Wy!KYSdkn{1W=gXbiJ4>O2Zp98if7qgb>XL))yP##)YibzZ ztlFZ8K~cER`rQ^6&r!01cWi)Uz}MVoz~qAw^Ah|^u46WD+K@LkC?dQZFb(9;X40b` zy0l^FEw#h3hb*#3R0R$ywAWMTK`g1mApd}==cP_$?GS$tk6d2EL|!xkJm1SgQSE~< zSstNV^cfkI|2~N^(jH+@--&m&LIy7MOvY&Hj>_bLNk3=>^hSfDexZhlP?HFz@#ajM zu*OL|Zw}t3QdU+z=bXQDu*#6d5tN~nO3b7Z*AYlpcyDBECM7NgLggNsP5nJCo+U$(ydU!lT1CTW)Xb^T?yh=Gys7#3&D3`A?Jv|+p7=hPl zO&!`9B|_#!3c~UcU%&Pb2xzmYOD$nyW~OB#(ULRC8#Z|Vo`%!uVvg#@5OffRkZLl_ zCSs}`GR5Vey{ETu55W4favqzPQ||JbN6&i*>gMmC8u~kX+Asrbyk=7)OY7V9d(4dg z2A`vg*|!w~?^OsC{%r*=t7P=xF=linp39nw8+lpz8-2RJ@XN2=n&mq1~CatvgwOp@bEwrB?k3JgEGaAwYU37rZY1!x3;$Gw|5lzAs$`w+1?50>FGhCb%B9_KB>S3 z@P)?XlRK)`i!FN40axhT8>ej0wsE>t&&SudD1L5!zVY_KTu^tttfpj-afSr1WfTRey+z!C~I(7tM-<`Ynv``RH0 z|CVLf^sANbM}y95BM2nz2|Y~n(Tq`spIB%zs738~=jnGW08cCsi(^AN`n?x>ylX5F zo0jX5nFL;9$=9D}pGQjB>9(1Fu{e|*-2w{bj$a}1PWVRi(4W*H+5aJlFsVWko|uoX zG1qXrzyAZxc#~+>EEgwL03tGCS=16c;nJbHv;uoeHfR_)1qb3W-UzOPF3D>fQx&q2 z-{K=w_Zjs;3S>rMeOOvLS`*Ha6zU@J5p&XM3+}eZTGH%K}oc6zgv;0ke z0?j^7(Py(1##@TZA}{w>5f(tG{xm6`F{He?46XQ`NzxNk4MP&{XApPrSAXXeNBc0i zxOs0!nB5m4s!AM&WS>`hXg(CyT=EAynDE~WE)-rk7rU8X zFD^cw&7<()O@8H4c{Cj)fSe8`e-{}hV`q(c8JL8I5()u2OQw`n| zSeRg>^rQJzu^W8D$O+4{vROMv3W4OaBrf>*;Nj`}&oC$=ffhGn_n7Fm^!0I{1txn#J;%UM6 znCMmWrD2m}AM0+D2OrKla+$VKzy6WZNvl=E>ugZCgi@i3kZC2jC!c2_q9FJ+G3@@) ztZ6M8kx+}ED>U3v7p*xP`_+3o@mJD`X~tCQHRjDpe~=x-_p{H3o0i`x%67)%nM&n` zU6QR$jD24bSN$Q_+W$AI&0y26JoC4llwD=9|L0M`XnjVo+gV?A4XYX~Bc6B$i$bB7 zefZt(1HOvp;X?f?>ipF0ZY|OEtv%1l&<$}VA+Kv@dftwk>-C5T@^=NwQV8FgAIdYuo(-Ggb*>q#n!cIjt*G36V2O>)-J z8?zSfWTLYLHsL?%kCCsvGR=w#%|}FXwrNd?rbM{9C9KfipH<%KS7rWYm9mqswH?L_ z0^KyRS*sVBHJG&b6c~^i@?J0~-SF0s1#cS&M84cEw-#ibS6JSP0bM@+R%hg=*BR;F zkv!2=*k6L8e>PECRR}F9ebYIZVd_4K&#$T2@ zbAsN|{Mx4g!58z{8#KF1ElAmZd^kS_+;H~z_T%~T<{mYm(ImZxl3j0&w5@t6AMY;` z`5q@GDoxIvPM25vQVvojE;4t;+A2PV51%PPwL-|sqNF(k{`GU*;uGFR^!$OzVy_p z9je+@JAXVK3j2y;ssQkug|0;r6SKI(kfeef)1_kvE3H&OMg!jCYAKO#`t3Eh{Pj)$%TbMzzV9~YBdy->A?`057hyC^AH+_%N11C@8N2sR;R9YCsb1f2cYu*?~V;gUBhQ$MT!a)nx}Fk@-V>jUFzbM(_MUc6=Hn#^H#)3Y+`HkbaOCNfS@dGTm$ARnGM_S@W) zbLVNp%}T+}j4zNrG!*SxJWJkO*<<mS^Y}Bho<0hC4&g9 zEjTRK&y~F#L@ENHA7BU-kGHF0)h0J(sH{?4=2vZ=RZ{?|+Oobmjg|#x;|HB>UtUsD z;^q1S(Z)brHHmA=umSiW#}izKj~C;~RBtVn69?#Q4$4;+>uy@QP;8t1X zczyR|G0upM75F90s!X7C^mh_>~Ku9nySdJBTl1IQ#w z3e}rQ;pE1LQ!I8eXT`WbTJ-|`xK@=(me%jI2L=25kG(h4AssL8l98LJmYMX)tWqU8 z1xUTI+b~uN8bXi}-SO^R>WOBGfNFCkU8&|B9rGn_eC3&8IeIZj)JEw%-s&>1D3K*z zJ+_ziCmV8Ga#Cp81o#*rX4fsNyeg3~w+?gJu+g9ZQS$190CCU%LSTvN*l#JjIUNFJ zMG5(3c&}jf_tH{zf1pa@{Q%nV5OO%!3f{H!@5+^or>3<$o?SEwyYipG zwgU8=PPxRr{sVl9BBPAk^(`E@FYGlZS1XM4=~ZG0G541B7LPc*e<_eTTU=r|6QU58 z>1g7ASpvjS9G4SCbZ_;p!{M3MG;$-)OUHR6(ZKaTejZ~a=FXpsNw<3M@?mvdDehAuRg$^9gstG`$8iTSX^L6m;H7duHT&?{DXmYc{io^7Ob{@ODlr@ znXxoESv6|3K$B^DXxFqT&eox_teUR4JHOWt!GrW~*-nmV(2wWA6cG*wd)TuSyrFCW z&bwy_9fCvqon!Hjm+F%mEOD59iMq7S7S644>#eGX^S0FQsr9k-F28d5cDo+F ziT6nbvMRtRPr5ZEa1=AcQZ}`E%cv+>9NKMsF~`@xVV+@Oae0#_t=6LND7A^ zW(zeeSF*VY8))aW50HUn+Q@Dvww_n4Y9Q? z^gCQ@GoMz%yB-B#4Y{x{Vmr1wSvJ5;mS025N0)4sGS@d^QGT>^V)Ov(S725R-&|3|@$mi!22`VR@? zmp3;x$5f>|wKy2xxte*7mnEi{KYi^p3VlAEpkl_5o~{c>Px~(I69;a(Xzf}Z$>fTH zh8#Da@RJ@j$>ja*@jeyLcR;sz=(rGeba}pUYM+C{QGp(p8JaA*vqPvgJ%V=7vTEse zk_MF1&JWkk`s3y2q(R>8#G-EMhkXgg_sk=G3C^`GRnc6n1(#fFPA2xPbvB@hShdO* zU7P&Ui12&Le;m?O)P*g?3kehj+z)RUrOvZKAGF8%vaIitIw17R`Gy20GT8QI}i! zHhMs2Z|&`Tk*L{Q-YBtNR_#zggt`0Q5i1+bA8*TQDi`l#G-E0sHnHyY?J*HyW?A#^ zZ#nH)_ybTKp!+M3y#dWg(TK{n zF1Zf(73)okRVYBPdR=5(F(?6iRD4l!AL>Y(H&4NxSZVgC30@e7P)KGhV=QRncZDJ| z>7g!GIGNko8tI~|-QFVbk<5MNYH9TpCLC#vpM26&f}9gASC#i6+S`f_<&}>l=93KH z#ICask^IPjYRt$nM1c(5-y1|glOj3AR2E3Tn<{Y~_h_ZGcI4NwrxKLujvkGRQ*QNq z0VD-P&NsS%gaLg+x%+tFLVSqd?DU~XpPaY*H9_#%8d)v{DkG zpYp#KC6eE)589RJd`|dA!VwD^LC5C)5|5@!n4FndU^j54NbUqG*-S&FrcIP77Pl`! zvUFUo6x7U>?)>4PTL#!7G;d@Bn*Kk^Lllgri~e0yC|`mI8N61vXVvcsTd!e*SzJtf zY&1g41E`pOv{FVSU-c>RER#rUg5BDRo?x%;h<4VX{^natj)WnNM@qu!m6g1jPE`gZ zr?X{AlCY!J!3Nix;r!xZ(*6VIdLFm)6RSl6uKRTlqATZxgqZAKf81BI24~-P)X&9I z+L_z~Ei1SZ>mxoEq*Cspp4c1twP5Y?QL$xW7D{yny&o(;yBL5!SVzK<%seF2jojh{o&b$>W0m$;N2E-yRt+>ziX!bFx^Q z55L7Lx;gmvX|(K%*{`2wA4X5WmA^E&fqis*E~GhG;K5vc?xv4?O-JBB9CHR5QF71k z$M>k2F|2@A2iSG`O&eR{RbX!RHi0EGl z>Tame(&RtsPY4K}%){nb|8#>fBO;zxz449K)^AW^KGLrGKtJmZlz-i%v*bvR&bkG% z1dKNj@n!&m#cY9M8vNpczbdnmBr@6O_sS(r6^_6}4>|hd9y{^>QjiD^|NlYp=tb>E zCKBjlY5fEPK_J?FP#H2&c1}bH1q)!SB7tZ1wNP4G+P@!SYr}fUG;MPI&!3_r22n(0 zN&(*h!M{FW5gtCNEG|z7K04xKJM`$kv;YJIuOJw>+gnd&D|k<7>NU>#6yN&qKV9Ly%F+6o% z`zCy`ZT--ieP`9CN; z{~h-KK|(q*{O^ikK>b2$>{TNa3-b?H$8F<{#wdb^ceefriS|(5z2$ZzA5`y-UnNz; zB>RrIj6nq6?Vnh+X>203xX!KoT*pvKCbxOGI0z?r*fC$wEnm=#Hg=m>1%tv(Gby(= za&>6v=@SZr5)k0TZ^l!L@AhSTDYs390xYcGuhJDNCQs%)cCP;>8nkgS_2)`&21jR7 z{1doJaD1gX-yalVd=xXhXRqJyv{H48SXw6YEPVRtnV45=0Jmo-SIFy2j)>zGHM6sR zhuoUk3>)w;DWs0ooC#Z!f-VeDmP0pd*f~+nz5HgICR5#LxSpKlOVsU98Cb7k<@vO4UBqy(8rVmh z&oc3Wz*gVLhV&qzcp}C{G!qNop zIj_()7}l|ogq|fIZh`Rj%W}7KWFdmS!S?JQ;RdFXwFm14gbj`cTGIBrW0i>i z+^8K`|B?nMs)PJ_POXYC8js!d2<^GMhCTj>kK+pafolK>qThlCP4_gIXpPzAtb_Cf z5^5t)PX!NlGrL{h*6~J^XMcZmkn3b~P}L?(0gy1oa+`fX$6eRO-z|pU<#??s8oxoi zW=I6)f8MGzt$jykubfHXVqg0+6K~4L;r4t-girb8xA+&^KD%*MmjUsP{g;+~#g=6X1RE;{PbUfDe*Djf4ooqPiu#9iDFb(hNk|)L zSL*aEjW!M4BEZi4?(Jb~{zI=<#^c=9X&V1WZ9JVXY4e{$B4p<1z$+|0`ya26*Z&Y3 z{8%(1-qrkQ-LNfnPvP@-hbeznxq)k7Jum^ef^5qF@q#ls;~9E*Q^AChI^Xx?#XMp7 z?kj;wB5e#>N8=g^ltF0{?}6W!4VHNY;!^e1EvNnKNqYxlvsS3kSG-iz#%$L2j28yQ zj=K##niqob1oqqp#_1#Hq1Ah%!uo$N_TilmYU2h=I8mXN&yF|0FQ5q0w5g=#%^&>` z+pWB?n<8xAfAS&{zWdKg(q}YI;bfM(LN1pr)9o<)c%=!N6-;Laj z^iQ!K5nFFOD`xNgGcfu|^1A%{%gm`c#6K;A==VSH8SRH>S#)M&<@6xynu1wMw5vi( z(!6@NG9E?riGMncq->PlJPU_kldDsWM?Uo`#tUTLwFcZ(^B19uD$CnhOCKJJ$SOts zP#rJDjCTT;-4s{r*-E`i^j}jcAU@4@YfqWNz$6tk*xWD2wUM{OtZ1V*Bi^x&I+bXt zp^t{MPt2{JBidNqBrB7|JfxJhd6RRyOccQ<%b)G1}$}t%A#?GaBQLC z!xIf0-xOLt1S08!51SL#v-7aX;0j29hEZT9jOm*rOIV)$0>|}z;?z9y?)IjZ*iv9=U5~pOgXIpZ>gjI<-0* zMs5*$4t^H14LxQ=uN<{wI|ngqqHM6qxJlalLp7#96|+z#6%#W9Ol%Sv1)7n>un%~yPMH8KVZ;Vmybv3CRgo~v%y?B?tXWGoxu z>Ohitm_Yv3+?mi_{nB{*!EVc1*pC+8h;vKH zMXYH8^$pvS`(LMV=ZuG#4~ve;SD&au(#{trkr3)4eKis z7_=dnD<#@artGNGpHHFfN6L;~c3MfAr4v4rG^7IuEEhqqbGwZuWaM5+enskk zm(YZOJP;91hQ(%-muN$g&^8af!Y7=V)O_1xQGhB8AB%(}4@tHGDy{FtOd{^WTBFuJ zY5WikmVct+!g{MDDJqJ9FNLEO2JgjRWTeFqqd*ue8D6nKE_?DA4R=1q3FV6#YZ}0A zE|EnK<$kF7AugQJkgu{RV;aeB#hrO9sZ`U@l)YauffaLJ&jJ;h6?~O(!~xfIbu&#; zS-hjn?td(@2L@P3$a6^TId%@I;0_$;ZF$bu_z=uoC#0c|NbVN@=yv^&UuX1!WlYz8 zR?C|C&k4V`zzRsX!t{z-!}#I>tu)jM3*a{|Za@|{t}f0RU)k$C4_G>GtD0_-#uS>B z>{Ue&R)>Q`BqV!YoW2CiVeVU+UUY~I>-Rc21xHY7?=ed2)3ONlzL*HZ zbBBX}I-lW!#v_$RqP*0{zY!yyJp)TgkDPGN%idDEykz&#WJl++;BN6Dd1*K{!h{u~ zAh_jZnD!f!_GrK{SK8OgXNcitQ`N0xonIshAM1RWx9}ZnbL+4SM1v?!6D)ccxcMQ- zIzi{SIi*qcXVRl|&mji{Pw(n0Xv~sx&Yj_2_(qG3GEW10Y&Km#FQ`S{eUaB)c)2}_ zJ#1bQQ<7_3Om~M;7@B-v>02hG5tePkfUsNmpPZHl*=G?Z*h=Kad-6aTgD$ zwR8YNkn#*0?r0qk!o?VrBRm*|AR^NkwTm-0nU_YbbiiYMF{@^3`FiEJjU5_ej4C`Y z5qlmK$cI$GYROD`pPj_EGOPcOC&^MMSQO;&Fm>)7A5jc}fDGv~Qo~LdW@3`L> zeuqRs0ztuL7L1;4tcYz4ue7LH$x$4TYUeCO3d^Oq%QFM_>#0{o9wP)}QQ)x`P9wE= zD@et#QQZ@K<i+$G^nrI-?W)M{3Z+$|4j6>PoCo6bW}b3 z*Os)_f4aJk?4Dmw7JOu@1pe{ZZFFuOGP&d7WYiJeFJ3(N z^nA{ErSzJDP#;b4I2VVHOYTzCu{KTHqDWt#%dMzA^aX8*C!DlZa{B^D>hte+-tVaU z{{YQES2LJ7?Q&K=xkg*uF-y3SyEzoGcz#yjG%|uK8o4u>9t+H zGyTlW44qm{+fb03dn1%Ci|TKFJr56Q{f^^zs55scTU_Y=UP8h~=0-3cTM&O8-{OF8U8?zY@^7h< zfxjO-C1a1o<=AdD3G|>{f7Z2jN8-zavq$8XX$BH*u0DHXRxFc&K{C9Wc!pqB=5=aq zU-iuR`1t5-t#vm#>*{M{vN>u2 z(aix3lHFSod+!Aac{@C-cxz1BQmz4?e1<`J?m_vvDPvyM3FZQMMBw}UcfB(W639@Y zKR?&l=8r?gx!c(Nl|NWZ)hE3{wWNOr?aEH6*_8z6qrrP9Cx1t;)QM^_FRM15DpZL82 zW{-}{B?~i)bNuR( zT5rzxGC`_;T4zY1V*SkYw7+J|++5vG9B$zB-pUF(78_{smds-plHaY?&accG2;{J- zC#R>zM#hp9K)B%_!wb4zG}&^N*HF@O<))=Yv=ZOn->)-qD61(dMkR$cIE+&GuT9hm z3x^U!i0T4-5@$OXnW9iy|ABJ$4pimW7d{(c%OWwR^To>t-`OsqcMz-Ap<4r<#eQjf zKbf$)tbd7aIS(OX(l(xkEBJLig))(mKAJv%6;!_hvmD3V+cWVBujQ?9Kn)yOGXC>L z^pqtud)??^gOvNoI#7=JHHpn4!KdhcW^yB!(k_#;!&!gM)T6pZyiQ0fr*1sa3*(*a zIhoD~x_%I*xghLQ6}*8kJT;Okdci-6E&9&;j6f+j7PEG?M4dohM2rUpo>-fH{JYt= zYlJKGmh23!zu>}i`+m-k!lWdUrRtpi7?DS+P%TXteS34Gqo-G1Sq=vWS6y8_Ei^9s z#^z*<+FQD>FBTeBRGzWk;`6G4K)z6EdSN&$K0X?iXmH&!U3Ob_aC~Ow@8tHzU_5P9 z&r~?*!eBU>nXg_cm$oe~DoJim!pf@Gqy`!(xHdFs726*@6C@c{l~R(CQB;#*bX!p2 zO;%U<_Dy{e6(7IMal8k_;)BjlKCdv}dBZ$^_Wvl}^g#vpD6h>;5Ev(5=$Yde78Dfx ztr;NXV_;ZP`uy3#qI<5|!bFsxU)!wEmxl@Ro*w72zTfrG^g=v$l#pg?z-Z|2>qsTD zeMVW7A7cL(aqk@z)%NZCHlTI8&hL-&-o58l-FmC2qF}9_z1CbajnDXwIlAay3Z#C+$IJSf zw8Jz;k(adKeJmye6yCVyQxpl{yEb;DxOPi;@onHc$PB#Fm<5F5vQW*(fd zbzf!r{Um8t(iXd@s*kR!4M=D7lRhjuSIFhDcy7rMuu#0hkkxfltMn^ptnP;*%T6Tq zo3o2!(;ps5-3l||Grx@X0_ssPFz%v`j!PirtTA4<(7 z2k%!cmFMP;8gto!gj0VI{o!9HED|98-c^yKHWr98$E+ps1O)O%tF({SUI6{zOei^s z)Y}wh<-?_8h?Z>F(|(_40m$BlDGm zgC7;Tw6d~sh*F^BFH`6Mc~b7x2tPYSfZaM|vpTvuH1fW&YOtBXsdo?eWE$+#N=xt3 z`7gM+om7DCOwOG?N~x}!8s53z+D!o>>LLC7hhtUy?6I>gl#cb$K)$ku5KPvuw>d!yxO$E9TX+!}T3P+M}cW2lwyW z!(g;!I;u{ebAp%>FUx@RyC^%0pC1VlR3052)i^kv^}eEhLe$6z+$>*bXF*Q`$w1=b z;=swUu%2jEjW2?Rz)&Ri^Zjh4kfFDsX!D(Gy;6r*WJYPTcoAyVNAIt5&PINS%qsY3 z7T@yXZC_Vje+m}m0jb9YsR)t%`*syb%ae}Pga`t;3Mt|p%Km)^%}ytEWjD=F$FlAa$ge$7oBk1)k{l5uFw&TZ?1ycrnd zjOxFxd#96eTO*cFz2W=0h%SUUmU_TB10-^GN*FaZAmU9s(Rx%l4z-f%FEWMI#r&Mc zYkwGH(;mYn_dxevK)s5z>#I!a&he?ZvArHyZV@5x9(kqIP1{Gt#JBLUZ@u=dE=Gt$ zwWVl$-dXeDrOQy=CjE5glb(Uo=E1esyI$0FF@#x+GDxl@Xwo|U>pW&HL1ddmE;Df$ zIy^Zk%*PiaUlie|n)gk$+T-i5j{Hy2@R1cv{aHsRVT#Xho^BZY2!mNci#FOBn`x3F z5bHm1pg12t*60->k;U2BAYqFw&IC4OQ&@kbr6(d3sfoXT zXVXx_A^7Z)hVHHPvgFy&rK8~X$s3sHL&nJVUNJGN1OtUQI@S;q?SSpEy!O24_m0NK z#!IM3z3Qgr1VH+Xbm)Iu!&Zr2|PHB4q}PES)WmXvxy zf=d70^=V_SnmRIKV&cy3vN3Cp*o{?l_1!nkZ&3%F?;PuCOH+S ztlFCX%R=J0Si;-zOY>FZC{8m$9H&5{`HVh)Hko4TyQ1~T{nD_;Vp;-f#DJamiSqGV zmgsWY9lSdG@9PmqA!tVi=Za}hgOA7;IS1wp6FN&*-9~9lLO9^FQzNT|#rC>k^!{4X zE9|;dg~y`(`VNaRoq_Y)&wYjF-`uwLEntacGc$^}<|=l33{Td?iwItUCZty{{S;~Z zoDtvQfz(KoCp2czwjAAwFe&A+>JDE{e1NKtc69D=boX!dT!B!9zP*EcCp$Y^P)LYZ zArXkcZ6w`g4P}_>)O$Zc>K!W7~n2%YI?&uQh5KCKT)iQr>?Hfg%5gW z$Iiy4eiEiV;vC*OHQ*$}(&EdGp1B^3Qo7o|JT|y)-vDKHf~=Quak)iB4LhSR>E1dy z4FuqmiJq-4c%L7EfLpd+bhVee{jQ}%06IH+ zc{PIG$Y^B74O5sQ^}w@!`t&rQp);0{YL#WO^iA5ERbKT3Uju!T8e9DHmbV1NgZlh%cQbb z@XS9>VG%w^DKNEeM}WdtqB&dD`M{F^t&AwJxXzhw!(qRw`d*v%Gr3JC)j;XUOQ3kD zH!vn_(35azo%%g{GSV}9V1-FFpkcrHH>(Yn5egrQ< z>ukp3%_R?OyeswT-twh{E0L2NqPAmHKc8l;S(iwtitKk8N;X7;=5M&Y9K$LILri(8^_{g^DUK*<{Ir#5TR2?r54 zng;Gm`%9f8txSP|fsZ8$5d~hmKkgiFP3g9c0+DvO*wx7?KAF$y)%l$A$y^Za+0r@f z<6-ZUw4k2k4N=TmPK=3$=7{iqH>DFVtOD8F-R+XPLn{7wx#4Vx%;02x9%O3tEUVFz zkZ9(o@v`5U?h#+GXFka1Y;Zf890SGw%0hS^RZPLx2ji8*pxVZ&Mn>Q*W=Mrud&99H z&qlQ7R2bS(OMFukCDtoHjn{uJj$<^~vb%X{dxa>vuWEZ6&QyllCTPF>8kziS;gj6J zn%sa@FGiT6dpaR^M>-}m+t(_?3aeUVZ32DnnN@@}dcPGgwNjgje5(!ftLXN%_-v8^ zA)8swaYakZ_N9@s_N0lOK7??Mv5!Un?#P-)G^=Q`LTi_DB8Pt*Wlt_mjcYMu=hK&V z-J#2rsL5$xHt=uWsE0WV3@hce>#;jZiCY4(X zJ6!?}Y7KkBmnhNl=-}HzTqgX~hKtQmlJM4I1GLXc+xwmkNQqUF_0jWvTTujSnKL53 z6^7F+1;HI|=G)SHnjcUPL13GJOiGpEbd*XtJ}%QG?t10ax)mW@cl;dSt6#eyou8}e8L z);J#NzAo+T~zphBq%oMWEb zPEkHs5N1;3Q;`|K2$vcHK8I>;zZ=@CsbaRy^g`7Q&Tt`I_)ahDu&6FeT(aU@{;>^ zt_2AqzC1K8x>wM&hcxV~dV5y8!X5SSa3|m|n0U6R4&QJ)N&b;F_O@ZL(pp_L>pZU> ztDm!^5U;T@dCXl-#WpEaPc*iZ>5Dl>eVogw-cYgxYpnXev;d;o(f&jT-6BJOpm=~K zXLoSY-h0qv))U4QdV!k~Smw0Kae8G6*DP$QHZ!aRgPTwk)KWB_SR7AkaUi?&o6}lR zhej%DV_OIt#CkH^?NiTi@fRk`R*5wW^)!E5p&LOsY{5j~%@MGRKS^-QaJEO7&W?uAP@(MQO4z2MG{YM;oi|F~cBR4#$VisN~1 zJ~dq-`znWv#A|mt2o8)fzYIBSu+9R!Ca8+rQ@7+GR_SE;VC#4Fu9Cs)Z47#!7FQ06 z2fVzv9==$RQQz(0LG*?B^5xu#S4Y8NEsZ7ST9a@@WL*EJW?=8Lf`<~*w0xZyJ|%;{smlq#TA!_ z#DK5)6N%A=TwkSzU(nKaftmg4C~sY51l)yf5CjyTtX*}oGxH>tVecclbSJOgZ)r(s zhu~t_n9B1 zUdUo-eO`&A^6c0hUC!m(9P_&2v$ovdfP0wD2Z=;zwM=RjUT)b9UTy_h;&Vz6Y|u7{ zT+wn$L~5VEE&sjCIXZ{NNnnS<%{Ne(A z;k8eT=GI<;T@EeG)x5o`yoNQF*;!KcYF#(!F2HxY1zR6p?>EJhU7LTu-l4`fmlZae zx-RyR${4$&d+C{vL;Ho{YE~+vzI+Xi8BcL*@t8F@lqA3urxp|)<%w;FUiG9Bv~mwS z3In~!##;z3WVXljkvrYn^VmOO3aWb-*Jqf#VV2`Dv=hUodH~VPrPr0+Wpkx^_tm1V zr-6XlGyks)d%ZJ>K6bCJ*5_u$Jj}p!e#z9!d;KwoB7kVE?ow!mcG{ruF5OjoFC(}u z!mG|VOo?9NpU~&hm;Bego!7I4dpk)*W6R075JMzrS`d4)LlO?UUzO5Fd^Vz}m^SSm zByH~VOar=KFvXFJOa4~Ps)%QM;+iINE9VrTq>a)fqIomTSiO*gV;Vc=x(XibQsR} zLOgx55;dKv%$ACM!sk_Nc_K{c{7a~%@En}-6Gy{-L-)bHAFomF1EE+jMmzWxh%cNa z6~?^_Qc|lL@Ac|iJ5cRuUOTUPl99$@*3apK&8}>&k?Xlsc^$k4%E^T{+KGQ~QIf4V;FJDU+u zXZJt3qr`jFyQ4^RKEo+8qArlwc5Q8LkxU6%IPZ0Z*je~=%83Qs+Mx32O4>^}CZDj{ zpXmBzESvUF@(Lq{J~A_4s|u>%HVU35H{y%Gj+$AYoTdgZY$WZ@6NvBO*|`$0&)J-kxe60;Rj@gFR)pXUgxsm%(7xOS>{hjC{v zw_(UMK}hWwtO1j(E|*Di443PtFvu%_)43w+EF{xQQ3>k=h0*^k0)Aa{S8EDT#y$K< zWB834I`Tz6dl1w+=8#7ZG_v|787R2BUAlWhxGJvbev21SVb>Q*U@_yXay#Y6ObT;E z)BV=^h+siw2mGZTB7RM3FLcZB8@2R0U%ksuIdQoKfpGSB^2x0Ic!v>@L{7SnE z84d^Mn}0_5oA;pTv?sW%PCSr zb6eSwDj8x({O`qnNI$>*`k{j~mhb#i@FGm@)$e`xe{~D1aT&bE*#~?cY3%{eebZ-W zaUc?4LFuc2>aJYgm)mo@;Lx}=n}YQEt=0Pw7qpz48*UB^W%-tuxg&Ds~^qHDQgFcifCkqwrj8R*&Q-6&hAW@ ziWF~&6_{ELDPoT6nM~UgPr#iXKx@xt8**jFWkb~8&LkE_Jv6a~pKi^AyP|6UcI~IoNY3#R^Mc28XTS$HMB>0p(d&`@VD8g1WOB^O zK&>sT0$qv`aEXo;GMyEYG#7OAgDxClxqMzjb|oiDPX)A6Z{76((k^#XwlSfo>1}kS z?6f0_NhQ1jLoKLm49a{(p|{PK5cbp zi9qtZscqi3QraRI4U3aouIH#>U$w=%1>bG{V0unVJxSA_GDbG~mcMN2m1^)s${)Sq zkdR zKeKbZ^~+7>f|DOG-n zedU<8fSt!JxBFT-5&q%%@pANN6`+d~^bnjxe(r>nC$hNW+eh?RvAU$<73%9zm^GY2 zzx3-uHI~D%oKzQA*G&tM!TQyUSEDoVXYulu3BzN34x6LzmO6vjP0GidrluAah^uV) z`FN-Vz0BPUo12?c8}^o2IniqB>XoLQ$Z3ySd!&8e@rUE*A8_?;(a}_$Y`+`B%7Viw z*I*+3rv%57N4*pTBt>Ko)Iuf8G|9LY8c=cVi(4Xey-~?nu}=?sDa1eJ9BWhs3GaR3EsM+RA2K4CZt*=oY*=m1 zbIvN-yz9ZZP!o_z-f7MD+xCMJ)+7zqlTqQ$v;B&LC7B8pSZHQc%o4NgOHO5~)>ANR zh|u(Y%u(Xxyf}$cYLRSxKrN^Sr4D0ywPB)uXC##m&99%+?SL0Xh|4DyY6csUH&qk%+wa9i3tZAvyfrm#7 zWy7r*!FyVYcze>G62ld6F9Dr64G%+63}vf0Va3S|MRi{Oky<(OXGWcm0O~7O08y;z z7fnJQ*L_2}Xq?DW}9ZSr1~*dpE0t z#${fYvqZs5?Ix6~01aNvhE9DX14Yx>WJTX^y{>m10Q#;huNb&p)hxy5XXls&21%uO z?6)s3Xa9^`US1vnus51j6V$}u#6)cOvcBgb=(qty-3ki&#m9Qbr@F|xtC;H0Lg6cz z2&KW5`{(y@G=_$W)|W=j^by;C?P=KxlipolzoOWZu0O6RLeL&dE@?Bdf`UatRT9We>K76Mgdy#Dd2fP1`XMEL zUOrMTn?I$bS`$iLFWDVDb>Rnng4Z2|_#@~)i&FdwSUN07(0;VZk6p>dX>T%G9h#eN35Hw)dziyASLOgxG zUC~QL-?X3-uYD^mrbNWOe;%W22- zz~}zJB5Y@zsR1%^WpZQMq@pTD%*yIJ_UV3LT(GE@;SN} zA#5n7eu2&^Ybm_Bcl;RIz74RVlaPeqhO{h298(vEbeXo?iNp{~wnn_p)t{K>6T7~^ z$TOaAa`zUY&Md60S8Ki89UaT^)+Cwig(KopQ^g!Es)^Fm(tKt+eBVFmgx1p2(_7iX zFWF0JD3g!kWfJ}NE7D%eoz<(4<`)UbVMZdJ<<;~v|@(( z`e<@1+sY`yhsQ*^pCXz!1u3S1 ze`;O4#nHc){f>)zOlG^=b}ap#R?8#tOvj@`^C}h1M3yPKVv0^JGs&o?&+$JOA_SYp zf2r7V#3QZE%wI0NTWaymA71IvRRoLDa?YK@deZJ?ww#s~M7}*fT~QbLPWUf{iHG8k z&tj}t^c1jQ(`A|>errcJ?ai_ya60KNS7qNuQk{jY!T9fx#;{U9n%fQP*dfJ5-5dF8 zm(jY^*doF_Z2r}vqCVbUUQ7Gn$$*w=#_J?)eSJ6jiZ#}Q!vZxuHs-#$`c6A0fCz!O zTK~+MA~Z7PT{Pjkd3F}alE4EoOH>*O=HsWd2>PYQbH4si5>{ zmULuggi*GM5E-K?8KWklHHVgbqLpY03}7BuYB&3}<>D5hoqh4Kx}&>sctfeDTZz@Ti!~`o)tkJOAb;#)IZ>^A zJ9F_o&&~)BsM$SpLrOr6yX9K;c@wJO=CvjX&qrB-golJYsT8`uS2l|Ccwj3~ID%?~oU z#u)kV?D+`2KNc06F~o0VI-U0gYKffdNi2t}9p3!4tR5vt>1K%I`cje?hk4e#M8a0W zqanXzW!grw981ByC|_l22&c~P(YQ^}{aFqx>tjv+wGfsypGx=QnWdO|EEgXx;`ny? z`_J+bQ`r`+%jLwH#L@4S@;)~|P>DSEtQp63#kSqye44831B%VsOyO5mT_Qsvc%tQ; z4QpTgK|ES#n9S(y9*D5$4 z#a`b8BVF;CZfn!deU5I%?b8Siz_4s~O9vnsh(A?NUjfS$TUZs|b&o^TvWE z7LmO7?%lkU$z$%zn1TlXz(!(t8>ae(zF9 z$;j+x{-AomeT!m=Ihv0roJo0(=@J{_3s!mvzkBG`1;wm^tfJfh)Mj8rA8+A*V8J3Q zNk-Lwy#OPf>`sXpP6%YwMi0?(!PvNU!}-DSf92r;oVzKz<1d|cZ1CIn1BFgo7bB*< zAh}CBxF8w1siJv668lus%^0nlx6AB8Vrizu`X88d znV`rEyCm}ipvfT7W^s{d{JF7Tlx(R~4cTS- zJPg;DXOe^BLm;(MjCN@5>WXirvQX*1UtGgNALS%d?7IB6eu8IB?xa3~(F$C!fHGaQ`QtC~EV2`hvRk;fHYDjlqMZ z0H^rLQ@6Tuaj~^xW(yb4>G&k4uRd_8ud`J3k*VetQ7FJ(y548zy5Snh3ER<2?wFS2 z-vR^M2GW9MZVY-$8n0Y{+tql5Q3W>l9_{&8irKMd3c1`-PbTu*t}qgr=DA!ZFyUaD9?x0Ss>4#TQ0bQ(wYs zq5Sf0Ik*+m8BE=ZfQ|kqOH)Y4>qn!y1HO5z>a6J z9MBF^C-&}c-7#yh@knv~(fIPPjg^g!g+-OMr-Q@ZMF3|^%sipk#cz!ekajPlCDxG= zx@2F^;qSH}Yt0xzCcb&s-7(t+hp4v zXU%t2H>hE5cfWX~D3tM0dn=9S;0lJsdTH4%h+Q-gB9W5=lg>Iz2&svsY ziDh?;UHxUxfRf1A;Gpw)WC8_c@4bhgoB#STd)#n+2?Qo@GfMY@_n2tlb$NNuX~JQt zVq)Cf^qr0_)n~tg7t)k-OXhE`q?Zaa{?fHNswJ9=AMlfK5{^KjJWhwy6J;45O--%j zn3-Bn6g>!1N)djis#>nsGJ3c{hOU01#Bd=xn_p6Qb;5|g*!u7>vB8$Oh1rgF!vZ~uwyL`7Ni;pEZ6th@)O_bWWYrg49$Cq|d z)S#;WW%ZB6(EHo_+c(sa-o_uLdy`SqXVKp=lWWZwR% z=40qS$CR2xPDv@`ysGTt@+w98;P6O6p|@`L!l{gH1#|lIt@1y_1Avg7%l%j@TlY(L zI}#<}_D1 z*l3oTI)#Mo1QRor)odp2sUHlU!oy?-c+rpr)@<-WSxw^Y3_j!caBCpqplO~TuNyx}f~ z4znl$qhBP(8)lzpFR}ajvq5ygQ@PY|rinKxfIl5-*%GVD*!>GB)nzNfRdd7=t0d2f z{l4ZGHM6#>ypc{#ZRg=3!^|E2!&p+`+XAs0k~8}U2M4=tZK_J5W0PJPsZ}+3Ylo>9 zCh+U%rXa$@LuikwQ*pyth~@}Br+fYM_BIB2Q`PwLon6LURW_U)lA>asRVYC{T~8x> zO_M)%+xXGiC#RRkGj_I?9tROp_t1PigJ{fl?Fy&Y-Xd{0Squ*!pXfoT-D4j>UvbFk zF=H#KKx)d0v|ayrES8Hvcx-F>NJKJ#J+*Zc&w@Rc2YSIKo;DNPqVT!hxi>kADQrBp zRyLP?;RZAN=LC?^fEJ^gJuQyh{Bz3+dY3b&^L#K*2AdUHrn2~-Y^e^hQr>Yf8Cq9} zwv~BQOr`HV65dZ`S%Buki6C@0HsYn5gV+q$BgQr{i^jAA_jF-(A=oTKr zq&5=kR30w-kp?{MB;!1qgl%dmbZf3G=jB-_48#l+6Fnuu8yp^%S5TrB^)k2gK;)J7 zotw-Q5T%!A7)^eNYJyeivmco&R_sQOmw|I$O z&q>h^scF&*IJynV!Cp-jU0c^hU|h|lz){N}qhHKVRBe%2+UhiLMI+*o>D`+WEvF&+ zqP@xvWnWs5=mV|H8@eq>>2(Br)v!GjKdJ9CnIy~op%ds1K{GzLn5UlO9AkEYg;c-k zX#O%glVw~$mwQ<|%=T%eCH=sjUfV|DVDyhu`S*#0RVR1IxMJ7=P5tO{Rw}u(8T;lt z=qM)INvYCHY^`O2WJf_mGmW-0m{V_c2n-Cu_IsUn z4to}h7#zI80Oq{hg$XY zU$_{{RH(Tlyw{P6{$wid$frSdmctcymhaR3k{Wr$NsaK>lD20{;APKwwz`K}3;+YC zXgQ!$QF&lc_zyazQRdWcs@7c6x6|q?^jo}1(< zB#7oLkU?f>Xl^cIE-mf7x;i(x(eC;X*lxVDOmKVx$M3aZa)ThxU?m(~WQDY=L8N>Z z5ji|Oba8P(#(3l*k>Eq_j4R5_g9A<$uC5yIhSXSwCPqdEoSr|vN3xvUFgTIey_}t$ zqb>o}(Gi3dd78Q&?X}Wi&M%R+h-}nZf7bNcnuSEfLh!Gw6ghhO5t`HGrZi>gcaLKy z4x7oyLch@Y^Po$1+vAnW#|pCDkG3X|(J&fFNt8g?lbTZjNtiX<xRl-`Zqi* zYZ9ni{*#9l0Un0E{aq8vRlsf3KGzHe0$EL={{&($E507)su(pwH(wAOrN4}$(C_rGdo)zydOIMIVs-d6*? zqUAO@f>b_NH3r%#?i;T)V~Z@nIC`Ie;Jj`c=gtElw@>eDBi*FX&or%p6Q_T`UC|LU0P$}IX{S@8#1#cNaGq!fRo zC%&-^4=#{NhRB~C?Of^KG~6bjw{@_dI9jKqG!+&Q85mSX+qlg2G#p>R!DRbEy~r^Nr}8Xgi-ZztMLZ&4K~LYwCDK?OBv3{4!C~*g!1utW_jI@T zhk=zjOXGxS&);0{)RG_tnxx@AqQ({gJ4WjkRi>+`CR|yXQbxTbUE0IwvFTve9EsgJ z9I;f0kq`LV648^k%?nebBEq1Q4Y#R|A3F6g(-Bu}+Pvfa`wWIcyEWK+oaCM7tWcCv zXg5gelmB#8@vT#QMSQ1@3%kB{^rOC{2p(3%q;`|6opD3Q)K;iZ4*-*iI5*3;eMVDYl}Aouk&qvgg#wBM;Nt<x+ zn8Ly+v9`mMlclcq@yFLCp?D7wBgPVzVtN(~-2kb{q`RjY;Js5{cm#OREcjalQVy(Q zR)NFvb8)`pion}O*98h{V74Fg$vlEqdU%dvn$^zszeFIDRWvK~Z6yS?a)6PKf*l(r z`%-_rp-;Y3jfHK%VF7-O`ZlE*DCH8#2YTYdYNCKD6kj2lSN(y<5RfXew<)MJLjP6( zH5T=gGH79IQ5xMyq5dz!4xo54ky0P@nr3q$0pBGN)0>D$k3rotl%fZ^tW}h$F&Cg- zKNMqcqiiX_mURR5JPh?EQThJTQk8m=ie4CilPd(7AA3V1Tm zR%<)0NY3#7f8t$;sMVNy7X8q174d zTFC=iox5~;;s})?S&6OPRekMkxttGGr)u93;&ThQkk@?&T~Er}r(W!zYLvHo^?q@v z>(gBq2fh_n$M1OZtq%U93&VoE-tKdHj4)9k$N8;v!YZo+hMd~a2qZdAJ{lEbbI`1Y z$d?>|exb$*`2xBB_|KedPZRg)x;+u+o{Dp_$S^jVyP7p5iclc490N{OsN3jH%J-GA z%V@Kf$a?Oztt;L9MfR1+x9feb4R7xJU4~Ts#6Y6#7>b*$Q$IZWc=+^h$xA(}splB!`&ynI8@m znmAnm0C~~#byLx+7!zo{p!3#vr8(R3D@6$j2@p~A1Luw~O>bdnRQn?QtS|g{YvMfg z4(+6KB?MByMKWW*V`rx2X=&G3mVBx&g-RiC-aLC)O48i3D5K;P`l%Suc%t3@LrY^mIeMW)RRD1lzx=LwLjCXaQ$ zTBU)C!a<;#FWbwyQQ_zT70z_^qePj-03v|klGY|cyoGsWenkLwoQ$BOQ3Inn!l2Ql{E`!iZF`CYLUZRO%k=yp3K=gpgmQsy+llQu9 z$S1RZ3HVV9qt6!W9-6Z&LiC@<%ljOSnI%(GdybFalcQ=Sr=r?KbA6R(t8xD_z0%`*^N$qW% zc^(CeUY!`R0xLVctZcteiuQ7i((d5s@FrwyfB(miKZ}cCuC58$Lr!b`b4@`*3Ir%z zh+*{q7Zt3sLRo_Wu@VG!nG=^KVfP+hH&Hji^WcA5qVvc0eV)38u4eL_KXc zqMmGkMj2xr@tZNEYl`nfI z%F$$Yv7NY#ZD#@r;&NebZq-)!$Lu(elCCzpSEONVrWlPItv`2xOJL>O;WIa$mo%Ms z>1ZqnjpbJk8_cJx0*G)hEp}A+BERPihSWU~?V5A%d>nz~rw5-7XuJ$W9t%&+Q^xV@ zVfRrd4Po;ZO|d3)Kk^qIF{)9Je^wv-uV#}Vc&U(lWj#)@rF2WY z2t``VAV)yw?ZY?#BtJnT2J2hNiK+0bniahwO7Bhp^qT1Dl|)XBoiL$3V(1JLW8uhmHN3F-7mXT zg#Os8T-JOIJA|`bq}!6c{g7j-t_8vwpUxw@zf;vInOezqrw20120Sq8G{yf?0qIqk zpwU%28#`gc{O+Hl4@pT$IrKMcc1BAR5{i4eRaFNPve|PC=-&Q8lv`LBpO7Nxb@F@g zKn+A3(hZP0yE+$;TaC=9j2uWalv&RG`ekfm6FRY_$nXf|Lyr*(8-Ue_May2p;a5%3 zp&*8C`e^CI1_I$zfVH=GhY^HPEU%123Bsl|Cv#~zVu4r z*14l5Z0|n9|D5$S#B{TF->6W~qSNE!Dq0^BmRHPrSwi+3agLRg6ljIr7Wfu|gYP!I zNC6Nv+5k(h*0m71!v&TMB`*5ME!ZUt!Kw-k6zGlmG`IK5X5n%ilfS<4Zb^^d_Z+HQ zdP`9LRX~aFv_~OR!^T)J1>e0s8pZW0^|>|X2kG^!cuQHcu`sMeQEYfM`O*>ZV2u>b z_8CK*m$al+r5g9z%E=QITnU-P#d^X_fs~qt)v<6zk<-d|DLtWwVM$G0HL%i{VY3FSO+YVAkt>M53~XCSIOKyA9P zjjlL>cgs)U4c=Z~Nwo#qG{uL3nrzHjV+bhFm6)cxeK0g zX+Q&57u8*2|B)PdvPaqw-bjwz_~MKEzfqvA=uC7JRq#r`^07|<$>;`xs`GEj(IWd* zMZ)WN)v(l8?{4BmoTyrt#z&7|1siiIzpIcsoR&S#|M_rFS`m}y_tld%5pdU9Fy$;~ zGitwk+PAtd7Gp9pH1sSoI3}GDRr9LFz z@}$ng#n7;^tRU>@==C386?2Lo5V&m36mN`g=@zj`MrHy%UvO~nbVFyUR#)2-e0=;7 zNhvck(`VXcHu*(G9Ln!riiyEXb5?!~y>IEMYB$ygN@9kM8c*h-&K>!O4{$iw)~sHw(uLJ48nj4C?2aOLNQZ z8YWi6W}SC>#=}aZDygxmAa}oh^;cv_Bew`kNohFx@#OZ$M(Hm+{axpWzWoQz$OeTW z^RoeTr?ODG-&<+u%PF+ta?7@s_^0Z$qwIsV=zj4YSCN+}f{S5_V#tPRlm zxNZM1*gNrzxwfoxINA)Q%;0ae)30-D$JCiu%#H3D-65np*}OPa5-ZRr|Bu_B|X?Eoi??@wQho_>^|~^YiS2pqT~Q7sU;2T-b0bz z@T|Y$o6`~P4V>xiuT6iHv1tx08t12ImpxX$ck7#>xsj0@;2cf<*d1x63;SN(eAu+M z1f{r-N=Q!huo7cCM$F+B-;VgE5ZjeBKt!mOtvlN0 z6%ANWpQ5vWRNA<8r$tz9uk-HP3@>;admmoJV035 z1jT}$CL370xNjXlzR8bm9d0bA+;Hw&RXRl^&Yg~G{c0^fEXDc1fEybgvk8lom*CM{=7St);d`4g&Mz`Dp#R|3%WzyvrL>f`tEpmt zg8d6VhEf^K2J9K={?Jn)>zxeT0C0E&{)mq%|8RJE{*jHz1HBRB44*#)CG0-}G3Xx- z%G^H~#sAdA4L?g=yhkqC3bbVT+5_;IZdUz`+bjx3+&dCQ`i#A~10|?l#T;-Gk7o=|MfhW&h8Pct_mzRfb@SRF{I?XZVr9S?_$sUZ@d|srh z;6v#{5WZ|Oo=HQ9S#bUOjaPSdB*;8@zT%8-lph4ey%;b4z5e{Ko4}LVn5;%C@73BT zyL8@90bK!puUVG5=Q*{675Q}YeC2+N&xY|xDU?YcJr%_;;+EEg%gRUO}+ zb@;xb!q0u*w=)+fIC4Ze^^Tn&$pbs;G(}}&QrY#*i%-zPqlW}ppI162>+1BqgMtO) za`@58d&!^w8Cj7gpP5YnbGgRm~GX0sxWVCkus z6v?dnFn>U8SoA=?ciW!wzd@TS^!z3Sffj(TCYB;Imnsc#yA-=002@jwR_loLiZuDj>qT!|GBI&M7+iqi$ayDWKa$T)yCz<3 ze)BIr65=e1v2Vd}aS~VEWVaIw`G#U~Mb!^Z%PWZr_Ok#g42xvp7-W-_mA_wHS^Trr z`5dg)m6H;!NgSK}#0M9I5M~=60Eg(@NIRN<#CNQ2ULhcaHn_#^=#CQ-7z?&4T?GIzP?l$ zOQ8p&+v_^MDS4+g0)WN2Y;P!JRoFV2*HcSDG+0sfFLNLp zSX*m@PZLvjW}_$+3JC}72DoG{46o}VGq3qbg*~y=(briHe@!HZb#H2fi)f9kMC-0z zOSyAXmqj{?iPJJd;*;nZ&pQ$+k$X)RfqUmzWCMo)&aQ$!O0~$vpz@0l-MLm)pB21y zhkiB%|Mk}_8c_?aES;#l?dUi+)0tT)C#U@s$PbYqA#Zz=idR@X*Eu+7yOVAy9c^kvrLKLI-Pyy0#ZERxNCi_hVf(}lN7jkSx5^h$ zsyLZksvbEbc{(clt&&>2?bpI?4Rx%66zwYYR9LdCv(fa52huz5R*>OghcuE}kGt}o zgiy+mUAer;v{9Z0RXY;MI$5+FZ?)u1# zPvgsxFi9q+OIS0qXDfxoRw^f?wq@xaI3NU-HgPs^Lt)X4bGF;h?3$|-5iN1vX*Ysb z&BhKwx@gfny?nDNkDSaBz~ZLcv}CXcHW%$$qFil%K>YRvo_UkKkf_CP&N4WeCbG6j zXMGhUvF?rBCvRt%qFSlBO^e2xu`hZeh%p0Z#)96jNn8)-spZ)jO22+z=h#^%ktPcn zb?5M`qZD&jS zDI61|&c_XW|4=Klx$~IKW!l2*A}|9C0%l>{qM99p-L~8y{&m09J?tr4{h+C22t=Bm z2!kc#S?ktqhZ7nJXMh_-x`vJSFTbKJ4oyp(z)*Of`IZW^-L z?mMRp^RK#eKVK=(qx1W*gT7K>1jXKKeUKKvJw#tUONQi%WQo%_`aOwrxaF0~xr8-I z+@E_sKNIb}rdJTEaqexjBtEhIg@mDpzT^Gc{_@sz5`s`xBY*%#n!=( z^K3M!4hxEkR-99RxIX7;VKM#;ZlbKJYV}hQSe~d`yy>#cd8N?9YE1ueWW23mLkuTd zWOL8)7Qgnd!Dn9r{7eK}P*40mvBXIPU3~EY5uldl{fR~UDFV*nW`RU$B@B|InBQ9i z+ZKH`otB2&@hGLKuOnS4>?glP(|_7m;!jFg*^AySR-vsFXI?`m@9Yd|YG~xn({1Sd z+E+6>fq-nFV&KuE9qJ=2tb^9IoMQu*=6*{CNMt-a>wXH%m!DCG!IrcEwXU2(p3|S|O=_Ou;YxWD?++CmTF(2kW?WLGy z6XWO6z^5H}5rZndwA23MlM~s?u1@I?=U*%4!?oqNs%a8oN}H(WQPY5onloCq-nK$z zPS?*XZ^jpsmYv-K2j)!n?oQsF^KFlrH?X*B+oFL><<*wu7w%ga%gZsC#Y@4| zfy=v>r4w4FN`UY@rfKX6x7Ml0YJvXtfeydI4qnvs+VDK>1g21Ibm55Q7U+Ll+iek1 z3>rOBMX*TBLIa-x^V#|xR#X3Iup(H(Wfe;7FYfH%q=suM1Iy%k;(LyLOs3FzTYE>1 zf=FKBJRm4pI-6@siC&0i59euRn~qvuxJuu_*d>nd(#+@s?VgYm{k>_RSercDPF78d53rtbXwh&;Fx*)*~WAm}_xuL<%xFc01@ev0=Q1csU%zS9hP z9ZSXLTyKQJUoGlXU_Pi>d{8S5nx9VzUETSr-nwd%RX7q@@t)e{T}}K_>Y%wbimBz% zSq)ErQnm<=vGcAUj62mSn$0GNRhUhxqx;|)^bQ(ogr-I0IC^Z}MN>(2q_elu8Ek2V z$oU0nR;BBW4$b7bjE65qfFR-7*4t3)SI*XP5_#sl{w#xSBlQLToCWM4d6!3KoXqN3 z^C&nG6(7(DQZDQj??)g#B{&4&mL3Gj^0&zzu`;Hj4jJ!N?8crWeHrKGe z0h#REa!^l6eI#dr-qs7Dyma5+;wRRjQhsbwH^fFTC93>Q2}`)$Ktoj`Fnqi zPFz3LJfk&e8ZrI0pXrJ5T0|*qAO0Iez`G2s8U8uI@yC}201W>P9cYx!tg$}XU#dE`4IA1P67j!2o(Z>U`T!vRfIs0!HtzuR_v& zriuJ;YeO&6Huo{}`qweJzV#Yge~zr-OJV3|L+@abDO17z%@y)-$>i&eI|LoV*(8#q zyd~oN2?{P_VX-I4!y0`ThnhUU0CPdwf?@j`j5u(oSgP2EcYzZicj=lXj@+p}I%xjB z7)S&tqznE1>*hCLa{RxD4>|C|_jffRr&iDYJe&*$eES;m_&-1W`Rxxr?LWukKJd+W z=OX|6|3R_7d;@>}`x@cP&4qJ90wjX}Ivpk8Jfv=ri~;4p27)|pL&n!0@j$BnzeoQ1 z=n>0*U4)MM-^2ayx1jMGX((0=dv}Ml21Bmt>&0Pa5WaeRRKE%8cbTG%%=S1O1Jrnv z>&7H7%2zSTn5Od4>9wU)ve8~N-4A=aM5%u(!lZ@@xoKqvF)-9lGlU!?qflpuIGn=Z;R9cae z*$GKC+Z+a%yLgl^71kXgpUHg>BgHF8!p&20tXIXf=LFN|tmKho(>S0-9o{?jpxZRjG3bn&Rce`6;a2RPyR z#x>|ryAga*1!~H`wsdj``dizwdY)lz3x^9AvBu#}G%x95l~=>a9jB;-tvN`ZIK0M> zo^)HD>@Db-RCx9zc-@UL)92_FApbHU3^Y)mRP#4TG!3$^o?1RWT`biA6S0XM6N9)n zALmK+^^r=_-i_c$B=Oc$d!qkZ1cLG`V7!-{TY1+JO(yvigf4x}726vpW|4nm_}9jg z@h0se_0gJKi|Mp$U?gsQ9uEVh(&;@C?vT;7C9ZXilr*PeDy}~~Pml;+e{NR$B&5B0 z{_MV_wRIFKBYkZ`uvnrr^F`~TGVC?mUzRt8{62R9`Xd(Gg&<~MKg+Ct2eWqdylJEq z7}#6mT2DsC!S6%zM1I5~{QFq$S25>(>}h|RHB4ZdO4AZeFwwUlN5+bLoA*cE^t+ik zWg8-M==8`iSfndqs1TIU4wVR^-9ODq+T(jX+Ur|jg`mrT@I-RpzB-m_Vm?xo{wpxu z*k73^Y6!g_E-{fikFpIJu6Z^3gGmm2xi%-yZv#?Vdaz7<6V(}#t!Ee`XxRQpkD~R= zb0B$6fwDe7p4HuH z?s*e3bK>54di*8j5a}8<{==g!A~;^*E=g)FqIn#a2w z#d4jT)1rX}GOnR$`@0ETTRf>laG2dME;l)A85uoT&dnqEY?$r{Wd19~>0MAVzT<96 zn;;#HHjkjH(Pt-WMU|>IE$uVbk5SR~=n@ zCB1(3Mh-VVHa=<4H0o2{1T8gn7zwL(2KOB?>lQ`LqtgZ^p}D%M1I6-R zp~KK9Co~0!c#^F@|J}jRGv?&J(uIe{y(rb>{s_(c>sdWR_U~~^ruy_Aq2cFq%?St! z)f0XH(g`l!yEmWr;Q2$D8+9PO4tH5lT*U0!DY1+$tYlRGyz*Z%T*mDw;l z78bmV)gujYJqtG-{1x@<*9e-*0r971$Yu&Bxz^h9f|4z&=ft7|t0m>No~x2em4qdR z>(PqFC#YHe+?qH9WlICZmQEF4^F5WXyv<&Y(wN=ueIM=p8pBAQJXCb;G*~n>mAXKs zu(7dmpt!>#=FQo?)xJ*V+xzobxuA{;^W~3UDF*8N=B8Nr-1Y_SS$vK%zkfZCJz}8N z-F}C%&xdXwPl0ujhT=7%laO=NZoLcrsF2l{p?&?Rh5;NmR1_^7+6^8EC$UK3}L}U#Kc@S)4Wz2VbLqmNi&+1q2b}O z$^4QziW8HQwzEe+P7iDJe8of)biEZ~daIpa2lNSg2fN|7ca3KSR1y(n0-?dJ_Zzbc z1qJaN8-;~*gxxeWv@$6yA82D-bf5as(B#O{j_)PDza3}Bv0zW~IN7LV=x34f5EFaj z@|KQiXlSTn+M0rbB8FBjI3#4c)0B`0jq&YoAG{ipr7n%RG;B`(8-@CHH9M>rzY z_E_!$x7W#Zo$J3Vbv?N$doeer?>%!${0Tyvx!e)Nktp+YeEclqI@mS!8<@up#@QDy zSIh>JdRYcSUVhiAwbrV1&(JMPK;2vP)1RBFh@q8r=1m2UWwzepq%ubu9i`(t+Q#N4 zK07D7oRA+8PQk zuTVgSm9F=>++lTC`W5q}B?teo-9q*EU|P9fzbEspEG>O!8>D#!`zx~~a!g73NQ7*Q z#mCt;&0OVdwXLlq^A-iSm&0D^``<$(F9&$$K6JOg#E`2~`<3*Ym?(pUD8t&-!wrus zA#qNWosZ^kC)#cVm*lwA<|I#98n&-)rmfMzJ7|c~3w8>r>FZV5GtRW&2xUGda$-(K zg#@i#8C0e0Z}if#2ZJ7_7WT&;A-oYZg#CNWRk`1a0tsSsWDoSaA#U)vN5Y$6D{1Vi zEPfq~K5Ke!*0udYab*HC@ALHAGb7Ve%|ykvTH~3Rl0RdsJ-sn`^Kpw1qmkI^Ueg~Q zu%Y=M_{_=$%J*)hGQ+`Z%o~SIPylgQsan)G0UpyMh#>W13Vxppd!D9(|OD)P}`1de+Me!ZPP2*`u$M!$- zCg`tk{p-}3`91kPJ;6Oa&n(Q;&EJ+yyoeGMWp2T)n@x>!FjwCBR8S}yo03v}v?;qy zNa(aVFf`Qb&@hj$SFP+xylQB~VL6yl%urw~CDk{&I?dDIp%`?svy(&{V<9!OW(#W& zKtDi+JTH|@H8G)eODq`uy&*1M(A8-kpB-4jH)HJJK~+P7GoRwxC!g_Q{G6Icx9oLh ze;2{b9*l14@-s3HX=(LM?gsB?)BSa|$B_AN4{@KPxZ&(ETL)=siPhT}$>%}tZfv3K zU{THM`C(Z6<;SzA2KUjym~LsNG;5}c>4~GZ^-BMpKb3nRjd0m4RDvXge8POVf8p73 zhJnEre|Md1=fj8m;gCcBg~CVg6gw^2n9ie)y!?`aq(Bt5=9P9 z4i|Vq$Xhx(x-Rp%qkd-bEY^3dtUNpg9U*Ny#6SmvcK6<)^BY>wp z`g#t-64}ZHdE`qWl`N*O9$0!WOfg4uzR_0|?ujbT?@g^g>}_10GXdFqQ)frTXardxkB9aL?wrSQF#42IIU8OJw16eo zbhhzaKM9k-9>nSI6W&iND=Sj{{r&WGFIHA(=hhvYOc?qdwBNYs6Kn){I2Bo0_VxF@ z$0Vg=piA;=YaW-2n4dq10tw2@Y;$dMy}Pq>;@_k$i<{g<#H!QAmzPm^zsYwFzk1M8 zyuxVzGT!qQ?GL($(o5$kOP))k)hAJ!L#uenB>Xr9>(-jh=lTsL zgCFP^(<;Q7lo|VroTR!_(z3y;Cvrvm9vNv#8g^bmua!o+g=VH)m!fCG{61cDz$a0u zQa(f_5f8vkSpV4>RR=j{kuBOcl_o_fp~iz=(Z!UzRs+-8*3)p6ml9W8Zb1{dXcM^iDe?{*EZ15IvM zD>O?eDSvcQ)_AC@s6DM5F?{Xvllsk@^^J8>Zu`##pT)&etyOFINch|iUWhpWl#u%G z1NM`m2_hr*E`sph-d?RbJFa9#6p>%2*#T3>A1SFQ*GSZqmDwHIpDaF>mhLwU6Q>O2 zBtk<)O_9qh!>O;Y{|k+Fj38z;bl3gx!AMD|z76~3%a?zk0~Q&nuk=43A=X@(`$98= z(dTlWVZfFd2J<@DWIpCoD)#FL@nkM^(HqOu2RfY?+Ln9`c74UA$2L>$osv-vxY*xp zmhYNFle3p|yg`T$WBoQF=zSg{XC0KvgG^-#Z?ItYmJ##5?TY`l=xm)ao~xDq!q&Aq z`&QWSmQl@awIwq=cxRuiOnB9Z-IB-3(fupYbCS{0H=K|Zis|fL0iIfnC5D$`3QSlB zbs5u^6G2-6p3gd1TxhvEDs5CBn@&&mmZ}FD5agtUKU=P*Q!O$^j+)c$!uz8g$?BOK zQ`9cj*@@nfjn2b!<)p6ut9!Meur#Wok~K>LC#p@K+w<04M~C#y>K>+-tLxC{YIg^Q zK}JRfX3(mrK~$$<#f&w&f{ste{oM_+LJEfyO1FdPnlpE3sMJ5YxYK(?4W>U#q%<{?J(?fE!k^5}X5jBjLws_G)A_uW z`6#gQI8#iG6(mHXj(1aaqpDFSe{ZM<7L?b!;cCjrG2=|neYA+7S03yPqHE_!>D``J zvKAO!ij9{bdX+)P)nrvdR`&W8J9iujabALi6&_icdwpMPnM<*qLQMZYURJfEQE))8 zqa`U6SJVANZ0?w~8Nj3Hp3&xHJRKLju>FcRzkTmdT+5J8q+&BHO2O8 zQvaD&qx)#aK(WtL%%5)q3EMXY6JHX0Rn9wZgq@)M2))>w-_$~pEKrtmqj~cNHUTp; zHNhDb>yIy-*lDOT1Do(vlN%6d(%5MBm)cH+3;&srdb3-et%#w?r@Uo&_m0PV?O8~s zT%I&3kKxz(~i|e!h z+P3cchPrycko5QO%smaOz0O1oVZH4xVLf(aH%cZ^CL;nn7QHmx5B$moe9EJ>MpfBu zLvHo_b`CMS^FB4L$(WIcRv9@RRKb*_1(oClJM}Jg^}Q-{xS|Jf<>)WV5}w|?g=~>H z&F;IMx{yK|MR3IwaFIU%Eer`)s35bDw>3+DvLV3Ht0crM_!j?(V8bJp(O#a+#E1mG zozLGQLa=r_Plzo@mrSHz)memmA4(kDNcUZEzOS|r38a2{Gj&hsbC-^p_YdO3lpjeU zSVcOc@h?SZ_u20y9&6`nPgki=<&lN?g&~m~D4!3hn>uUs5SDe@o4H>ME?l0wh))H4&8tZu)1GXzDO;xUE-tAjyt;n~+t zv{*g*YP}n^7kKSY1d-WOI8=*js;j*LwizE{_8~$_%RayI{KTDz!u?WUO7xX`9=dao ztvr>PghbKTq|ZBZY)s-MUAQBV7RZBQ5~9#&8nk5pBX0=d?f5mgSkAIKlr28Ehd#QK zB8}^H70&!~)-&U2~F z=@}2iy&x~;(yTIFj;r0-5syw$wbdUz!|hkJ43H|&pB+Z*`6(*e;aM>lhKm*3y_(Ov zQiykAr;a8a(Wyg8U~<|{basDr=L~-)NU(@JB0Y5LMATz6>XU`>Jxn8sC|3o&x=N!( zs+O7~wnO8QFr2_#j}X!#HN;A_8Y{&k?K{dfC7Q8ju1@`9X!xm_3`yly6G45eiTq~EaLXzarF%BZH!&6(v9bxo2)s3bjE z#tas?)JDDZpB1_3JK!_IqpM~`0%N@mYUz*kv3FcyA+}+BTa`ucuRqq|lrEPh<5s*I z2J+ALZevQp<>$_TWzTFqf8Fm7N}QZHl~KLrkIz;XO?qXNi!SEzG}~{&OnO#nnzWSr z6Z03yFVM_nv2yifjFCsuQx9I^>tkcM;^>1LF5o^NV-0NP-TcB<)baAK<=eX;%^^A2 zksR@&LpbWl>;traCq680UeL$zOVjN4TFXCkDoTZ}=xC});W2b|25AE#S|;@Qw5JJa zsklTT_`k_5PukFpDd}Zy=A;QS|Gk|Uv0u-Snmr;4>h>D)Hls7%GtaVgOmThfvWBuX z{4l;eQ&zS;XPfY=AaloHTV{`zzi8CKYdD{}qE$r z`uEl@5fVBRkT%RhP2S3Me6z5pWD9m?zQZKtb)GENsQAypn_3F9d5+4LmFec8Xk*A73*)1;z4$7d$*9WP9Bc=}$wb$#jl% zLIg3F?O+ne`ue)f!9Mk4Dze9rCulY}5XgsToOdrqPq)XTe35=jKY#vwJWn2eSZeEi z0%{3CU=ksof3}A{1Yr=@8uvz5>5@XY-!N)aQd3Z5bTnZ*gI_-Zy0-A|Mu$hdP8%}L zpP%?cSkd46-QVhyf42sA#r;Gd;{Sf4ciPO*aAmp#4U#g;OUU}=HyXr#<3GdWh$9{e ze?H7Xf{YYByz$7fu{L37*7%>LOWDLT2+Hdg-NoLP`5Q`5-XMP%Y#~5N5weBi?E>U# zvWGWHU9u*S^U5J3HuIOZZElzl;nxoXJskR}*$lRw4$kuZ6?8n$!K zvVEaSA;@~dP(Tg7MyO`FHfqP476nQ0v7GX!0Md%pu7>)gr{e^?*}pz%$?l_r@9!JF zyNe1*v0+POXva3K;v-6yc_>Hh{NefC@p5E5J#NJs4>I)0#AUF6?-%9|R+*YhJ9 zl5MA-vH( zIg$s-{f@We8MS^$OYuZVOG#<7AOft^ii0B}c6JNTvVkyXSLVwlKs9e#MVytjIhg7+ zJnz=o*?I2L@~Hjf)@`v&t8O_cB}F@-KMtE*$Ym#%xD@Jony=uaQ}c>5*=H>FB-p%n zMa=Ggv!=CF?&+l6B?T8VJW5DtPBIZ+*V9CKr>CE(CEF}r6Swn^#5sxz0Ytz219*`B z?<>vV3s1>Cr;OQd&PfD3_qIljtQHVU-#62An#~Q;Cz(FOORoOd)=7wqFWBGJkiP2S z^^ z8Xp~<=8)f&MBYQ3M+Z4-LClU!8R8ohdb!@S*J)=HbR znEm%JOxMTawjOYVBX_~;%NP(18n~STUKi8*uHoY2Fb;vIbFRCLqNk@-!}PeJ-H3ax z#d|Qzx>8U@u;RDsu5X4pA;M5L_axWH6;>Aq0Rb-+D?N7y5&09A7JlaDTR(<{hzSWj zdOQ2G>%~L^UVC3pZ|eEn?T55~tkHZ>EV1VN7JrR0KK^VroT{g!l^h$(Zg-H9&UtUm znJkf$D|i<9hE|S1UKEI`XbB8dnOFD&F}=oK=Lct=-y57vh0fMq-gku6!7oh)pOTaD zIL2;X0bLeWT)&fqr*=a$7#bS7=;?Jfq|=m~oD4u#f}ZcRP2F7a=eGy-yJ?HA2hn|N zYa0;+9Pz>TCo}Z@@k~dg8U@O9aZ(BWDoR>6$3(t>s?;o5^u8g*aUi*v>_c37TJ^6VHyt$V0P-X*&Y;!!&EVJ9Z9>?h`+JVSa;`=$W;X;)nnB8SYVE_k`rk)R98 zf^L1)grd%?7%=J7Z-b^Mv+(97ht=GqnVI0=;P3cM?n|v+R8&+x>#&l=!}A?C>HfaJ z-0V4*&HdIp+8B@X<}0QIdS2cg*uItt8zBw{7uWDFf#G$>??FL`TBD+m8C#R-`-zSF zlUmwL7t&?TQlg3|z91fiZnsvU{I_|6l2CI(6v#8whb<0^n4iJ}@x|FyDI~4LkcVUB3=D_%f9QCOMCyvi9wk9yvwk z-3`{93)}}YNTiWV2)fjnpgY~{(H{gad%hM|$>23jgD?HX^^6bRdb5zE@!o9C2S)mS zNJx+Fvx+S-2pU#yNUU_^h zLGBe0JY&2hlBhuz85I1AvAO(CeO=48mGzL9|75@^-M3YBrZhia8{F#UWxZSy9|1XE5DywQV&=xPV*3Tq)Orw{+fkXy)az1UcgV%MFO0QZhfv zZ(3k)3o`i$f$(y=J6hLqaBxTx1K5(9aO=H~uXxQNF0ts*Z3C25nVs)9FbQ%lkWy(j z^;qC{7YhwX*SpyvKNb;(S)4(Qo27h0>L&-)C%ICL9YS_946;o$Ig3By8Sie#5v>

    ENlL8m_!un$EDI*h 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 Date: Tue, 25 Jun 2024 10:12:57 -0500 Subject: [PATCH 20/22] test: add e2e for scenario where `wallet_switchEthereumChain` call is canceled with other confirmations queued behind it (#25341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Add an e2e to catch regressions similar to the bug identified in[ this patch/cherry-pick fix](https://github.com/MetaMask/metamask-extension/pull/25339) to the v12.0.0 RC branch We had an e2e for a similar flow but where the `wallet_switchEthereumChain` confirmation was confirmed rather than cancelled. The bug occurred when the confirmation was cancelled and so this test handles that case. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25341?quickstart=1) ## **Related issues** See: https://consensys.slack.com/archives/CTQAGKY5V/p1718385169900809?thread_ts=1718140104.578969&cid=CTQAGKY5V ## **Manual testing steps** N/A ## **Screenshots/Recordings** N/A ## **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. --- .../dapp1-switch-dapp2-send.spec.js | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) 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 02f496f68235..6cab2f88d96b 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 @@ -12,7 +12,7 @@ const { } = require('../../helpers'); describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { - it('should queue send tx after switch network confirmation.', async function () { + it('should queue send tx after switch network confirmation and transaction should target the correct network after switch is confirmed', async function () { const port = 8546; const chainId = 1338; await withFixtures( @@ -165,4 +165,158 @@ describe('Request Queuing Dapp 1, Switch Tx -> Dapp 2 Send Tx', function () { }, ); }); + + it('should queue send tx after switch network confirmation and transaction should target the correct network after switch is cancelled.', async function () { + const port = 8546; + const chainId = 1338; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port, + chainId, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: 7777, + chainId: 1000, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // Open Dapp One + 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: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // Network Selector + await driver.clickElement('[data-testid="network-display"]'); + + // Switch to second network + await driver.clickElement({ + text: 'Localhost 8546', + css: 'p', + }); + + // Wait for the first dapp's connect confirmation to disappear + await driver.waitUntilXWindowHandles(2); + + // TODO: Request Queuing bug when opening both dapps at the same time will have them stuck on the same network, with will be incorrect for one of them. + // Open Dapp Two + await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect to dapp 2 + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await switchToNotificationWindow(driver, 4); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.switchToWindowWithUrl(DAPP_URL); + + // switchEthereumChain request + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], + }); + + // Initiate switchEthereumChain on Dapp Two + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + await driver.clickElement('#sendButton'); + + await switchToNotificationWindow(driver, 4); + await driver.findClickableElements({ + text: 'Cancel', + tag: 'button', + }); + + await driver.clickElement({ text: 'Cancel', tag: 'button' }); + + // Wait for switch confirmation to close then tx confirmation to show. + await driver.waitUntilXWindowHandles(3); + + await switchToNotificationWindow(driver, 4); + + // Check correct network on the send confirmation. + await driver.findElement({ + css: '[data-testid="network-display"]', + text: 'Localhost 8546', + }); + + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + + // Switch back to the extension + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + await driver.clickElement( + '[data-testid="account-overview__activity-tab"]', + ); + + // Check for transaction + await driver.wait(async () => { + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .activity-list-item', + ); + return confirmedTxes.length === 1; + }, 10000); + }, + ); + }); }); From 7add8acc8244f185a2462f14f368d931d16783bc Mon Sep 17 00:00:00 2001 From: Bilal <44588480+BZahory@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:51:40 -0400 Subject: [PATCH 21/22] fix: do not use STX for swap+send approval (#25510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Paired w/ @dan437 and @forest-diggs-consensys We removed STX functionality for Swap+Send in #25422. However, we still get the STX flow because the `swapApproval` type we use is still routed as an STX. Since only Swap+Send uses the `swapApproval` type for STXs routed through `submitSmartTransactionHook`, we can exclude it in the same way we excluded the `swapAndSend` type in #25422. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25510?quickstart=1) ## **Related issues** Fixes: #25356 ## **Manual testing steps** 1. Enable Smart Transactions in settings 2. Switch to Ethereum Mainnet 3. Submit a swap+send transaction that requires an approval (i.e. from an ERC-20 w/o an existing approval) 4. Ensure that the STX screen does not show after submitting 5. Ensure that the contract interaction isn't marked as "unapproved" ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-extension/assets/44588480/d3fb13ca-4aa0-43a8-8689-37f7e49af10a ### **After** https://github.com/MetaMask/metamask-extension/assets/44588480/d34ab118-ed69-4418-8c35-06774099e092 ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/scripts/lib/transaction/smart-transactions.test.ts | 7 +++++++ app/scripts/lib/transaction/smart-transactions.ts | 8 ++++++-- ui/ducks/send/send.js | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/scripts/lib/transaction/smart-transactions.test.ts b/app/scripts/lib/transaction/smart-transactions.test.ts index 04c029c3ef8b..eed52a909f67 100644 --- a/app/scripts/lib/transaction/smart-transactions.test.ts +++ b/app/scripts/lib/transaction/smart-transactions.test.ts @@ -141,6 +141,13 @@ describe('submitSmartTransactionHook', () => { expect(result).toEqual({ transactionHash: undefined }); }); + it('falls back to regular transaction submit if the transaction type is "swapApproval"', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + request.transactionMeta.type = TransactionType.swapApproval; + const result = await submitSmartTransactionHook(request); + expect(result).toEqual({ transactionHash: undefined }); + }); + it('falls back to regular transaction submit if /getFees throws an error', async () => { const request: SubmitSmartTransactionRequestMocked = createRequest(); jest diff --git a/app/scripts/lib/transaction/smart-transactions.ts b/app/scripts/lib/transaction/smart-transactions.ts index 97e37965d6ea..21a95e94494d 100644 --- a/app/scripts/lib/transaction/smart-transactions.ts +++ b/app/scripts/lib/transaction/smart-transactions.ts @@ -121,8 +121,12 @@ class SmartTransactionHook { } async submit() { - const isUnsupportedTransactionTypeForSmartTransaction = - this.#transactionMeta?.type === TransactionType.swapAndSend; + const isUnsupportedTransactionTypeForSmartTransaction = this + .#transactionMeta?.type + ? [TransactionType.swapAndSend, TransactionType.swapApproval].includes( + this.#transactionMeta.type, + ) + : false; // Will cause TransactionController to publish to the RPC provider as normal. const useRegularTransactionSubmit = { transactionHash: undefined }; diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index ce7ddc4e9bfe..50039e02f650 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -3017,6 +3017,7 @@ export function signTransaction(history) { { ...bestQuote.approvalNeeded, amount: '0x0' }, { requireApproval: false, + // TODO: create new type for swap+send approvals; works as stopgap bc swaps doesn't use this type for STXs in `submitSmartTransactionHook` (via `TransactionController`) type: TransactionType.swapApproval, swaps: { hasApproveTx: true, From 2cd5813f45421df82815b95438eafad1c4014a30 Mon Sep 17 00:00:00 2001 From: Bilal <44588480+BZahory@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:33:59 -0400 Subject: [PATCH 22/22] perf: use global token list in hook (#25501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The `useIsOriginalTokenSymbol` is meant to check a given token's symbol – one that can be overridden by the user – against the true token symbol. We currently fetch this token symbol using the `AssetsContractController`'s `getTokenSymbol` function which calls its sister `getTokenStandardAndDetails` function, which uses the network's provider (e.g., Infura) to query that token's on-chain metadata. Because this hook is rendered (i.e. the blockchain/node is queried) once for each token on each render, users can get rate-limited relatively quickly; this is much more likely for users that import a large amount of tokens or spend a long time in the extension. The solution is to prefer the token list saved in the global state over the API. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/25501?quickstart=1) ## **Related issues** Fixes: #24859 ## **Manual testing steps** 1. Go to the home page 2. Open the "networks" tab in dev tools; add a filter for that network's provider (e.g., "infura" for Mainnet, assuming default network settings) 3. Switch back and forth between the NFTs/Activity tab and the Tokens tab 4. Ensure we aren't violating the RPC provider API with requests every time the token tab loads ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-extension/assets/44588480/5b2c797d-9c77-437a-b50f-dc8126177b32 ### **After** https://github.com/MetaMask/metamask-extension/assets/44588480/9dc3cffd-87a6-43c4-bc79-a4116095626d ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/hooks/useIsOriginalTokenSymbol.js | 31 +++++++++++++++- ui/hooks/useIsOriginalTokenSymbol.test.js | 45 ++++++++++++++++++++--- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/ui/hooks/useIsOriginalTokenSymbol.js b/ui/hooks/useIsOriginalTokenSymbol.js index 332c28a1cde8..8b12c118bf95 100644 --- a/ui/hooks/useIsOriginalTokenSymbol.js +++ b/ui/hooks/useIsOriginalTokenSymbol.js @@ -1,17 +1,44 @@ +// TODO: reconsider this approach altogether +// checking against on-chain data to see if a user has changed a token symbol is not ideal +// we should just keep track of the original symbol in state, or better yet, rely on the address instead of the symbol +// see: https://github.com/MetaMask/metamask-extension/pull/21610 (original PR) + import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { getTokenSymbol } from '../store/actions'; +import { getTokenList } from '../selectors'; +/** + * This hook determines whether a token uses the original symbol based on data not influenced by the user. + * + * @param {string} tokenAddress - the address of the token + * @param {string} tokenSymbol - the local symbol of the token + * @returns a boolean indicating whether the token uses the original symbol + */ export function useIsOriginalTokenSymbol(tokenAddress, tokenSymbol) { const [isOriginalNativeSymbol, setIsOriginalNativeSymbol] = useState(null); + const tokens = useSelector(getTokenList); + useEffect(() => { async function getTokenSymbolForToken(address) { - const symbol = await getTokenSymbol(address); + // attempt to fetch from cache first + let trueSymbol = tokens[address?.toLowerCase()]?.symbol; + + // if tokens aren't available, fetch from the blockchain + if (!trueSymbol) { + trueSymbol = await getTokenSymbol(address); + } + + // if the symbol is the same as the tokenSymbol, it's the original setIsOriginalNativeSymbol( - symbol?.toLowerCase() === tokenSymbol?.toLowerCase(), + trueSymbol?.toLowerCase() === tokenSymbol?.toLowerCase(), ); } + getTokenSymbolForToken(tokenAddress); + // no need to wait for tokens to load, since we'd fetch without them if they aren't available + // eslint-disable-next-line react-hooks/exhaustive-deps }, [tokenAddress, tokenSymbol]); return isOriginalNativeSymbol; diff --git a/ui/hooks/useIsOriginalTokenSymbol.test.js b/ui/hooks/useIsOriginalTokenSymbol.test.js index afac3bf650b2..2952e15ff461 100644 --- a/ui/hooks/useIsOriginalTokenSymbol.test.js +++ b/ui/hooks/useIsOriginalTokenSymbol.test.js @@ -1,5 +1,8 @@ -import { renderHook, act } from '@testing-library/react-hooks'; +import { act } from '@testing-library/react-hooks'; import * as actions from '../store/actions'; +import mockState from '../../test/data/mock-state.json'; + +import { renderHookWithProvider } from '../../test/lib/render-helpers'; import { useIsOriginalTokenSymbol } from './useIsOriginalTokenSymbol'; // Mocking the getTokenSymbol function @@ -7,6 +10,17 @@ jest.mock('../store/actions', () => ({ getTokenSymbol: jest.fn(), })); +const state = { + metamask: { + ...mockState.metamask, + tokenList: { + '0x1234': { + symbol: 'ABCD', + }, + }, + }, +}; + describe('useIsOriginalTokenSymbol', () => { it('useIsOriginalTokenSymbol returns correct value when token symbol matches', async () => { const tokenAddress = '0x123'; @@ -17,8 +31,9 @@ describe('useIsOriginalTokenSymbol', () => { let result; await act(async () => { - result = renderHook(() => - useIsOriginalTokenSymbol(tokenAddress, tokenSymbol), + result = renderHookWithProvider( + () => useIsOriginalTokenSymbol(tokenAddress, tokenSymbol), + state, ); }); @@ -35,12 +50,32 @@ describe('useIsOriginalTokenSymbol', () => { let result; await act(async () => { - result = renderHook(() => - useIsOriginalTokenSymbol(tokenAddress, tokenSymbol), + result = renderHookWithProvider( + () => useIsOriginalTokenSymbol(tokenAddress, tokenSymbol), + state, ); }); // Expect the hook to return false when the symbol matches the original symbol expect(result.result.current).toBe(false); }); + + it('useIsOriginalTokenSymbol uses cached value when available', async () => { + const tokenAddress = '0x1234'; + const tokenSymbol = 'ABCD'; + + actions.getTokenSymbol.mockResolvedValue('Should not matter'); // Mock the getTokenSymbol function + + let result; + + await act(async () => { + result = renderHookWithProvider( + () => useIsOriginalTokenSymbol(tokenAddress, tokenSymbol), + state, + ); + }); + + // Expect the hook to return true when the symbol matches the original symbol + expect(result.result.current).toBe(true); + }); });