diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e6c7de55169..9f9d3aacfbbf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -53,6 +53,13 @@ develop_master_rc_only: &develop_master_rc_only - master - /^Version-v(\d+)[.](\d+)[.](\d+)/ +exclude_develop_master_rc: &exclude_develop_master_rc + filters: + branches: + ignore: + - develop + - master + - /^Version-v(\d+)[.](\d+)[.](\d+)/ aliases: # Shallow Git Clone - &shallow-git-clone @@ -95,6 +102,11 @@ aliases: workflows: test_and_release: + when: + not: + matches: + pattern: /^l10n_crowdin_action$/ + value: << pipeline.git.branch >> jobs: - create_release_pull_request: <<: *rc_branch_only @@ -105,6 +117,10 @@ workflows: - prep-deps - check-pr-tag - prep-deps + - get-changed-files-with-git-diff: + <<: *exclude_develop_master_rc + requires: + - prep-deps - test-deps-audit: requires: - prep-deps @@ -147,11 +163,7 @@ workflows: - prep-build-test-mv2: requires: - prep-deps - - prep-build-confirmation-redesign-test: - requires: - - prep-deps - - prep-build-confirmation-redesign-test-mv2: - <<: *develop_master_rc_only + - prep-build-test-webpack: requires: - prep-deps - prep-build-test-flask: @@ -184,31 +196,33 @@ workflows: - test-lint-changelog: requires: - prep-deps + - test-e2e-chrome-webpack: + requires: + - prep-build-test-webpack + - get-changed-files-with-git-diff - test-e2e-chrome: requires: - prep-build-test - - test-e2e-chrome-confirmation-redesign: - requires: - - prep-build-confirmation-redesign-test + - get-changed-files-with-git-diff - test-e2e-firefox: requires: - prep-build-test-mv2 - - test-e2e-firefox-confirmation-redesign: - <<: *develop_master_rc_only - requires: - - prep-build-confirmation-redesign-test-mv2 + - get-changed-files-with-git-diff - test-e2e-chrome-rpc: requires: - prep-build-test + - get-changed-files-with-git-diff - test-api-specs: requires: - prep-build-test - test-e2e-chrome-multiple-providers: requires: - prep-build-test + - get-changed-files-with-git-diff - test-e2e-chrome-flask: requires: - prep-build-test-flask + - get-changed-files-with-git-diff - test-e2e-firefox-flask: <<: *develop_master_rc_only requires: @@ -216,12 +230,14 @@ workflows: - test-e2e-chrome-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-mmi-playwright - OPTIONAL: requires: - prep-build-test-mmi-playwright - test-e2e-chrome-rpc-mmi: requires: - prep-build-test-mmi + - get-changed-files-with-git-diff - test-e2e-chrome-vault-decryption: filters: branches: @@ -230,19 +246,6 @@ workflows: - /^Version-v(\d+)[.](\d+)[.](\d+)/ requires: - prep-build - - test-unit-jest-main: - requires: - - prep-deps - - test-unit-jest-development: - requires: - - prep-deps - - upload-coverage: - requires: - - test-unit-jest-main - - test-unit-jest-development - - test-unit-global: - requires: - - prep-deps - test-storybook: requires: - prep-deps @@ -283,10 +286,6 @@ workflows: - test-lint-shellcheck - test-lint-lockfile - test-lint-changelog - - test-unit-jest-main - - test-unit-jest-development - - test-unit-global - - upload-coverage - validate-source-maps - validate-source-maps-beta - validate-source-maps-flask @@ -295,14 +294,13 @@ workflows: - test-mozilla-lint-flask-mv2 - test-e2e-chrome - test-e2e-chrome-multiple-providers - - test-e2e-chrome-confirmation-redesign - - test-e2e-firefox-confirmation-redesign - test-e2e-firefox - test-e2e-chrome-flask - test-e2e-firefox-flask - test-e2e-chrome-mmi - test-e2e-chrome-rpc-mmi - test-e2e-chrome-vault-decryption + - test-e2e-chrome-webpack - test-storybook - benchmark: requires: @@ -353,9 +351,30 @@ workflows: requires: - prep-build-ts-migration-dashboard + locales_only: + when: + matches: + pattern: /^l10n_crowdin_action$/ + value: << pipeline.git.branch >> + jobs: + - prep-deps + - get-changed-files-with-git-diff: + requires: + - prep-deps + - validate-locales-only: + requires: + - get-changed-files-with-git-diff + - test-lint: + requires: + - prep-deps + - all-tests-pass: + requires: + - test-lint + - validate-locales-only + jobs: trigger-beta-build: - executor: node-browsers-medium-plus + executor: node-browsers-small steps: - run: *shallow-git-clone - run: sudo corepack enable @@ -370,8 +389,7 @@ jobs: steps: - run: name: Build beta prod - command: | - .circleci/scripts/trigger-beta-build.sh + command: .circleci/scripts/trigger-beta-build.sh - run: name: Move beta build to 'dist-beta' to avoid conflict with production build command: mv ./dist ./dist-beta @@ -444,8 +462,7 @@ jobs: - run: sudo corepack enable - run: name: Save Yarn version - command: | - yarn --version > /tmp/YARN_VERSION + command: yarn --version > /tmp/YARN_VERSION - restore_cache: keys: # First try to get the specific cache for the checksum of the yarn.lock file. @@ -472,6 +489,33 @@ jobs: - node_modules - build-artifacts + # This job is used for the e2e quality gate. + # It must be run before any job which uses the run-all.js script. + # The job is skipped in develop, master or RC branches. + get-changed-files-with-git-diff: + executor: node-browsers-small + steps: + - run: *shallow-git-clone + - run: sudo corepack enable + - attach_workspace: + at: . + - run: + name: Get changed files with git diff + command: npx tsx .circleci/scripts/git-diff-develop.ts + - persist_to_workspace: + root: . + paths: + - changed-files + + validate-locales-only: + executor: node-browsers-small + steps: + - run: *shallow-git-clone + - run: sudo corepack enable + - attach_workspace: + at: . + - run: yarn tsx .circleci/scripts/validate-locales-only.ts + validate-lavamoat-allow-scripts: executor: node-browsers-small steps: @@ -866,49 +910,25 @@ jobs: - dist-test-mv2 - builds-test-mv2 - prep-build-confirmation-redesign-test: + prep-build-test-webpack: executor: node-linux-medium steps: - run: *shallow-git-clone - - run: corepack enable - attach_workspace: at: . - run: - name: Build extension for testing - command: ENABLE_CONFIRMATION_REDESIGN="true" yarn build:test - - run: - name: Move test build to 'dist-test' to avoid conflict with production build - command: mv ./dist ./dist-test-confirmations - - run: - name: Move test zips to 'builds-test' to avoid conflict with production build - command: mv ./builds ./builds-test-confirmations - - persist_to_workspace: - root: . - paths: - - dist-test-confirmations - - builds-test-confirmations - - prep-build-confirmation-redesign-test-mv2: - executor: node-linux-medium - steps: - - run: *shallow-git-clone - - run: corepack enable - - attach_workspace: - at: . + name: Activate yarn + command: corepack enable - run: name: Build extension for testing - command: ENABLE_CONFIRMATION_REDESIGN="true" yarn build:test:mv2 - - run: - name: Move test build to 'dist-test-confirmations-mv2' to avoid conflict with production build - command: mv ./dist ./dist-test-confirmations-mv2 + command: yarn build:test:webpack - run: - name: Move test zips to 'builds-test-confirmations-mv2' to avoid conflict with production build - command: mv ./builds ./builds-test-confirmations-mv2 + name: Move test build to 'dist-test-webpack' to avoid conflict with production build + command: mv ./dist ./dist-test-webpack - persist_to_workspace: root: . paths: - - dist-test-confirmations-mv2 - - builds-test-confirmations-mv2 + - dist-test-webpack prep-build-storybook: executor: node-linux-medium @@ -1051,6 +1071,27 @@ jobs: name: depcheck command: yarn depcheck + test-e2e-chrome-webpack: + executor: node-browsers-medium-plus + parallelism: 20 + steps: + - run: *shallow-git-clone + - run: sudo corepack enable + - attach_workspace: + at: . + - run: + name: Move test build to dist + command: mv ./dist-test-webpack ./dist + - run: + name: test:e2e:chrome:webpack + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:webpack + no_output_timeout: 5m + - store_artifacts: + path: test-artifacts + destination: test-artifacts + - store_test_results: + path: test/test-results/e2e + test-api-specs: executor: node-browsers-medium-plus steps: @@ -1067,8 +1108,7 @@ jobs: - gh/install - run: name: test:api-specs - command: | - timeout 20m yarn test:api-specs --retries 2 + command: .circleci/scripts/test-run-e2e.sh yarn test:api-specs no_output_timeout: 5m - run: name: Comment on PR @@ -1099,42 +1139,8 @@ jobs: command: mv ./builds-test ./builds - run: name: test:e2e:chrome - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome --retries 1 - fi - no_output_timeout: 5m - - store_artifacts: - path: test-artifacts - destination: test-artifacts - - store_test_results: - path: test/test-results/e2e - - test-e2e-chrome-confirmation-redesign: - executor: node-browsers-medium-plus - parallelism: 20 - steps: - - run: *shallow-git-clone - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: Move test build to dist - command: mv ./dist-test-confirmations ./dist - - run: - name: Move test zips to builds - command: mv ./builds-test-confirmations ./builds - - run: - name: test:e2e:chrome-confirmation-redesign - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome:confirmation-redesign --retries 1 - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome no_output_timeout: 5m - environment: - ENABLE_CONFIRMATION_REDESIGN: "true" - store_artifacts: path: test-artifacts destination: test-artifacts @@ -1157,11 +1163,7 @@ jobs: command: mv ./builds-test ./builds - run: name: test:e2e:chrome:rpc - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome:rpc --retries 1 - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:rpc no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1185,11 +1187,7 @@ jobs: command: mv ./builds-test ./builds - run: name: test:e2e:chrome:multi-provider - command: | - if .circleci/scripts/test-run-e2e.sh - then - yarn test:e2e:chrome:multi-provider --retries 1 - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:multi-provider no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1212,11 +1210,7 @@ jobs: command: mv ./builds-test-mmi ./builds - run: name: test:e2e:chrome:rpc - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome:rpc --retries 1 --build-type=mmi - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:rpc --build-type=mmi no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1232,12 +1226,8 @@ jobs: - attach_workspace: at: . - run: - name: test:e2e:chrome:vault - command: | - if .circleci/scripts/test-run-e2e.sh - then - yarn test:e2e:single test/e2e/vault-decryption-chrome.spec.js --browser chrome --retries 1 - fi + name: test:e2e:single + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:single test/e2e/vault-decryption-chrome.spec.js --browser chrome no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1261,12 +1251,7 @@ jobs: command: mv ./builds-test-flask-mv2 ./builds - run: name: test:e2e:firefox:flask - command: | - export ENABLE_MV3=false - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:firefox:flask --retries 1 - fi + command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox:flask no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1290,11 +1275,7 @@ jobs: command: mv ./builds-test-flask ./builds - run: name: test:e2e:chrome:flask - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome:flask --retries 1 - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:flask no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1318,11 +1299,7 @@ jobs: command: mv ./builds-test-mmi ./builds - run: name: test:e2e:chrome:mmi - command: | - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:chrome:mmi --retries 1 --build-type=mmi - fi + command: .circleci/scripts/test-run-e2e.sh yarn test:e2e:chrome:mmi --build-type=mmi no_output_timeout: 5m - store_artifacts: path: test-artifacts @@ -1344,14 +1321,13 @@ jobs: command: mv ./dist-test-mmi-playwright ./dist - run: name: Install chromium - command: | - yarn playwright install chromium + command: yarn playwright install chromium - run: name: test:e2e:chrome:mmi command: | - TESTFILES=$(circleci tests glob "test/e2e/mmi/**/*.spec.ts") + TESTFILES=$(circleci tests glob "test/e2e/playwright/mmi/**/*.spec.ts") echo "$TESTFILES" - echo "$TESTFILES" | timeout 20m circleci tests run --command="xvfb-run xargs yarn playwright test" verbose --split-by=timings + echo "$TESTFILES" | timeout 20m circleci tests run --command="xvfb-run xargs yarn playwright test --project=mmi" verbose --split-by=timings no_output_timeout: 10m - slack/notify: branch_pattern: Version-v* @@ -1372,38 +1348,39 @@ jobs: name: report for pipeline integration path: public/playwright/playwright-reports/junit/test-results.xml - test-e2e-firefox: - executor: node-browsers-medium-plus - parallelism: 24 + test-e2e-swap-playwright - OPTIONAL: + executor: playwright + parallelism: 2 steps: - run: *shallow-git-clone - - run: sudo corepack enable + - run: corepack enable - attach_workspace: at: . - run: - name: Move test build to dist - command: mv ./dist-test-mv2 ./dist - - run: - name: Move test zips to builds - command: mv ./builds-test-mv2 ./builds + name: Install chromium + command: yarn playwright install chromium - run: - name: test:e2e:firefox + name: test:e2e:chrome:swap command: | - export ENABLE_MV3=false - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:firefox --retries 1 - fi - no_output_timeout: 5m + TESTFILES=$(circleci tests glob "test/e2e/playwright/swap/**/*.spec.ts") + echo "$TESTFILES" + echo "$TESTFILES" | timeout 20m circleci tests run --command="xvfb-run xargs yarn playwright test --project=swap" verbose + no_output_timeout: 10m + - slack/notify: + event: fail + mentions: <@davide.brocchetto> + template: basic_fail_1 + channel: C06BEG71R1S - store_artifacts: - path: test-artifacts - destination: test-artifacts + name: html-report and artifacts + path: public/playwright/playwright-reports - store_test_results: - path: test/test-results/e2e + name: report for pipeline integration + path: public/playwright/playwright-reports/junit/test-results.xml - test-e2e-firefox-confirmation-redesign: + test-e2e-firefox: executor: node-browsers-medium-plus - parallelism: 20 + parallelism: 24 steps: - run: *shallow-git-clone - run: sudo corepack enable @@ -1411,21 +1388,14 @@ jobs: at: . - run: name: Move test build to dist - command: mv ./dist-test-confirmations-mv2 ./dist + command: mv ./dist-test-mv2 ./dist - run: name: Move test zips to builds - command: mv ./builds-test-confirmations-mv2 ./builds + command: mv ./builds-test-mv2 ./builds - run: - name: test:e2e:firefox-confirmation-redesign - command: | - export ENABLE_MV3=false - if .circleci/scripts/test-run-e2e.sh - then - timeout 20m yarn test:e2e:firefox --retries 1 - fi + name: test:e2e:firefox + command: ENABLE_MV3=false .circleci/scripts/test-run-e2e.sh yarn test:e2e:firefox no_output_timeout: 5m - environment: - ENABLE_CONFIRMATION_REDESIGN: "true" - store_artifacts: path: test-artifacts destination: test-artifacts @@ -1559,9 +1529,6 @@ jobs: path: builds-test - store_artifacts: path: builds-test-flask - - store_artifacts: - path: coverage - destination: coverage - store_artifacts: path: test-artifacts destination: test-artifacts @@ -1583,12 +1550,6 @@ jobs: command: | echo "export PARENT_COMMIT=$(git merge-base origin/HEAD HEAD)" >> $BASH_ENV source $BASH_ENV - - run: - name: Set commit message env var - command: | - commit_title=$(git show -s --format='%s' HEAD) - echo "export SHA1_COMMIT_TITLE=\"$commit_title\"" >> $BASH_ENV - source $BASH_ENV - run: name: build:announce command: ./development/metamaskbot-build-announce.js @@ -1617,8 +1578,7 @@ jobs: command: yarn sentry:publish --build-type mmi - run: name: Create GitHub release - command: | - .circleci/scripts/release-create-gh-release.sh + command: .circleci/scripts/release-create-gh-release.sh job-publish-storybook: executor: node-browsers-small @@ -1654,65 +1614,6 @@ jobs: git config user.email metamaskbot@users.noreply.github.com yarn ts-migration:dashboard:deploy - test-unit-jest-development: - executor: node-browsers-small - steps: - - run: *shallow-git-clone - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: jest development unit tests - command: yarn test:coverage:jest:dev - - persist_to_workspace: - root: . - paths: - - coverage - - store_test_results: - path: test/test-results/junit.xml - - test-unit-jest-main: - executor: node-browsers-medium-plus - parallelism: 8 - steps: - - run: *shallow-git-clone - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: test:coverage:jest - command: yarn test:coverage:jest - - persist_to_workspace: - root: . - paths: - - coverage - - store_test_results: - path: test/test-results/junit.xml - - upload-coverage: - executor: node-browsers-small - steps: - - run: *shallow-git-clone - - run: sudo corepack enable - - attach_workspace: - at: . - - codecov/upload - - persist_to_workspace: - root: . - paths: - - coverage - - test-unit-global: - executor: node-browsers-small - steps: - - run: *shallow-git-clone - - run: sudo corepack enable - - attach_workspace: - at: . - - run: - name: test:unit:global - command: yarn test:unit:global - validate-source-maps: executor: node-browsers-small steps: @@ -1733,8 +1634,7 @@ jobs: at: . - run: name: Validate source maps - command: | - .circleci/scripts/validate-source-maps-beta.sh + command: .circleci/scripts/validate-source-maps-beta.sh validate-source-maps-mmi: executor: node-browsers-small diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts new file mode 100644 index 000000000000..bca1bdeec143 --- /dev/null +++ b/.circleci/scripts/git-diff-develop.ts @@ -0,0 +1,99 @@ +import { hasProperty } from '@metamask/utils'; +import { exec as execCallback } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; + +const exec = promisify(execCallback); + +/** + * Fetches the git repository with a specified depth. + * + * @param depth - The depth to use for the fetch command. + * @returns True if the fetch is successful, otherwise false. + */ +async function fetchWithDepth(depth: number): Promise { + try { + await exec(`git fetch --depth ${depth} origin develop`); + await exec(`git fetch --depth ${depth} origin ${process.env.CIRCLE_BRANCH}`); + return true; + } catch (error: unknown) { + console.error(`Failed to fetch with depth ${depth}:`, error); + return false; + } +} + +/** + * Attempts to fetch the necessary commits until the merge base is found. + * It tries different fetch depths and performs a full fetch if needed. + * + * @throws If an unexpected error occurs during the execution of git commands. + */ +async function fetchUntilMergeBaseFound() { + const depths = [1, 10, 100]; + for (const depth of depths) { + console.log(`Attempting git diff with depth ${depth}...`); + await fetchWithDepth(depth); + + try { + await exec(`git merge-base origin/HEAD HEAD`); + return; + } catch (error: unknown) { + if ( + error instanceof Error && + hasProperty(error, 'code') && + error.code === 1 + ) { + console.error(`Error 'no merge base' encountered with depth ${depth}. Incrementing depth...`); + } else { + throw error; + } + } + } + await exec(`git fetch --unshallow origin develop`); +} + +/** + * Performs a git diff command to get the list of files changed between the current branch and the origin. + * It first ensures that the necessary commits are fetched until the merge base is found. + * + * @returns The output of the git diff command, listing the changed files. + * @throws If unable to get the diff after fetching the merge base or if an unexpected error occurs. + */ +async function gitDiff(): Promise { + await fetchUntilMergeBaseFound(); + const { stdout: diffResult } = await exec(`git diff --name-only origin/HEAD...${process.env.CIRCLE_BRANCH}`); + if (!diffResult) { + throw new Error('Unable to get diff after full checkout.'); + } + return diffResult; +} + +/** + * Stores the output of git diff to a file. + * + * @returns Returns a promise that resolves when the git diff output is successfully stored. + */ +async function storeGitDiffOutput() { + try { + console.log("Attempting to get git diff..."); + const diffOutput = await gitDiff(); + console.log(diffOutput); + + // Create the directory + const outputDir = 'changed-files'; + fs.mkdirSync(outputDir, { recursive: true }); + + // Store the output of git diff + const outputPath = path.resolve(outputDir, 'changed-files.txt'); + fs.writeFileSync(outputPath, diffOutput.trim()); + + console.log(`Git diff results saved to ${outputPath}`); + process.exit(0); + } catch (error: any) { + console.error('An error occurred:', error.message); + process.exit(1); + } +} + +storeGitDiffOutput(); diff --git a/.circleci/scripts/test-run-e2e.sh b/.circleci/scripts/test-run-e2e.sh index ff23712349ba..25ae1eb7b64c 100755 --- a/.circleci/scripts/test-run-e2e.sh +++ b/.circleci/scripts/test-run-e2e.sh @@ -11,4 +11,16 @@ then exit 1 fi +# Run the actual test command from the parameters +timeout 20m "$@" --retries 1 + +# Error code 124 means the command timed out +if [ $? -eq 124 ]; then + # Once deleted, if someone tries to "Rerun failed tests" the result will be + # "Error: can not rerun failed tests: no failed tests could be found" + echo 'Timeout error, deleting the test results' + rm -rf test/test-results/e2e + exit 124 +fi + exit 0 diff --git a/.circleci/scripts/validate-locales-only.ts b/.circleci/scripts/validate-locales-only.ts new file mode 100644 index 000000000000..d34be572c0bb --- /dev/null +++ b/.circleci/scripts/validate-locales-only.ts @@ -0,0 +1,31 @@ +const { readChangedFiles } = require('../../test/e2e/changedFilesUtil.js'); + +/** + * Verifies that all changed files are in the /_locales/ directory. + * Fails the build if any changed files are outside of the /_locales/ directory. + * Fails if no changed files are detected. + */ +async function validateLocalesOnlyChangedFiles() { + const changedFiles = await readChangedFiles(); + if (!changedFiles || changedFiles.length === 0) { + console.error('Failure: No changed files detected.'); + process.exit(1); + } + const invalidFiles = changedFiles.filter( + (file) => !file.startsWith('app/_locales/'), + ); + if (invalidFiles.length > 0) { + console.error( + 'Failure: Changed files must be in the /_locales/ directory.\n Changed Files:', + changedFiles, + '\n Invalid Files:', + invalidFiles, + ); + process.exit(1); + } else { + console.log('Passed validation'); + process.exit(0); + } +} + +validateLocalesOnlyChangedFiles(); diff --git a/.depcheckrc.yml b/.depcheckrc.yml index e4169c5436f0..7ce26732fcf7 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -58,6 +58,23 @@ ignores: - 'resolve-url-loader' # jest environments - 'jest-environment-jsdom' + # webpack + - '@pmmmwh/react-refresh-webpack-plugin' # dev tool + - 'webpack-dev-server' # dev tool + - 'html-bundler-webpack-plugin' # build tool + - 'postcss-loader' # build tool + - '@swc/helpers' # build tool + - browserslist # build tool + - 'buffer' # polyfill + - 'crypto-browserify' # polyfill + - 'process' # polyfill + - 'stream-http' # polyfill + - 'rimraf' # misc: install helper + - 'json-schema-to-ts' # misc: typescript helper + - 'https-browserify' # polyfill + - 'path-browserify' # polyfill + - 'nyc' # coverage + - 'core-js-pure' # polyfills # babel - '@babel/plugin-transform-logical-assignment-operators' # trezor diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 80e6d89d346a..25d48bbc1b41 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,7 +13,6 @@ ], "settings": { "editor.formatOnSave": true, - "git.autofetch": true, "git.ignoreRebaseWarning": true, "git.rebaseWhenSync": true, "gitlens.showWelcomeOnInstall": false @@ -48,7 +47,7 @@ } }, - "postAttachCommand": "/usr/local/share/desktop-init.sh && git pull --rebase; yarn download-builds", + "postAttachCommand": "/usr/local/share/desktop-init.sh && git pull; yarn download-builds", // This is a working Infura key, but it's on the Free Plan and has very limited requests per second // If you want to use your own INFURA_PROJECT_ID, follow the instructions in README.md @@ -56,5 +55,5 @@ "runArgs": ["--shm-size=1g"], - "updateContentCommand": "sudo .devcontainer/install.sh && yarn --immutable && yarn tsx .devcontainer/setup-browsers.ts && echo 'export DISPLAY=:1' >> ~/.bashrc" + "updateContentCommand": "sudo .devcontainer/install.sh && corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 yarn --immutable && yarn tsx .devcontainer/setup-browsers.ts && echo 'export DISPLAY=:1' >> ~/.bashrc" } diff --git a/.devcontainer/download-builds.ts b/.devcontainer/download-builds.ts index 1253c8a81599..83e1822379a1 100644 --- a/.devcontainer/download-builds.ts +++ b/.devcontainer/download-builds.ts @@ -52,7 +52,11 @@ async function getBuilds(branch: string, jobNames: string[]) { const artifacts = await response.json(); - if (!artifacts || artifacts.length === 0) { + if ( + !artifacts || + artifacts.length === 0 || + artifacts.message === 'Not Found' + ) { return []; } diff --git a/.eslintrc.js b/.eslintrc.js index 9f7fed5928ed..c4c4cc41927e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -257,7 +257,7 @@ module.exports = { * Mocha library. */ { - files: ['test/e2e/**/*.spec.js', 'test/unit-global/*.test.js'], + files: ['test/e2e/**/*.spec.js'], extends: ['@metamask/eslint-config-mocha'], rules: { // In Mocha tests, it is common to use `this` to store values or do @@ -281,7 +281,9 @@ module.exports = { '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/bridge.test.ts', + 'app/scripts/controllers/swaps/**/*.test.js', + 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/controllers/preferences.test.js', @@ -297,6 +299,7 @@ module.exports = { 'test/jest/*.js', 'test/lib/timer-helpers.js', 'test/e2e/helpers.test.js', + 'test/unit-global/*.test.js', 'ui/**/*.test.js', 'ui/__mocks__/*.js', 'shared/lib/error-utils.test.js', @@ -365,7 +368,6 @@ module.exports = { 'development/**/*.js', 'test/e2e/benchmark.js', 'test/helpers/setup-helper.js', - 'test/run-unit-tests.js', ], rules: { 'node/no-process-exit': 'off', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8bf55d58dbd4..51e5a51129b8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,11 +8,10 @@ # those changes on build, release and publishing outcomes. * @MetaMask/extension-devs -**/snaps/** @MetaMask/snaps-devs development/ @MetaMask/extension-devs @kumavis lavamoat/ @MetaMask/extension-devs @MetaMask/supply-chain @MetaMask/snaps-devs -# The offscreen.ts script file that is included in the offscreedocument html +# The offscreen.ts script file that is included in the offscreen document html # file is responsible, at present, for loading the snaps execution environment # for MV3. Any changes to this file should require at least one member of the # snaps development team to review and approve the changes. @@ -84,3 +83,13 @@ ui/components/component-library @MetaMask/design-system-engineers # Slack handle: @accounts-team-devs | Slack channel: #metamask-accounts-team app/scripts/lib/snap-keyring @MetaMask/accounts-engineers + +# Swaps team to own code for the swaps folder +ui/pages/swaps @MetaMask/swaps-engineers +app/scripts/controllers/swaps @MetaMask/swaps-engineers + +# Snaps +**/snaps/** @MetaMask/snaps-devs +shared/constants/permissions.ts @MetaMask/snaps-devs +ui/helpers/utils/permission.js @MetaMask/snaps-devs +ui/hooks/useTransactionInsights.js @MetaMask/snaps-devs diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index cec32fc26969..2b72c66524bd 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -48,6 +48,18 @@ body: label: Error messages or log output description: Please copy and paste any relevant error messages or log output. This will be automatically formatted, so there is no need for backticks. render: shell + - type: dropdown + id: stage + attributes: + label: Detection stage + description: At what stage was the bug detected? + options: + - In production (default) + - In beta + - During release testing + - On the development branch + validations: + required: true - type: input id: version attributes: diff --git a/.github/scripts/add-team-label-to-pr.ts b/.github/scripts/add-team-label-to-pr.ts deleted file mode 100644 index fbbbe30cac37..000000000000 --- a/.github/scripts/add-team-label-to-pr.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as core from '@actions/core'; -import { context, getOctokit } from '@actions/github'; -import { GitHub } from '@actions/github/lib/utils'; - -import { retrieveLabel } from './shared/label'; -import { Labelable, addLabelByIdToLabelable } from './shared/labelable'; -import { retrievePullRequest } from './shared/pull-request'; - -main().catch((error: Error): void => { - console.error(error); - process.exit(1); -}); - -async function main(): Promise { - // "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions. - // We can't use "GITHUB_TOKEN" here, as its permissions are scoped to the repository where the action is running. - // "GITHUB_TOKEN" does not have access to other repositories, even when they belong to the same organization. - // As we want to get files which are not necessarily located in the same repository, - // we need to create our own "RELEASE_LABEL_TOKEN" with "repo" permissions. - // Such a token allows to access other repositories of the MetaMask organisation. - const personalAccessToken = process.env.RELEASE_LABEL_TOKEN; - if (!personalAccessToken) { - core.setFailed('RELEASE_LABEL_TOKEN not found'); - process.exit(1); - } - - // Initialise octokit, required to call Github GraphQL API - const octokit: InstanceType = getOctokit(personalAccessToken, { - previews: ['bane'], // The "bane" preview is required for adding, updating, creating and deleting labels. - }); - - // Retrieve pull request info from context - const pullRequestRepoOwner = context.repo.owner; - const pullRequestRepoName = context.repo.repo; - const pullRequestNumber = context.payload.pull_request?.number; - if (!pullRequestNumber) { - core.setFailed('Pull request number not found'); - process.exit(1); - } - - // Retrieve pull request - const pullRequest: Labelable = await retrievePullRequest( - octokit, - pullRequestRepoOwner, - pullRequestRepoName, - pullRequestNumber, - ); - - // Get the team label id based on the author of the pull request - const teamLabelId = await getTeamLabelIdByAuthor( - octokit, - pullRequestRepoOwner, - pullRequestRepoName, - pullRequest.author, - ); - - // Add the team label by id to the pull request - await addLabelByIdToLabelable(octokit, pullRequest, teamLabelId); -} - -// This helper function gets the team label id based on the author of the pull request -const getTeamLabelIdByAuthor = async ( - octokit: InstanceType, - repoOwner: string, - repoName: string, - author: string, -): Promise => { - // Retrieve the teams.json file from the repository - const { data } = (await octokit.request( - 'GET /repos/{owner}/{repo}/contents/{path}', - { owner: repoOwner, repo: 'MetaMask-planning', path: 'teams.json' }, - )) as { data: { content: string } }; - - // Parse the teams.json file content to json from base64 - const teamMembers: Record = JSON.parse(atob(data.content)); - - // Get the label name based on the author - const labelName = teamMembers[author]; - - if (!labelName) { - core.setFailed(`Team label not found for author: ${author}`); - process.exit(1); - } - - // Retrieve the label id based on the label name - const labelId = await retrieveLabel(octokit, repoOwner, repoName, labelName); - - return labelId; -}; diff --git a/.github/scripts/check-template-and-add-labels.ts b/.github/scripts/check-template-and-add-labels.ts index a526ccc33b6e..ee9e4b4d163d 100644 --- a/.github/scripts/check-template-and-add-labels.ts +++ b/.github/scripts/check-template-and-add-labels.ts @@ -21,6 +21,13 @@ import { import { TemplateType, templates } from './shared/template'; import { retrievePullRequest } from './shared/pull-request'; +enum RegressionStage { + Development, + Testing, + Beta, + Production +} + const knownBots = ["metamaskbot", "dependabot", "github-actions", "sentry-io"]; main().catch((error: Error): void => { @@ -120,23 +127,9 @@ async function main(): Promise { invalidIssueTemplateLabel, ); - // Extract release version from bug report issue body (if existing) - const releaseVersion = extractReleaseVersionFromBugReportIssueBody( - labelable.body, - ); + // Add regression label to the bug report issue + addRegressionLabelToIssue(octokit, labelable); - // Add regression prod label to the bug report issue if release version was found in issue body - if(isReleaseCandidateIssue(labelable)) { - console.log( - `Issue ${labelable?.number} is not a production issue. Regression prod label is not needed.`, - ); - } else if (releaseVersion) { - await addRegressionProdLabelToIssue(octokit, releaseVersion, labelable); - } else { - console.log( - `No release version was found in body of bug report issue ${labelable?.number}.`, - ); - } } else { const errorMessage = "Issue body does not match any of expected templates ('general-issue.yml' or 'bug-report.yml').\n\nMake sure issue's body includes all section titles.\n\nSections titles are listed here: https://github.com/MetaMask/metamask-extension/blob/develop/.github/scripts/shared/template.ts#L14-L37"; @@ -215,6 +208,28 @@ function extractTemplateTypeFromBody(body: string): TemplateType { return TemplateType.None; } +// This helper function extracts regression stage (Development, Testing, Production) from bug report issue's body. +function extractRegressionStageFromBugReportIssueBody( + body: string, +): RegressionStage | undefined { + const detectionStageRegex = /### Detection stage\s*\n\s*(.*)/i; + const match = body.match(detectionStageRegex); + const extractedAnswer = match ? match[1].trim() : undefined; + + switch (extractedAnswer) { + case 'On the development branch': + return RegressionStage.Development; + case 'During release testing': + return RegressionStage.Testing; + case 'In beta': + return RegressionStage.Beta; + case 'In production (default)': + return RegressionStage.Production; + default: + return undefined; + } +} + // This helper function extracts release version from bug report issue's body. function extractReleaseVersionFromBugReportIssueBody( body: string, @@ -235,49 +250,54 @@ function extractReleaseVersionFromBugReportIssueBody( return version; } -// This function adds the correct "regression-prod-x.y.z" label to the issue, and removes other ones -async function addRegressionProdLabelToIssue( +// This function adds the correct regression label to the issue, and removes other ones +async function addRegressionLabelToIssue( octokit: InstanceType, - releaseVersion: string, issue: Labelable, ): Promise { - // Craft regression prod label to add - const regressionProdLabel: Label = { - name: `regression-prod-${releaseVersion}`, - color: '5319E7', // violet - description: `Regression bug that was found in production in release ${releaseVersion}`, - }; - - let regressionProdLabelFound: boolean = false; - const regressionProdLabelsToBeRemoved: { + // Extract regression stage from bug report issue body (if existing) + const regressionStage = extractRegressionStageFromBugReportIssueBody( + issue.body, + ); + + // Extract release version from bug report issue body (if existing) + const releaseVersion = extractReleaseVersionFromBugReportIssueBody( + issue.body, + ); + + // Craft regression label to add + const regressionLabel: Label = craftRegressionLabel(regressionStage, releaseVersion); + + let regressionLabelFound: boolean = false; + const regressionLabelsToBeRemoved: { id: string; name: string; }[] = []; // Loop over issue's labels, to see if regression labels are either missing, or to be removed issue?.labels?.forEach((label) => { - if (label?.name === regressionProdLabel.name) { - regressionProdLabelFound = true; - } else if (label?.name?.startsWith('regression-prod-')) { - regressionProdLabelsToBeRemoved.push(label); + if (label?.name === regressionLabel.name) { + regressionLabelFound = true; + } else if (label?.name?.startsWith('regression-')) { + regressionLabelsToBeRemoved.push(label); } }); // Add regression prod label to the issue if missing - if (regressionProdLabelFound) { + if (regressionLabelFound) { console.log( - `Issue ${issue?.number} already has ${regressionProdLabel.name} label.`, + `Issue ${issue?.number} already has ${regressionLabel.name} label.`, ); } else { console.log( - `Add ${regressionProdLabel.name} label to issue ${issue?.number}.`, + `Add ${regressionLabel.name} label to issue ${issue?.number}.`, ); - await addLabelToLabelable(octokit, issue, regressionProdLabel); + await addLabelToLabelable(octokit, issue, regressionLabel); } // Remove other regression prod label from the issue await Promise.all( - regressionProdLabelsToBeRemoved.map((label) => { + regressionLabelsToBeRemoved.map((label) => { removeLabelFromLabelable(octokit, issue, label?.id); }), ); @@ -309,9 +329,42 @@ async function userBelongsToMetaMaskOrg( return Boolean(userBelongsToMetaMaskOrgResult?.user?.organization?.id); } -// This function checks if issue is a release candidate (RC) issue, discovered during release regression testing phase. If so, it means it is not a production issue. -function isReleaseCandidateIssue( - issue: Labelable, -): boolean { - return Boolean(issue.labels.find(label => label.name.startsWith('regression-RC'))); +// This function crafts appropriate label, corresponding to regression stage and release version. +function craftRegressionLabel(regressionStage: RegressionStage | undefined, releaseVersion: string | undefined): Label { + switch (regressionStage) { + case RegressionStage.Development: + return { + name: `regression-develop`, + color: '5319E7', // violet + description: `Regression bug that was found on development branch, but not yet present in production`, + }; + + case RegressionStage.Testing: + return { + name: `regression-RC-${releaseVersion || '*'}`, + color: '744C11', // orange + description: releaseVersion ? `Regression bug that was found in release candidate (RC) for release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-RC-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Beta: + return { + name: `regression-beta-${releaseVersion || '*'}`, + color: 'D94A83', // pink + description: releaseVersion ? `Regression bug that was found in beta in release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-beta-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Production: + return { + name: `regression-prod-${releaseVersion || '*'}`, + color: '5319E7', // violet + description: releaseVersion ? `Regression bug that was found in production in release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + default: + return { + name: `regression-*`, + color: 'EDEDED', // grey + description: `TODO: Unknown regression stage. Please replace with correct regression label: 'regression-develop', 'regression-RC-x.y.z', or 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + } } diff --git a/.github/workflows/add-release-label.yml b/.github/workflows/add-release-label.yml index f92f3f5fdc95..94ba76fcefa0 100644 --- a/.github/workflows/add-release-label.yml +++ b/.github/workflows/add-release-label.yml @@ -12,25 +12,13 @@ jobs: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - - run: corepack enable - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # This is needed to checkout all branches - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn + fetch-depth: 0 # This is needed to checkout all branches - - name: Install dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Get the next semver version id: get-next-semver-version diff --git a/.github/workflows/add-team-label.yml b/.github/workflows/add-team-label.yml index eba9e48c1f15..2046456ef426 100644 --- a/.github/workflows/add-team-label.yml +++ b/.github/workflows/add-team-label.yml @@ -1,4 +1,4 @@ -name: Add team label to PR when it is opened +name: Add team label on: pull_request: @@ -7,31 +7,8 @@ on: jobs: add-team-label: - runs-on: ubuntu-latest - steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - - run: corepack enable - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # This is needed to checkout all branches - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn - - - name: Install dependencies - run: yarn --immutable - - - name: Add team label to PR - id: add-team-label-to-pr - env: - RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} - run: yarn run add-team-label-to-pr + uses: metamask/github-tools/.github/workflows/add-team-label.yml@058012b49ff2fbd9649c566ba43b29497f93b21d + permissions: + pull-requests: write + secrets: + PERSONAL_ACCESS_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} diff --git a/.github/workflows/check-attributions.yml b/.github/workflows/check-attributions.yml index f1c531e34e9d..4880cc65ebe8 100644 --- a/.github/workflows/check-attributions.yml +++ b/.github/workflows/check-attributions.yml @@ -17,13 +17,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 - - run: corepack enable - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + - name: Check attributions changes run: yarn attributions:check diff --git a/.github/workflows/check-pr-labels.yml b/.github/workflows/check-pr-labels.yml index eebd284a1561..19c0576feaae 100644 --- a/.github/workflows/check-pr-labels.yml +++ b/.github/workflows/check-pr-labels.yml @@ -1,4 +1,4 @@ -name: 'Check PR has required labels' +name: Check PR has required labels on: pull_request: branches: @@ -17,28 +17,13 @@ jobs: pull-requests: read steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 # This retrieves only the latest commit. - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn - - name: Install dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Check PR has required labels - id: check-pr-has-required-labels env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: yarn run check-pr-has-required-labels diff --git a/.github/workflows/check-template-and-add-labels.yml b/.github/workflows/check-template-and-add-labels.yml index 42fd2f236c72..e9c1c2844ca5 100644 --- a/.github/workflows/check-template-and-add-labels.yml +++ b/.github/workflows/check-template-and-add-labels.yml @@ -10,25 +10,11 @@ jobs: check-template-and-add-labels: runs-on: ubuntu-latest steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 # This retrieves only the latest commit. - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn - - name: Install dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Check template and add labels id: check-template-and-add-labels diff --git a/.github/workflows/close-bug-report.yml b/.github/workflows/close-bug-report.yml index dbe8f17acc6f..46dcd0a9e9ff 100644 --- a/.github/workflows/close-bug-report.yml +++ b/.github/workflows/close-bug-report.yml @@ -12,28 +12,13 @@ jobs: runs-on: ubuntu-latest if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'Version-v') steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 # This retrieves only the latest commit. - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn - - name: Install dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Close release bug report issue - id: close-release-bug-report-issue env: BUG_REPORT_REPO: MetaMask-planning BUG_REPORT_TOKEN: ${{ secrets.BUG_REPORT_TOKEN }} diff --git a/.github/workflows/codespaces.yml b/.github/workflows/codespaces.yml index d424a6a3e336..3455e2db54d4 100644 --- a/.github/workflows/codespaces.yml +++ b/.github/workflows/codespaces.yml @@ -13,21 +13,8 @@ jobs: name: Generate cache image runs-on: ubuntu-latest steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 1 # This retrieves only the latest commit. - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index 001db44cde2f..b4979c8f3e7b 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -9,25 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn + fetch-depth: 0 # This is needed to checkout all branches - - name: Install dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Run fitness functions env: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a1c5cc29a575..d14fefe82717 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,10 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Download actionlint id: download-actionlint run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.23 shell: bash + - name: Check workflow files run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml new file mode 100644 index 000000000000..8de1899e0dfa --- /dev/null +++ b/.github/workflows/run-integration-tests.yml @@ -0,0 +1,20 @@ +name: Run integration tests + +on: + push: + branches: [develop, master] + pull_request: + types: [opened,reopened,synchronize] + +jobs: + test-integration: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: test:integration:coverage + run: yarn test:integration:coverage diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml new file mode 100644 index 000000000000..6765352f5d7d --- /dev/null +++ b/.github/workflows/run-unit-tests.yml @@ -0,0 +1,72 @@ +name: Run unit tests + +on: + push: + branches: [develop, master] + pull_request: + types: [opened,reopened,synchronize] + +jobs: + test-unit: + runs-on: ubuntu-latest + strategy: + matrix: + shard: [1, 2, 3, 4, 5, 6] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: test:unit:coverage + run: yarn test:unit:coverage --shard=${{ matrix.shard }}/${{ strategy.job-total }} + + - name: Rename coverage to shard coverage + run: mv coverage/coverage-final.json coverage/coverage-${{matrix.shard}}.json + + - uses: actions/upload-artifact@v4 + with: + name: coverage-${{matrix.shard}} + path: coverage/coverage-${{matrix.shard}}.json + + test-webpack: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main + + - name: test:unit:webpack:coverage + run: yarn test:unit:webpack:coverage + + - name: Rename coverage + run: mv coverage/coverage-final.json coverage/coverage-webpack.json + + - uses: actions/upload-artifact@v4 + with: + name: coverage-webpack + path: coverage/coverage-webpack.json + + report-coverage: + runs-on: ubuntu-latest + needs: + - test-unit + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download coverage from shards + uses: actions/download-artifact@v4 + with: + path: coverage + pattern: coverage-* + merge-multiple: true + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.github/workflows/update-attributions.yml b/.github/workflows/update-attributions.yml index 0a674975d0e5..3e107d324216 100644 --- a/.github/workflows/update-attributions.yml +++ b/.github/workflows/update-attributions.yml @@ -58,14 +58,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - - run: corepack enable - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install Yarn dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Get commit SHA id: commit-sha run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" @@ -86,18 +80,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - - run: corepack enable - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable --immutable-cache + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Generate Attributions run: yarn attributions:generate - name: Cache attributions file - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: attribution.txt key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} @@ -125,7 +113,7 @@ jobs: id: commit-sha run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Restore attributions file - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: attribution.txt key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} diff --git a/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml index 41d75af08ae0..1baef7fb4460 100644 --- a/.github/workflows/update-lavamoat-policies.yml +++ b/.github/workflows/update-lavamoat-policies.yml @@ -51,11 +51,6 @@ jobs: outputs: COMMIT_SHA: ${{ steps.commit-sha.outputs.COMMIT_SHA }} steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - name: Checkout repository uses: actions/checkout@v4 - name: Checkout pull request @@ -63,13 +58,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install Yarn dependencies - run: yarn --immutable + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Get commit SHA id: commit-sha run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" @@ -80,11 +70,6 @@ jobs: needs: - prepare steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - name: Checkout repository uses: actions/checkout@v4 - name: Checkout pull request @@ -92,17 +77,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable --immutable-cache + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Update LavaMoat build policy run: yarn lavamoat:build:auto - name: Cache build policy - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: lavamoat/build-system key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} @@ -119,11 +99,6 @@ jobs: - prepare - update-lavamoat-build-policy steps: - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - - run: corepack enable - name: Checkout repository uses: actions/checkout@v4 - name: Checkout pull request @@ -131,15 +106,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: 'yarn' - - name: Install dependencies from cache - run: yarn --immutable --immutable-cache + - name: Setup environment + uses: metamask/github-tools/.github/actions/setup-environment@main - name: Restore build policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/build-system key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} @@ -149,7 +119,7 @@ jobs: env: INFURA_PROJECT_ID: 00000000000 - name: Cache ${{ matrix.build-type }} application policy - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: lavamoat/browserify/${{ matrix.build-type }} key: cache-${{ matrix.build-type }}-${{ needs.prepare.outputs.COMMIT_SHA }} @@ -179,7 +149,7 @@ jobs: id: commit-sha run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Restore build policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/build-system key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} @@ -188,25 +158,25 @@ jobs: # Ensure this is synchronized with the list above in the "update-lavamoat-webapp-policy" job # and with the build type list in `builds.yml` - name: Restore main application policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/browserify/main key: cache-main-${{ needs.prepare.outputs.COMMIT_SHA }} fail-on-cache-miss: true - name: Restore beta application policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/browserify/beta key: cache-beta-${{ needs.prepare.outputs.COMMIT_SHA }} fail-on-cache-miss: true - name: Restore flask application policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/browserify/flask key: cache-flask-${{ needs.prepare.outputs.COMMIT_SHA }} fail-on-cache-miss: true - name: Restore mmi application policy - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: lavamoat/browserify/mmi key: cache-mmi-${{ needs.prepare.outputs.COMMIT_SHA }} diff --git a/.gitignore b/.gitignore index 4f2d481b8807..eb8b930adf69 100644 --- a/.gitignore +++ b/.gitignore @@ -66,11 +66,10 @@ development/generate-attributions/.yarn/* # MMI Playwright public/playwright -test/e2e/mmi/.env -test/e2e/mmi/playwright.config.ts -test/e2e/mmi/specs/**/*-darwin.png -test/e2e/mmi/dist/ - +test/e2e/playwright/mmi/.env +test/e2e/playwright/mmi/playwright.config.ts +test/e2e/playwright/mmi/specs/**/*-darwin.png +test/e2e/playwright/mmi/dist/ lavamoat/**/policy-debug.json # Attributions @@ -79,3 +78,5 @@ licenseInfos.json # API Spec tests html-report/ +/app/images/branding + diff --git a/.metamaskrc.dist b/.metamaskrc.dist index 62dfdeb64634..745c51528c3e 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -6,6 +6,7 @@ INFURA_PROJECT_ID=00000000000 ;PASSWORD=METAMASK PASSWORD ;SEGMENT_WRITE_KEY= +;BRIDGE_USE_DEV_APIS= ;SWAPS_USE_DEV_APIS= ;PORTFOLIO_URL= ;TRANSACTION_SECURITY_PROVIDER= @@ -27,9 +28,18 @@ BLOCKAID_PUBLIC_KEY= ; Enable the redesigned confirmations still in development, without needing to toggle the developer setting. ; ENABLE_CONFIRMATION_REDESIGN= +; URL of security alerts API used to validate dApp requests +; SECURITY_ALERTS_API_URL='http://localhost:3000' +; Temporary mechanism to enable security alerts API prior to release +; SECURITY_ALERTS_API_ENABLED='true' + ; Enables the Settings Page - Developer Options ; ENABLE_SETTINGS_PAGE_DEV_OPTIONS=true ; The endpoint used to submit errors and tracing data to Sentry. ; The below is the `test-metamask` project. ; SENTRY_DSN_DEV=https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496 + +; Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render +; This should NEVER be enabled in production since it slows down react +; ENABLE_WHY_DID_YOU_RENDER=false diff --git a/.prettierignore b/.prettierignore index e70feef786f9..9c4b3868464b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,18 +1,16 @@ -node_modules/**/* -lavamoat/**/policy.json -dist/**/* -builds/**/* -test-*/**/* -app/vendor/** -.nyc_output/**/* -test/e2e/send-eth-with-private-key-test/**/* *.scss -development/chromereload.js -development/ts-migration-dashboard/filesToConvert.json -development/ts-migration-dashboard/build/** -public/**/* -development/charts/** +.nyc_output/**/* node_modules/**/* -storybook-build/**/* -jest-coverage/**/* -coverage/**/* +/app/vendor/** +/builds/**/* +/coverage/**/* +/development/charts/** +/development/chromereload.js +/development/ts-migration-dashboard/filesToConvert.json +/development/ts-migration-dashboard/build/** +/dist/**/* +/lavamoat/**/policy.json +/storybook-build/**/* +/test-artifacts/**/* +/test/test-results/**/* +/test/e2e/send-eth-with-private-key-test/**/* diff --git a/.storybook/initial-states/transactions.js b/.storybook/initial-states/transactions.js index 6dd76b2be7e8..f7d81759770e 100644 --- a/.storybook/initial-states/transactions.js +++ b/.storybook/initial-states/transactions.js @@ -7,7 +7,6 @@ const MOCK_TX_TYPE = { INCOMING: 'incoming', PERSONAL_SIGN: 'personal_sign', RETRY: 'retry', - SIGN: 'eth_sign', SIGN_TYPED_DATA: 'eth_signTypedData', SIMPLE_SEND: 'simpleSend', SMART: 'smart', @@ -420,27 +419,6 @@ export const MOCK_TRANSACTION_BY_TYPE = { 'Error: [ethjs-query] while formatting outputs from RPC \'{"value":{"code":-32000,"message":"replacement transaction underpriced"}}\'\n at chrome-extension://hbljfohiafgaaaabejngpgolnboohpaf/common-5.js:14346:29', }, }, - [MOCK_TX_TYPE.SIGN]: { - id: 5177046356058675, - msgParams: { - from: '0xabce7847fd3661a9b7c86aaf1daea08d9da5750e', - data: - '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', - origin: 'https://metamask.github.io', - }, - txParams: { - from: '0xabc14609ef9e09776ac5fe00bdbfef57bcdefebb', - gas: '0x5208', - gasPrice: '0x77359400', - nonce: '0x3', - value: '0x00', - data: - '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', - }, - time: 1653451051909, - status: 'unapproved', - type: 'eth_sign', - }, [MOCK_TX_TYPE.SIGN_TYPED_DATA]: { id: 5177046356058598, msgParams: { diff --git a/.storybook/main.js b/.storybook/main.js index 3c67bb2b5ce1..554f6d955ee0 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -45,6 +45,9 @@ module.exports = { config.resolve.alias['../../../../store/actions'] = require.resolve( '../ui/__mocks__/actions.js', ); + config.resolve.alias['../../../../../../store/actions'] = require.resolve( + '../ui/__mocks__/actions.js', + ); config.resolve.fallback = { child_process: false, constants: false, diff --git a/.storybook/preview.js b/.storybook/preview.js index b0486d83aeeb..fc077a637f79 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -20,6 +20,7 @@ import { metamaskStorybookTheme } from './metamask-storybook-theme'; import { DocsContainer } from '@storybook/addon-docs'; import { useDarkMode } from 'storybook-dark-mode'; import { themes } from '@storybook/theming'; +import { AlertMetricsProvider } from '../ui/components/app/alert-system/contexts/alertMetricsContext'; // eslint-disable-next-line /* @ts-expect-error: Avoids error from window property not existing */ @@ -124,13 +125,15 @@ const metamaskDecorator = (story, context) => { - - {story()} - + + + {story()} + + diff --git a/.storybook/test-data.js b/.storybook/test-data.js index fab382d4b6b8..274fdfdb3651 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1,6 +1,5 @@ import { draftTransactionInitialState } from '../ui/ducks/send'; import { KeyringType } from '../shared/constants/keyring'; -import { NetworkType } from '@metamask/controller-utils'; import { NetworkStatus } from '@metamask/network-controller'; import { EthAccountType } from '@metamask/keyring-api'; import { CHAIN_IDS } from '../shared/constants/network'; @@ -8,6 +7,7 @@ import { copyable, divider, heading, panel, text } from '@metamask/snaps-sdk'; import { getJsxElementFromComponent } from '@metamask/snaps-utils'; import { FirstTimeFlowType } from '../shared/constants/onboarding'; import { ETH_EOA_METHODS } from '../shared/constants/eth-methods'; +import { mockNetworkState } from '../test/stub/networks'; const state = { invalidCustomNetwork: { @@ -181,15 +181,6 @@ const state = { 1559: true, }, }, - selectedNetworkClientId: NetworkType.mainnet, - networksMetadata: { - [NetworkType.mainnet]: { - EIPS: { - 1559: true, - }, - status: NetworkStatus.Available, - }, - }, gasFeeEstimates: '0x5208', swapsState: { quotes: {}, @@ -705,13 +696,6 @@ const state = { connectedStatusPopoverHasBeenShown: true, swapsWelcomeMessageHasBeenShown: true, defaultHomeActiveTabName: 'Tokens', - providerConfig: { - type: 'sepolia', - ticker: 'ETH', - nickname: 'Sepolia', - rpcUrl: '', - chainId: '0xaa36a7', - }, network: '5', accounts: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { @@ -1204,8 +1188,6 @@ const state = { v: '0x93', }, ], - unapprovedMsgs: {}, - unapprovedMsgCount: 0, unapprovedPersonalMsgs: {}, unapprovedPersonalMsgCount: 0, unapprovedDecryptMsgs: {}, @@ -1236,21 +1218,23 @@ const state = { accounts: ['0x9d0ba4ddac06032527b140912ec808ab9451b788'], }, ], - networkConfigurations: { - 'test-networkConfigurationId-1': { + ...mockNetworkState({ + id: 'test-networkConfigurationId-1', rpcUrl: 'https://testrpc.com', chainId: '0x1', nickname: 'mainnet', - rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' }, - }, - 'test-networkConfigurationId-2': { + blockExplorerUrl: 'https://etherscan.io', + metadata: { + EIPS: { 1559: true }, + status: NetworkStatus.Available, + } + }, { + id: 'test-networkConfigurationId-2', rpcUrl: 'http://localhost:8545', chainId: '0x539', ticker: 'ETH', nickname: 'Localhost 8545', - rpcPrefs: {}, - }, - }, + }), accountTokens: { '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { '0x1': [ @@ -1484,7 +1468,19 @@ const state = { }, }, ensResolutionsByAddress: {}, - pendingApprovals: {}, + pendingApprovals: { + '741bad30-45b6-11ef-b6ec-870d18dd6c01': { + id: '741bad30-45b6-11ef-b6ec-870d18dd6c01', + origin: 'http://127.0.0.1:8080', + type: 'transaction', + time: 1721383540624, + requestData: { + txId: '741bad30-45b6-11ef-b6ec-870d18dd6c01', + }, + requestState: null, + expectsResult: true, + }, + }, pendingApprovalCount: 0, subjectMetadata: { 'http://localhost:8080': { diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 24e3747bfa9b..51c7db9f6211 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -70,6 +70,7 @@ "quickstart", "recompiles", "shellcheck", + "SIWE", "sourcemaps", "sprintf", "testcase", diff --git a/.vscode/launch.json b/.vscode/launch.json index e801fb65f8b7..bb8b8618b403 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,24 @@ "program": "${workspaceFolder}/node_modules/jest/bin/jest" } }, + { + "type": "node", + "request": "launch", + "name": "Jest Integration: current file", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--config", + "jest.integration.config.js", + "--testTimeout", + "30000" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + }, { "type": "node", "request": "launch", diff --git a/.vscode/package.json-schema.json b/.vscode/package.json-schema.json index c377e274b3ce..458329d0741c 100644 --- a/.vscode/package.json-schema.json +++ b/.vscode/package.json-schema.json @@ -5,9 +5,22 @@ "required": ["lavamoat"], "properties": { "scripts": { + "required": ["webpack", "webpack:clearcache", "postinstall"], "properties": { "start:dev": { "description": "Runs `yarn start` with the addition of react/redux backend devtool servers enabled." + }, + "webpack": { + "type": "string", + "description": "Builds the extension in \"dev\" mode. Run `yarn webpack --help` for usage information." + }, + "webpack:clearcache": { + "type": "string", + "description": "Deletes webpack's build cache. Useful to force a rebuild (webpack not detecting changes, node_modules have changed, etc)." + }, + "postinstall": { + "type": "string", + "description": "Runs automatically after running `yarn` (`yarn install`) in order to prime the webpack dev build." } } }, @@ -16,6 +29,62 @@ "properties": { "autoprefixer": { "description": "Used by our build systems to automatically add prefixes to CSS rules based on our browserslist." + }, + "@types/chrome": { + "type": "string", + "description": "Provides type definitions for the Chrome extension manifest.json files." + }, + "buffer": { + "type": "string", + "description": "Provides a global Buffer object for use in the browser (webpack)" + }, + "crypto-browserify": { + "type": "string", + "description": "Polyfill's node's crypto API for use in the browser (webpack)" + }, + "dotenv": { + "type": "string", + "description": "Loads environment variables from a .metamaskrc file (webpack)" + }, + "fflate": { + "type": "string", + "description": "Provides zip capabilities for bundling (webpack)" + }, + "postcss-loader": { + "type": "string", + "description": "Loads postcss plugins (webpack)" + }, + "process": { + "type": "string", + "description": "Provides a global process object for use in the browser (webpack)" + }, + "schema-utils": { + "type": "string", + "description": "Provides utilities for validating options objects (webpack)" + }, + "stream-http": { + "type": "string", + "description": "Polyfill's node's stream API for use in the browser (webpack)" + }, + "@swc/core": { + "type": "string", + "description": "Transpiles javascript and typescript (webpack)" + }, + "tsx": { + "type": "string", + "description": "Provides blazing fast typescript compilation (no type checking) (webpack)" + }, + "rimraf": { + "type": "string", + "description": "Provides a cross-platform way of deleting files from the command line (webpack)" + }, + "json-schema-to-ts": { + "type": "string", + "description": "Generates typescript types from json schemas (webpack)" + }, + "@pmmmwh/react-refresh-webpack-plugin": { + "type": "string", + "description": "Provides hot reloading for react components (webpack)" } } }, diff --git a/.yarn/patches/@metamask-assets-controllers-npm-33.0.0-3e7448c4cd.patch b/.yarn/patches/@metamask-assets-controllers-npm-33.0.0-3e7448c4cd.patch deleted file mode 100644 index 8961c8f629e6..000000000000 --- a/.yarn/patches/@metamask-assets-controllers-npm-33.0.0-3e7448c4cd.patch +++ /dev/null @@ -1,58 +0,0 @@ -diff --git a/dist/chunk-FGAZXVKS.js b/dist/chunk-FGAZXVKS.js -index 4a6aa7918599269951044b9f8c7c076a7fe6a9c7..4c1054a867ff6aef1e26f8d84e2831f234c4e44e 100644 ---- a/dist/chunk-FGAZXVKS.js -+++ b/dist/chunk-FGAZXVKS.js -@@ -503,6 +503,7 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({ - if (fallbackCurrencyToNativeCurrencyConversionRate === null) { - return {}; - } -+ const convertFallbackToNative = (value) => value !== void 0 && value !== null ? value * fallbackCurrencyToNativeCurrencyConversionRate : void 0; - const updatedContractExchangeRates = Object.entries( - contractExchangeInformations - ).reduce((acc, [tokenAddress, token]) => { -@@ -510,7 +511,15 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({ - ...acc, - [tokenAddress]: { - ...token, -- price: token.price ? token.price * fallbackCurrencyToNativeCurrencyConversionRate : void 0 -+ currency: nativeCurrency, -+ price: convertFallbackToNative(token.price), -+ marketCap: convertFallbackToNative(token.marketCap), -+ allTimeHigh: convertFallbackToNative(token.allTimeHigh), -+ allTimeLow: convertFallbackToNative(token.allTimeLow), -+ totalVolume: convertFallbackToNative(token.totalVolume), -+ high1d: convertFallbackToNative(token.high1d), -+ low1d: convertFallbackToNative(token.low1d), -+ dilutedMarketCap: convertFallbackToNative(token.dilutedMarketCap) - } - }; - return acc; -diff --git a/dist/chunk-P3O5CVAH.mjs b/dist/chunk-P3O5CVAH.mjs -index 32379704450baee58a37623b1d0f6b471e69de2e..069c3deec15ad85057c910ecf0db31d856fd6ae2 100644 ---- a/dist/chunk-P3O5CVAH.mjs -+++ b/dist/chunk-P3O5CVAH.mjs -@@ -503,6 +503,7 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({ - if (fallbackCurrencyToNativeCurrencyConversionRate === null) { - return {}; - } -+ const convertFallbackToNative = (value) => value !== void 0 && value !== null ? value * fallbackCurrencyToNativeCurrencyConversionRate : void 0; - const updatedContractExchangeRates = Object.entries( - contractExchangeInformations - ).reduce((acc, [tokenAddress, token]) => { -@@ -510,7 +511,15 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({ - ...acc, - [tokenAddress]: { - ...token, -- price: token.price ? token.price * fallbackCurrencyToNativeCurrencyConversionRate : void 0 -+ currency: nativeCurrency, -+ price: convertFallbackToNative(token.price), -+ marketCap: convertFallbackToNative(token.marketCap), -+ allTimeHigh: convertFallbackToNative(token.allTimeHigh), -+ allTimeLow: convertFallbackToNative(token.allTimeLow), -+ totalVolume: convertFallbackToNative(token.totalVolume), -+ high1d: convertFallbackToNative(token.high1d), -+ low1d: convertFallbackToNative(token.low1d), -+ dilutedMarketCap: convertFallbackToNative(token.dilutedMarketCap) - } - }; - return acc; diff --git a/.yarn/patches/@metamask-eth-ledger-bridge-keyring-npm-2.0.1-7a5d815b2d.patch b/.yarn/patches/@metamask-eth-ledger-bridge-keyring-npm-2.0.1-7a5d815b2d.patch new file mode 100644 index 000000000000..786d5cd1b226 --- /dev/null +++ b/.yarn/patches/@metamask-eth-ledger-bridge-keyring-npm-2.0.1-7a5d815b2d.patch @@ -0,0 +1,18 @@ +diff --git a/dist/ledger-keyring.js b/dist/ledger-keyring.js +index 2386b2e7fe36d1e65ef74f0a19d3b41450dcfa48..f999a0ab465cce7a450a5812f1d7aa6e39b74aed 100644 +--- a/dist/ledger-keyring.js ++++ b/dist/ledger-keyring.js +@@ -150,7 +150,12 @@ class LedgerKeyring extends events_1.EventEmitter { + }); + } + catch (error) { +- throw error instanceof Error ? error : new Error('Unknown error'); ++ ++ /** ++ * For Fixing issue 22837, when ledger is locked and didnt open the ethereum app in ledger, ++ * The extension will always show `unknown error`, below change will transform the error to something meaningful. ++ */ ++ throw error instanceof Error ? error : new Error('Unlock your Ledger device and open the ETH app'); + } + if (updateHdk && payload.chainCode) { + this.hdk.publicKey = buffer_1.Buffer.from(payload.publicKey, 'hex'); diff --git a/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch b/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch deleted file mode 100644 index e4e69c39609c..000000000000 --- a/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch +++ /dev/null @@ -1,2630 +0,0 @@ -diff --git a/dist/GasFeeController.js b/dist/GasFeeController.js -index 089bba219bac937eeccb423f3e69ad5407ef9be0..b6bdda12ceeb0c38b344b20f80c3afc5120ce3f9 100644 ---- a/dist/GasFeeController.js -+++ b/dist/GasFeeController.js -@@ -3,12 +3,12 @@ - - - --var _chunkH5WHAYLIjs = require('./chunk-H5WHAYLI.js'); --require('./chunk-Q2YPK5SL.js'); -+var _chunkX74LQX2Yjs = require('./chunk-X74LQX2Y.js'); -+require('./chunk-2MFVV2BX.js'); - - - - - --exports.GAS_API_BASE_URL = _chunkH5WHAYLIjs.GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; exports.default = _chunkH5WHAYLIjs.GasFeeController_default; -+exports.GAS_ESTIMATE_TYPES = _chunkX74LQX2Yjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkX74LQX2Yjs.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkX74LQX2Yjs.LEGACY_GAS_PRICES_API_URL; exports.default = _chunkX74LQX2Yjs.GasFeeController_default; - //# sourceMappingURL=GasFeeController.js.map -\ No newline at end of file -diff --git a/dist/GasFeeController.mjs b/dist/GasFeeController.mjs -index 14ab557e85665a30cbd8b4cec41448d5b88fed91..9b9b90786ac35a4cf320d00a933ef151cdf03821 100644 ---- a/dist/GasFeeController.mjs -+++ b/dist/GasFeeController.mjs -@@ -1,14 +1,14 @@ - import { -- GAS_API_BASE_URL, - GAS_ESTIMATE_TYPES, - GasFeeController, -- GasFeeController_default --} from "./chunk-BEVZS3YV.mjs"; --import "./chunk-KORLXV32.mjs"; -+ GasFeeController_default, -+ LEGACY_GAS_PRICES_API_URL -+} from "./chunk-A7NHJBXX.mjs"; -+import "./chunk-R3IOI7AK.mjs"; - export { -- GAS_API_BASE_URL, - GAS_ESTIMATE_TYPES, - GasFeeController, -+ LEGACY_GAS_PRICES_API_URL, - GasFeeController_default as default - }; - //# sourceMappingURL=GasFeeController.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-2MFVV2BX.js b/dist/chunk-2MFVV2BX.js -new file mode 100644 -index 0000000000000000000000000000000000000000..4f83a713baa28f3d687c50ed7ad3c79715a2b588 ---- /dev/null -+++ b/dist/chunk-2MFVV2BX.js -@@ -0,0 +1,156 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var __accessCheck = (obj, member, msg) => { -+ if (!member.has(obj)) -+ throw TypeError("Cannot " + msg); -+}; -+var __privateGet = (obj, member, getter) => { -+ __accessCheck(obj, member, "read from private field"); -+ return getter ? getter.call(obj) : member.get(obj); -+}; -+var __privateAdd = (obj, member, value) => { -+ if (member.has(obj)) -+ throw TypeError("Cannot add the same private member more than once"); -+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -+}; -+var __privateSet = (obj, member, value, setter) => { -+ __accessCheck(obj, member, "write to private field"); -+ setter ? setter.call(obj, value) : member.set(obj, value); -+ return value; -+}; -+var __privateMethod = (obj, member, method) => { -+ __accessCheck(obj, member, "access private method"); -+ return method; -+}; -+ -+// src/gas-util.ts -+ -+ -+ -+ -+ -+var _controllerutils = require('@metamask/controller-utils'); -+var _bnjs = require('bn.js'); var _bnjs2 = _interopRequireDefault(_bnjs); -+var makeClientIdHeader = (clientId) => ({ "X-Client-Id": clientId }); -+function normalizeGWEIDecimalNumbers(n) { -+ const numberAsWEIHex = _controllerutils.gweiDecToWEIBN.call(void 0, n).toString(16); -+ const numberAsGWEI = _controllerutils.weiHexToGweiDec.call(void 0, numberAsWEIHex); -+ return numberAsGWEI; -+} -+async function fetchGasEstimates(url, clientId) { -+ const estimates = await _controllerutils.handleFetch.call(void 0, -+ url, -+ clientId ? { headers: makeClientIdHeader(clientId) } : void 0 -+ ); -+ return { -+ low: { -+ ...estimates.low, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.low.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.low.suggestedMaxFeePerGas -+ ) -+ }, -+ medium: { -+ ...estimates.medium, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.medium.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.medium.suggestedMaxFeePerGas -+ ) -+ }, -+ high: { -+ ...estimates.high, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.high.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.high.suggestedMaxFeePerGas -+ ) -+ }, -+ estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee), -+ historicalBaseFeeRange: estimates.historicalBaseFeeRange, -+ baseFeeTrend: estimates.baseFeeTrend, -+ latestPriorityFeeRange: estimates.latestPriorityFeeRange, -+ historicalPriorityFeeRange: estimates.historicalPriorityFeeRange, -+ priorityFeeTrend: estimates.priorityFeeTrend, -+ networkCongestion: estimates.networkCongestion -+ }; -+} -+async function fetchLegacyGasPriceEstimates(url, clientId) { -+ const result = await _controllerutils.handleFetch.call(void 0, url, { -+ referrer: url, -+ referrerPolicy: "no-referrer-when-downgrade", -+ method: "GET", -+ mode: "cors", -+ headers: { -+ "Content-Type": "application/json", -+ ...clientId && makeClientIdHeader(clientId) -+ } -+ }); -+ return { -+ low: result.SafeGasPrice, -+ medium: result.ProposeGasPrice, -+ high: result.FastGasPrice -+ }; -+} -+async function fetchEthGasPriceEstimate(ethQuery) { -+ const gasPrice = await _controllerutils.query.call(void 0, ethQuery, "gasPrice"); -+ return { -+ gasPrice: _controllerutils.weiHexToGweiDec.call(void 0, gasPrice).toString() -+ }; -+} -+function calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, gasFeeEstimates) { -+ const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; -+ const maxPriorityFeePerGasInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, maxPriorityFeePerGas); -+ const maxFeePerGasInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, maxFeePerGas); -+ const estimatedBaseFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, estimatedBaseFee); -+ const effectiveMaxPriorityFee = _bnjs2.default.min( -+ maxPriorityFeePerGasInWEI, -+ maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI) -+ ); -+ const lowMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -+ low.suggestedMaxPriorityFeePerGas -+ ); -+ const mediumMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -+ medium.suggestedMaxPriorityFeePerGas -+ ); -+ const highMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -+ high.suggestedMaxPriorityFeePerGas -+ ); -+ let lowerTimeBound; -+ let upperTimeBound; -+ if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) { -+ lowerTimeBound = null; -+ upperTimeBound = "unknown"; -+ } else if (effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)) { -+ lowerTimeBound = low.minWaitTimeEstimate; -+ upperTimeBound = low.maxWaitTimeEstimate; -+ } else if (effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)) { -+ lowerTimeBound = medium.minWaitTimeEstimate; -+ upperTimeBound = medium.maxWaitTimeEstimate; -+ } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) { -+ lowerTimeBound = high.minWaitTimeEstimate; -+ upperTimeBound = high.maxWaitTimeEstimate; -+ } else { -+ lowerTimeBound = 0; -+ upperTimeBound = high.maxWaitTimeEstimate; -+ } -+ return { -+ lowerTimeBound, -+ upperTimeBound -+ }; -+} -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+exports.__privateGet = __privateGet; exports.__privateAdd = __privateAdd; exports.__privateSet = __privateSet; exports.__privateMethod = __privateMethod; exports.normalizeGWEIDecimalNumbers = normalizeGWEIDecimalNumbers; exports.fetchGasEstimates = fetchGasEstimates; exports.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates; exports.fetchEthGasPriceEstimate = fetchEthGasPriceEstimate; exports.calculateTimeEstimate = calculateTimeEstimate; -+//# sourceMappingURL=chunk-2MFVV2BX.js.map -\ No newline at end of file -diff --git a/dist/chunk-2MFVV2BX.js.map b/dist/chunk-2MFVV2BX.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..c6d83c1c168ee0b053be10a74334ac712bb6a6a1 ---- /dev/null -+++ b/dist/chunk-2MFVV2BX.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/gas-util.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n query,\n handleFetch,\n gweiDecToWEIBN,\n weiHexToGweiDec,\n} from '@metamask/controller-utils';\nimport type EthQuery from '@metamask/eth-query';\nimport BN from 'bn.js';\n\nimport type {\n GasFeeEstimates,\n EthGasPriceEstimate,\n EstimatedGasFeeTimeBounds,\n unknownString,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\n\nconst makeClientIdHeader = (clientId: string) => ({ 'X-Client-Id': clientId });\n\n/**\n * Convert a decimal GWEI value to a decimal string rounded to the nearest WEI.\n *\n * @param n - The input GWEI amount, as a decimal string or a number.\n * @returns The decimal string GWEI amount.\n */\nexport function normalizeGWEIDecimalNumbers(n: string | number) {\n const numberAsWEIHex = gweiDecToWEIBN(n).toString(16);\n const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex);\n return numberAsGWEI;\n}\n\n/**\n * Fetch gas estimates from the given URL.\n *\n * @param url - The gas estimate URL.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas estimates.\n */\nexport async function fetchGasEstimates(\n url: string,\n clientId?: string,\n): Promise {\n const estimates = await handleFetch(\n url,\n clientId ? { headers: makeClientIdHeader(clientId) } : undefined,\n );\n return {\n low: {\n ...estimates.low,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxFeePerGas,\n ),\n },\n medium: {\n ...estimates.medium,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxFeePerGas,\n ),\n },\n high: {\n ...estimates.high,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxFeePerGas,\n ),\n },\n estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),\n historicalBaseFeeRange: estimates.historicalBaseFeeRange,\n baseFeeTrend: estimates.baseFeeTrend,\n latestPriorityFeeRange: estimates.latestPriorityFeeRange,\n historicalPriorityFeeRange: estimates.historicalPriorityFeeRange,\n priorityFeeTrend: estimates.priorityFeeTrend,\n networkCongestion: estimates.networkCongestion,\n };\n}\n\n/**\n * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium\n * high values from that API.\n *\n * @param url - The URL to fetch gas price estimates from.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas price estimates.\n */\nexport async function fetchLegacyGasPriceEstimates(\n url: string,\n clientId?: string,\n): Promise {\n const result = await handleFetch(url, {\n referrer: url,\n referrerPolicy: 'no-referrer-when-downgrade',\n method: 'GET',\n mode: 'cors',\n headers: {\n 'Content-Type': 'application/json',\n ...(clientId && makeClientIdHeader(clientId)),\n },\n });\n return {\n low: result.SafeGasPrice,\n medium: result.ProposeGasPrice,\n high: result.FastGasPrice,\n };\n}\n\n/**\n * Get a gas price estimate from the network using the `eth_gasPrice` method.\n *\n * @param ethQuery - The EthQuery instance to call the network with.\n * @returns A gas price estimate.\n */\nexport async function fetchEthGasPriceEstimate(\n ethQuery: EthQuery,\n): Promise {\n const gasPrice = await query(ethQuery, 'gasPrice');\n return {\n gasPrice: weiHexToGweiDec(gasPrice).toString(),\n };\n}\n\n/**\n * Estimate the time it will take for a transaction to be confirmed.\n *\n * @param maxPriorityFeePerGas - The max priority fee per gas.\n * @param maxFeePerGas - The max fee per gas.\n * @param gasFeeEstimates - The gas fee estimates.\n * @returns The estimated lower and upper bounds for when this transaction will be confirmed.\n */\nexport function calculateTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n): EstimatedGasFeeTimeBounds {\n const { low, medium, high, estimatedBaseFee } = gasFeeEstimates;\n\n const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas);\n const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas);\n const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee);\n\n const effectiveMaxPriorityFee = BN.min(\n maxPriorityFeePerGasInWEI,\n maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI),\n );\n\n const lowMaxPriorityFeeInWEI = gweiDecToWEIBN(\n low.suggestedMaxPriorityFeePerGas,\n );\n const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN(\n medium.suggestedMaxPriorityFeePerGas,\n );\n const highMaxPriorityFeeInWEI = gweiDecToWEIBN(\n high.suggestedMaxPriorityFeePerGas,\n );\n\n let lowerTimeBound;\n let upperTimeBound;\n\n if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) {\n lowerTimeBound = null;\n upperTimeBound = 'unknown' as unknownString;\n } else if (\n effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = low.minWaitTimeEstimate;\n upperTimeBound = low.maxWaitTimeEstimate;\n } else if (\n effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = medium.minWaitTimeEstimate;\n upperTimeBound = medium.maxWaitTimeEstimate;\n } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) {\n lowerTimeBound = high.minWaitTimeEstimate;\n upperTimeBound = high.maxWaitTimeEstimate;\n } else {\n lowerTimeBound = 0;\n upperTimeBound = high.maxWaitTimeEstimate;\n }\n\n return {\n lowerTimeBound,\n upperTimeBound,\n };\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-A7NHJBXX.mjs b/dist/chunk-A7NHJBXX.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..1dd920c4a314888548db98885976b336d2fec79d ---- /dev/null -+++ b/dist/chunk-A7NHJBXX.mjs -@@ -0,0 +1,390 @@ -+import { -+ __privateAdd, -+ __privateGet, -+ __privateMethod, -+ __privateSet, -+ calculateTimeEstimate, -+ fetchEthGasPriceEstimate, -+ fetchGasEstimates, -+ fetchLegacyGasPriceEstimates -+} from "./chunk-R3IOI7AK.mjs"; -+ -+// src/GasFeeController.ts -+import { -+ convertHexToDecimal, -+ safelyExecute, -+ toHex -+} from "@metamask/controller-utils"; -+import EthQuery from "@metamask/eth-query"; -+import { StaticIntervalPollingController } from "@metamask/polling-controller"; -+import { v1 as random } from "uuid"; -+var LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`; -+var GAS_ESTIMATE_TYPES = { -+ FEE_MARKET: "fee-market", -+ LEGACY: "legacy", -+ ETH_GASPRICE: "eth_gasPrice", -+ NONE: "none" -+}; -+var metadata = { -+ gasFeeEstimatesByChainId: { -+ persist: true, -+ anonymous: false -+ }, -+ gasFeeEstimates: { persist: true, anonymous: false }, -+ estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, -+ gasEstimateType: { persist: true, anonymous: false }, -+ nonRPCGasFeeApisDisabled: { persist: true, anonymous: false } -+}; -+var name = "GasFeeController"; -+var defaultState = { -+ gasFeeEstimatesByChainId: {}, -+ gasFeeEstimates: {}, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.NONE, -+ nonRPCGasFeeApisDisabled: false -+}; -+var _getProvider, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn; -+var GasFeeController = class extends StaticIntervalPollingController { -+ /** -+ * Creates a GasFeeController instance. -+ * -+ * @param options - The controller options. -+ * @param options.interval - The time in milliseconds to wait between polls. -+ * @param options.messenger - The controller messenger. -+ * @param options.state - The initial state. -+ * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current -+ * network is EIP-1559 compatible. -+ * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the -+ * current network is compatible with the legacy gas price API. -+ * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current -+ * account is EIP-1559 compatible. -+ * @param options.getChainId - Returns the current chain ID. -+ * @param options.getProvider - Returns a network provider for the current network. -+ * @param options.onNetworkDidChange - A function for registering an event handler for the -+ * network state change event. -+ * @param options.legacyAPIEndpoint - The legacy gas price API URL. This option is primarily for -+ * testing purposes. -+ * @param options.EIP1559APIEndpoint - The EIP-1559 gas price API URL. -+ * @param options.clientId - The client ID used to identify to the gas estimation API who is -+ * asking for estimates. -+ */ -+ constructor({ -+ interval = 15e3, -+ messenger, -+ state, -+ getCurrentNetworkEIP1559Compatibility, -+ getCurrentAccountEIP1559Compatibility, -+ getChainId, -+ getCurrentNetworkLegacyGasAPICompatibility, -+ getProvider, -+ onNetworkDidChange, -+ legacyAPIEndpoint = LEGACY_GAS_PRICES_API_URL, -+ EIP1559APIEndpoint, -+ clientId -+ }) { -+ super({ -+ name, -+ metadata, -+ messenger, -+ state: { ...defaultState, ...state } -+ }); -+ __privateAdd(this, _onNetworkControllerDidChange); -+ __privateAdd(this, _getProvider, void 0); -+ this.intervalDelay = interval; -+ this.setIntervalLength(interval); -+ this.pollTokens = /* @__PURE__ */ new Set(); -+ this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; -+ this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; -+ this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; -+ __privateSet(this, _getProvider, getProvider); -+ this.EIP1559APIEndpoint = EIP1559APIEndpoint; -+ this.legacyAPIEndpoint = legacyAPIEndpoint; -+ this.clientId = clientId; -+ this.ethQuery = new EthQuery(__privateGet(this, _getProvider).call(this)); -+ if (onNetworkDidChange && getChainId) { -+ this.currentChainId = getChainId(); -+ onNetworkDidChange(async (networkControllerState) => { -+ await __privateMethod(this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -+ }); -+ } else { -+ this.currentChainId = this.messagingSystem.call( -+ "NetworkController:getState" -+ ).providerConfig.chainId; -+ this.messagingSystem.subscribe( -+ "NetworkController:networkDidChange", -+ async (networkControllerState) => { -+ await __privateMethod(this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -+ } -+ ); -+ } -+ } -+ async resetPolling() { -+ if (this.pollTokens.size !== 0) { -+ const tokens = Array.from(this.pollTokens); -+ this.stopPolling(); -+ await this.getGasFeeEstimatesAndStartPolling(tokens[0]); -+ tokens.slice(1).forEach((token) => { -+ this.pollTokens.add(token); -+ }); -+ } -+ } -+ async fetchGasFeeEstimates(options) { -+ return await this._fetchGasFeeEstimateData(options); -+ } -+ async getGasFeeEstimatesAndStartPolling(pollToken) { -+ const _pollToken = pollToken || random(); -+ this.pollTokens.add(_pollToken); -+ if (this.pollTokens.size === 1) { -+ await this._fetchGasFeeEstimateData(); -+ this._poll(); -+ } -+ return _pollToken; -+ } -+ /** -+ * Gets and sets gasFeeEstimates in state. -+ * -+ * @param options - The gas fee estimate options. -+ * @param options.shouldUpdateState - Determines whether the state should be updated with the -+ * updated gas estimates. -+ * @returns The gas fee estimates. -+ */ -+ async _fetchGasFeeEstimateData(options = {}) { -+ const { shouldUpdateState = true, networkClientId } = options; -+ let ethQuery, isEIP1559Compatible, isLegacyGasAPICompatible, decimalChainId; -+ if (networkClientId !== void 0) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ isLegacyGasAPICompatible = networkClient.configuration.chainId === "0x38"; -+ decimalChainId = convertHexToDecimal(networkClient.configuration.chainId); -+ try { -+ const result = await this.messagingSystem.call( -+ "NetworkController:getEIP1559Compatibility", -+ networkClientId -+ ); -+ isEIP1559Compatible = result || false; -+ } catch { -+ isEIP1559Compatible = false; -+ } -+ ethQuery = new EthQuery(networkClient.provider); -+ } -+ ethQuery ?? (ethQuery = this.ethQuery); -+ isLegacyGasAPICompatible ?? (isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility()); -+ decimalChainId ?? (decimalChainId = convertHexToDecimal(this.currentChainId)); -+ try { -+ isEIP1559Compatible ?? (isEIP1559Compatible = await this.getEIP1559Compatibility()); -+ } catch (e) { -+ console.error(e); -+ isEIP1559Compatible ?? (isEIP1559Compatible = false); -+ } -+ const gasFeeCalculations = await determineGasFeeCalculations({ -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ fetchGasEstimates, -+ fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchLegacyGasPriceEstimates, -+ fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchEthGasPriceEstimate, -+ calculateTimeEstimate, -+ clientId: this.clientId, -+ ethQuery, -+ nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled -+ }); -+ if (shouldUpdateState) { -+ const chainId = toHex(decimalChainId); -+ this.update((state) => { -+ if (this.currentChainId === chainId) { -+ state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -+ state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -+ state.gasEstimateType = gasFeeCalculations.gasEstimateType; -+ } -+ state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -+ state.gasFeeEstimatesByChainId[chainId] = { -+ gasFeeEstimates: gasFeeCalculations.gasFeeEstimates, -+ estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds, -+ gasEstimateType: gasFeeCalculations.gasEstimateType -+ }; -+ }); -+ } -+ return gasFeeCalculations; -+ } -+ /** -+ * Remove the poll token, and stop polling if the set of poll tokens is empty. -+ * -+ * @param pollToken - The poll token to disconnect. -+ */ -+ disconnectPoller(pollToken) { -+ this.pollTokens.delete(pollToken); -+ if (this.pollTokens.size === 0) { -+ this.stopPolling(); -+ } -+ } -+ stopPolling() { -+ if (this.intervalId) { -+ clearInterval(this.intervalId); -+ } -+ this.pollTokens.clear(); -+ this.resetState(); -+ } -+ /** -+ * Prepare to discard this controller. -+ * -+ * This stops any active polling. -+ */ -+ destroy() { -+ super.destroy(); -+ this.stopPolling(); -+ } -+ _poll() { -+ if (this.intervalId) { -+ clearInterval(this.intervalId); -+ } -+ this.intervalId = setInterval(async () => { -+ await safelyExecute(() => this._fetchGasFeeEstimateData()); -+ }, this.intervalDelay); -+ } -+ /** -+ * Fetching token list from the Token Service API. -+ * -+ * @private -+ * @param networkClientId - The ID of the network client triggering the fetch. -+ * @returns A promise that resolves when this operation completes. -+ */ -+ async _executePoll(networkClientId) { -+ await this._fetchGasFeeEstimateData({ networkClientId }); -+ } -+ resetState() { -+ this.update(() => { -+ return defaultState; -+ }); -+ } -+ async getEIP1559Compatibility() { -+ const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility(); -+ const currentAccountIsEIP1559Compatible = this.getCurrentAccountEIP1559Compatibility?.() ?? true; -+ return currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible; -+ } -+ getTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { -+ if (!this.state.gasFeeEstimates || this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) { -+ return {}; -+ } -+ return calculateTimeEstimate( -+ maxPriorityFeePerGas, -+ maxFeePerGas, -+ this.state.gasFeeEstimates -+ ); -+ } -+ enableNonRPCGasFeeApis() { -+ this.update((state) => { -+ state.nonRPCGasFeeApisDisabled = false; -+ }); -+ } -+ disableNonRPCGasFeeApis() { -+ this.update((state) => { -+ state.nonRPCGasFeeApisDisabled = true; -+ }); -+ } -+}; -+_getProvider = new WeakMap(); -+_onNetworkControllerDidChange = new WeakSet(); -+onNetworkControllerDidChange_fn = async function(networkControllerState) { -+ const newChainId = networkControllerState.providerConfig.chainId; -+ if (newChainId !== this.currentChainId) { -+ this.ethQuery = new EthQuery(__privateGet(this, _getProvider).call(this)); -+ await this.resetPolling(); -+ this.currentChainId = newChainId; -+ } -+}; -+var GasFeeController_default = GasFeeController; -+ -+// src/determineGasFeeCalculations.ts -+async function determineGasFeeCalculations(args) { -+ try { -+ return await getEstimatesUsingFallbacks(args); -+ } catch (error) { -+ if (error instanceof Error) { -+ throw new Error( -+ `Gas fee/price estimation failed. Message: ${error.message}` -+ ); -+ } -+ throw error; -+ } -+} -+async function getEstimatesUsingFallbacks(request) { -+ const { -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ nonRPCGasFeeApisDisabled -+ } = request; -+ try { -+ if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) { -+ return await getEstimatesUsingFeeMarketEndpoint(request); -+ } -+ if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) { -+ return await getEstimatesUsingLegacyEndpoint(request); -+ } -+ throw new Error("Main gas fee/price estimation failed. Use fallback"); -+ } catch { -+ return await getEstimatesUsingProvider(request); -+ } -+} -+async function getEstimatesUsingFeeMarketEndpoint(request) { -+ const { -+ fetchGasEstimates: fetchGasEstimates2, -+ fetchGasEstimatesUrl, -+ clientId, -+ calculateTimeEstimate: calculateTimeEstimate2 -+ } = request; -+ const estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -+ const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -+ const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -+ suggestedMaxPriorityFeePerGas, -+ suggestedMaxFeePerGas, -+ estimates -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds, -+ gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -+ }; -+} -+async function getEstimatesUsingLegacyEndpoint(request) { -+ const { -+ fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ } = request; -+ const estimates = await fetchLegacyGasPriceEstimates2( -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -+ }; -+} -+async function getEstimatesUsingProvider(request) { -+ const { ethQuery, fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2 } = request; -+ const estimates = await fetchEthGasPriceEstimate2(ethQuery); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -+ }; -+} -+ -+export { -+ determineGasFeeCalculations, -+ LEGACY_GAS_PRICES_API_URL, -+ GAS_ESTIMATE_TYPES, -+ GasFeeController, -+ GasFeeController_default -+}; -+//# sourceMappingURL=chunk-A7NHJBXX.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-A7NHJBXX.mjs.map b/dist/chunk-A7NHJBXX.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..d40e21b8870bb3ef2d3a092e9abdbb83527764f3 ---- /dev/null -+++ b/dist/chunk-A7NHJBXX.mjs.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n} from './gas-util';\n\nexport const LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`;\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.legacyAPIEndpoint - The legacy gas price API URL. This option is primarily for\n * testing purposes.\n * @param options.EIP1559APIEndpoint - The EIP-1559 gas price API URL.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n legacyAPIEndpoint = LEGACY_GAS_PRICES_API_URL,\n EIP1559APIEndpoint,\n clientId,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n legacyAPIEndpoint?: string;\n // eslint-disable-next-line @typescript-eslint/naming-convention\n EIP1559APIEndpoint: string;\n clientId?: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = EIP1559APIEndpoint;\n this.legacyAPIEndpoint = legacyAPIEndpoint;\n this.clientId = clientId;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"],"mappings":"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","names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"]} -\ No newline at end of file -diff --git a/dist/chunk-BEVZS3YV.mjs b/dist/chunk-BEVZS3YV.mjs -deleted file mode 100644 -index 3be5db0695ee6a2c71e64ed76c43a1f30cc9e5ac..0000000000000000000000000000000000000000 ---- a/dist/chunk-BEVZS3YV.mjs -+++ /dev/null -@@ -1,396 +0,0 @@ --import { -- __privateAdd, -- __privateGet, -- __privateMethod, -- __privateSet, -- calculateTimeEstimate, -- fetchEthGasPriceEstimate, -- fetchGasEstimates, -- fetchLegacyGasPriceEstimates --} from "./chunk-KORLXV32.mjs"; -- --// src/GasFeeController.ts --import { -- convertHexToDecimal, -- safelyExecute, -- toHex --} from "@metamask/controller-utils"; --import EthQuery from "@metamask/eth-query"; --import { StaticIntervalPollingController } from "@metamask/polling-controller"; --import { v1 as random } from "uuid"; --var GAS_API_BASE_URL = "https://gas.api.infura.io"; --var GAS_ESTIMATE_TYPES = { -- FEE_MARKET: "fee-market", -- LEGACY: "legacy", -- ETH_GASPRICE: "eth_gasPrice", -- NONE: "none" --}; --var metadata = { -- gasFeeEstimatesByChainId: { -- persist: true, -- anonymous: false -- }, -- gasFeeEstimates: { persist: true, anonymous: false }, -- estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, -- gasEstimateType: { persist: true, anonymous: false }, -- nonRPCGasFeeApisDisabled: { persist: true, anonymous: false } --}; --var name = "GasFeeController"; --var defaultState = { -- gasFeeEstimatesByChainId: {}, -- gasFeeEstimates: {}, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.NONE, -- nonRPCGasFeeApisDisabled: false --}; --var _getProvider, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn; --var GasFeeController = class extends StaticIntervalPollingController { -- /** -- * Creates a GasFeeController instance. -- * -- * @param options - The controller options. -- * @param options.interval - The time in milliseconds to wait between polls. -- * @param options.messenger - The controller messenger. -- * @param options.state - The initial state. -- * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current -- * network is EIP-1559 compatible. -- * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the -- * current network is compatible with the legacy gas price API. -- * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current -- * account is EIP-1559 compatible. -- * @param options.getChainId - Returns the current chain ID. -- * @param options.getProvider - Returns a network provider for the current network. -- * @param options.onNetworkDidChange - A function for registering an event handler for the -- * network state change event. -- * @param options.clientId - The client ID used to identify to the gas estimation API who is -- * asking for estimates. -- * @param options.infuraAPIKey - The Infura API key used for infura API requests. -- */ -- constructor({ -- interval = 15e3, -- messenger, -- state, -- getCurrentNetworkEIP1559Compatibility, -- getCurrentAccountEIP1559Compatibility, -- getChainId, -- getCurrentNetworkLegacyGasAPICompatibility, -- getProvider, -- onNetworkDidChange, -- clientId, -- infuraAPIKey -- }) { -- super({ -- name, -- metadata, -- messenger, -- state: { ...defaultState, ...state } -- }); -- __privateAdd(this, _onNetworkControllerDidChange); -- __privateAdd(this, _getProvider, void 0); -- this.intervalDelay = interval; -- this.setIntervalLength(interval); -- this.pollTokens = /* @__PURE__ */ new Set(); -- this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; -- this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; -- this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; -- __privateSet(this, _getProvider, getProvider); -- this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`; -- this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`; -- this.clientId = clientId; -- this.infuraAPIKey = infuraAPIKey; -- this.ethQuery = new EthQuery(__privateGet(this, _getProvider).call(this)); -- if (onNetworkDidChange && getChainId) { -- this.currentChainId = getChainId(); -- onNetworkDidChange(async (networkControllerState) => { -- await __privateMethod(this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -- }); -- } else { -- this.currentChainId = this.messagingSystem.call( -- "NetworkController:getState" -- ).providerConfig.chainId; -- this.messagingSystem.subscribe( -- "NetworkController:networkDidChange", -- async (networkControllerState) => { -- await __privateMethod(this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -- } -- ); -- } -- } -- async resetPolling() { -- if (this.pollTokens.size !== 0) { -- const tokens = Array.from(this.pollTokens); -- this.stopPolling(); -- await this.getGasFeeEstimatesAndStartPolling(tokens[0]); -- tokens.slice(1).forEach((token) => { -- this.pollTokens.add(token); -- }); -- } -- } -- async fetchGasFeeEstimates(options) { -- return await this._fetchGasFeeEstimateData(options); -- } -- async getGasFeeEstimatesAndStartPolling(pollToken) { -- const _pollToken = pollToken || random(); -- this.pollTokens.add(_pollToken); -- if (this.pollTokens.size === 1) { -- await this._fetchGasFeeEstimateData(); -- this._poll(); -- } -- return _pollToken; -- } -- /** -- * Gets and sets gasFeeEstimates in state. -- * -- * @param options - The gas fee estimate options. -- * @param options.shouldUpdateState - Determines whether the state should be updated with the -- * updated gas estimates. -- * @returns The gas fee estimates. -- */ -- async _fetchGasFeeEstimateData(options = {}) { -- const { shouldUpdateState = true, networkClientId } = options; -- let ethQuery, isEIP1559Compatible, isLegacyGasAPICompatible, decimalChainId; -- if (networkClientId !== void 0) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- isLegacyGasAPICompatible = networkClient.configuration.chainId === "0x38"; -- decimalChainId = convertHexToDecimal(networkClient.configuration.chainId); -- try { -- const result = await this.messagingSystem.call( -- "NetworkController:getEIP1559Compatibility", -- networkClientId -- ); -- isEIP1559Compatible = result || false; -- } catch { -- isEIP1559Compatible = false; -- } -- ethQuery = new EthQuery(networkClient.provider); -- } -- ethQuery ?? (ethQuery = this.ethQuery); -- isLegacyGasAPICompatible ?? (isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility()); -- decimalChainId ?? (decimalChainId = convertHexToDecimal(this.currentChainId)); -- try { -- isEIP1559Compatible ?? (isEIP1559Compatible = await this.getEIP1559Compatibility()); -- } catch (e) { -- console.error(e); -- isEIP1559Compatible ?? (isEIP1559Compatible = false); -- } -- const gasFeeCalculations = await determineGasFeeCalculations({ -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- fetchGasEstimates, -- fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchLegacyGasPriceEstimates, -- fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchEthGasPriceEstimate, -- calculateTimeEstimate, -- clientId: this.clientId, -- ethQuery, -- infuraAPIKey: this.infuraAPIKey, -- nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled -- }); -- if (shouldUpdateState) { -- const chainId = toHex(decimalChainId); -- this.update((state) => { -- if (this.currentChainId === chainId) { -- state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -- state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -- state.gasEstimateType = gasFeeCalculations.gasEstimateType; -- } -- state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -- state.gasFeeEstimatesByChainId[chainId] = { -- gasFeeEstimates: gasFeeCalculations.gasFeeEstimates, -- estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds, -- gasEstimateType: gasFeeCalculations.gasEstimateType -- }; -- }); -- } -- return gasFeeCalculations; -- } -- /** -- * Remove the poll token, and stop polling if the set of poll tokens is empty. -- * -- * @param pollToken - The poll token to disconnect. -- */ -- disconnectPoller(pollToken) { -- this.pollTokens.delete(pollToken); -- if (this.pollTokens.size === 0) { -- this.stopPolling(); -- } -- } -- stopPolling() { -- if (this.intervalId) { -- clearInterval(this.intervalId); -- } -- this.pollTokens.clear(); -- this.resetState(); -- } -- /** -- * Prepare to discard this controller. -- * -- * This stops any active polling. -- */ -- destroy() { -- super.destroy(); -- this.stopPolling(); -- } -- _poll() { -- if (this.intervalId) { -- clearInterval(this.intervalId); -- } -- this.intervalId = setInterval(async () => { -- await safelyExecute(() => this._fetchGasFeeEstimateData()); -- }, this.intervalDelay); -- } -- /** -- * Fetching token list from the Token Service API. -- * -- * @private -- * @param networkClientId - The ID of the network client triggering the fetch. -- * @returns A promise that resolves when this operation completes. -- */ -- async _executePoll(networkClientId) { -- await this._fetchGasFeeEstimateData({ networkClientId }); -- } -- resetState() { -- this.update(() => { -- return defaultState; -- }); -- } -- async getEIP1559Compatibility() { -- const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility(); -- const currentAccountIsEIP1559Compatible = this.getCurrentAccountEIP1559Compatibility?.() ?? true; -- return currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible; -- } -- getTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { -- if (!this.state.gasFeeEstimates || this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) { -- return {}; -- } -- return calculateTimeEstimate( -- maxPriorityFeePerGas, -- maxFeePerGas, -- this.state.gasFeeEstimates -- ); -- } -- enableNonRPCGasFeeApis() { -- this.update((state) => { -- state.nonRPCGasFeeApisDisabled = false; -- }); -- } -- disableNonRPCGasFeeApis() { -- this.update((state) => { -- state.nonRPCGasFeeApisDisabled = true; -- }); -- } --}; --_getProvider = new WeakMap(); --_onNetworkControllerDidChange = new WeakSet(); --onNetworkControllerDidChange_fn = async function(networkControllerState) { -- const newChainId = networkControllerState.providerConfig.chainId; -- if (newChainId !== this.currentChainId) { -- this.ethQuery = new EthQuery(__privateGet(this, _getProvider).call(this)); -- await this.resetPolling(); -- this.currentChainId = newChainId; -- } --}; --var GasFeeController_default = GasFeeController; -- --// src/determineGasFeeCalculations.ts --async function determineGasFeeCalculations(args) { -- try { -- return await getEstimatesUsingFallbacks(args); -- } catch (error) { -- if (error instanceof Error) { -- throw new Error( -- `Gas fee/price estimation failed. Message: ${error.message}` -- ); -- } -- throw error; -- } --} --async function getEstimatesUsingFallbacks(request) { -- const { -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- nonRPCGasFeeApisDisabled -- } = request; -- try { -- if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) { -- return await getEstimatesUsingFeeMarketEndpoint(request); -- } -- if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) { -- return await getEstimatesUsingLegacyEndpoint(request); -- } -- throw new Error("Main gas fee/price estimation failed. Use fallback"); -- } catch { -- return await getEstimatesUsingProvider(request); -- } --} --async function getEstimatesUsingFeeMarketEndpoint(request) { -- const { -- fetchGasEstimates: fetchGasEstimates2, -- fetchGasEstimatesUrl, -- infuraAPIKey, -- clientId, -- calculateTimeEstimate: calculateTimeEstimate2 -- } = request; -- const estimates = await fetchGasEstimates2( -- fetchGasEstimatesUrl, -- infuraAPIKey, -- clientId -- ); -- const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -- const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -- suggestedMaxPriorityFeePerGas, -- suggestedMaxFeePerGas, -- estimates -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds, -- gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -- }; --} --async function getEstimatesUsingLegacyEndpoint(request) { -- const { -- fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -- fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, -- clientId -- } = request; -- const estimates = await fetchLegacyGasPriceEstimates2( -- fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, -- clientId -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -- }; --} --async function getEstimatesUsingProvider(request) { -- const { ethQuery, fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2 } = request; -- const estimates = await fetchEthGasPriceEstimate2(ethQuery); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -- }; --} -- --export { -- determineGasFeeCalculations, -- GAS_API_BASE_URL, -- GAS_ESTIMATE_TYPES, -- GasFeeController, -- GasFeeController_default --}; --//# sourceMappingURL=chunk-BEVZS3YV.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-BEVZS3YV.mjs.map b/dist/chunk-BEVZS3YV.mjs.map -deleted file mode 100644 -index fc90025f10e73e5cdafc8964cd84365e51ad0c42..0000000000000000000000000000000000000000 ---- a/dist/chunk-BEVZS3YV.mjs.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const GAS_API_BASE_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"],"mappings":"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","names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"]} -\ No newline at end of file -diff --git a/dist/chunk-H5WHAYLI.js b/dist/chunk-H5WHAYLI.js -deleted file mode 100644 -index 3d6f8458707153d0b3cdd19da2bce7cb32da8eef..0000000000000000000000000000000000000000 ---- a/dist/chunk-H5WHAYLI.js -+++ /dev/null -@@ -1,396 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -- -- -- -- -- -- -- -- --var _chunkQ2YPK5SLjs = require('./chunk-Q2YPK5SL.js'); -- --// src/GasFeeController.ts -- -- -- -- --var _controllerutils = require('@metamask/controller-utils'); --var _ethquery = require('@metamask/eth-query'); var _ethquery2 = _interopRequireDefault(_ethquery); --var _pollingcontroller = require('@metamask/polling-controller'); --var _uuid = require('uuid'); --var GAS_API_BASE_URL = "https://gas.api.infura.io"; --var GAS_ESTIMATE_TYPES = { -- FEE_MARKET: "fee-market", -- LEGACY: "legacy", -- ETH_GASPRICE: "eth_gasPrice", -- NONE: "none" --}; --var metadata = { -- gasFeeEstimatesByChainId: { -- persist: true, -- anonymous: false -- }, -- gasFeeEstimates: { persist: true, anonymous: false }, -- estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, -- gasEstimateType: { persist: true, anonymous: false }, -- nonRPCGasFeeApisDisabled: { persist: true, anonymous: false } --}; --var name = "GasFeeController"; --var defaultState = { -- gasFeeEstimatesByChainId: {}, -- gasFeeEstimates: {}, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.NONE, -- nonRPCGasFeeApisDisabled: false --}; --var _getProvider, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn; --var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingController { -- /** -- * Creates a GasFeeController instance. -- * -- * @param options - The controller options. -- * @param options.interval - The time in milliseconds to wait between polls. -- * @param options.messenger - The controller messenger. -- * @param options.state - The initial state. -- * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current -- * network is EIP-1559 compatible. -- * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the -- * current network is compatible with the legacy gas price API. -- * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current -- * account is EIP-1559 compatible. -- * @param options.getChainId - Returns the current chain ID. -- * @param options.getProvider - Returns a network provider for the current network. -- * @param options.onNetworkDidChange - A function for registering an event handler for the -- * network state change event. -- * @param options.clientId - The client ID used to identify to the gas estimation API who is -- * asking for estimates. -- * @param options.infuraAPIKey - The Infura API key used for infura API requests. -- */ -- constructor({ -- interval = 15e3, -- messenger, -- state, -- getCurrentNetworkEIP1559Compatibility, -- getCurrentAccountEIP1559Compatibility, -- getChainId, -- getCurrentNetworkLegacyGasAPICompatibility, -- getProvider, -- onNetworkDidChange, -- clientId, -- infuraAPIKey -- }) { -- super({ -- name, -- metadata, -- messenger, -- state: { ...defaultState, ...state } -- }); -- _chunkQ2YPK5SLjs.__privateAdd.call(void 0, this, _onNetworkControllerDidChange); -- _chunkQ2YPK5SLjs.__privateAdd.call(void 0, this, _getProvider, void 0); -- this.intervalDelay = interval; -- this.setIntervalLength(interval); -- this.pollTokens = /* @__PURE__ */ new Set(); -- this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; -- this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; -- this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; -- _chunkQ2YPK5SLjs.__privateSet.call(void 0, this, _getProvider, getProvider); -- this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`; -- this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`; -- this.clientId = clientId; -- this.infuraAPIKey = infuraAPIKey; -- this.ethQuery = new (0, _ethquery2.default)(_chunkQ2YPK5SLjs.__privateGet.call(void 0, this, _getProvider).call(this)); -- if (onNetworkDidChange && getChainId) { -- this.currentChainId = getChainId(); -- onNetworkDidChange(async (networkControllerState) => { -- await _chunkQ2YPK5SLjs.__privateMethod.call(void 0, this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -- }); -- } else { -- this.currentChainId = this.messagingSystem.call( -- "NetworkController:getState" -- ).providerConfig.chainId; -- this.messagingSystem.subscribe( -- "NetworkController:networkDidChange", -- async (networkControllerState) => { -- await _chunkQ2YPK5SLjs.__privateMethod.call(void 0, this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -- } -- ); -- } -- } -- async resetPolling() { -- if (this.pollTokens.size !== 0) { -- const tokens = Array.from(this.pollTokens); -- this.stopPolling(); -- await this.getGasFeeEstimatesAndStartPolling(tokens[0]); -- tokens.slice(1).forEach((token) => { -- this.pollTokens.add(token); -- }); -- } -- } -- async fetchGasFeeEstimates(options) { -- return await this._fetchGasFeeEstimateData(options); -- } -- async getGasFeeEstimatesAndStartPolling(pollToken) { -- const _pollToken = pollToken || _uuid.v1.call(void 0, ); -- this.pollTokens.add(_pollToken); -- if (this.pollTokens.size === 1) { -- await this._fetchGasFeeEstimateData(); -- this._poll(); -- } -- return _pollToken; -- } -- /** -- * Gets and sets gasFeeEstimates in state. -- * -- * @param options - The gas fee estimate options. -- * @param options.shouldUpdateState - Determines whether the state should be updated with the -- * updated gas estimates. -- * @returns The gas fee estimates. -- */ -- async _fetchGasFeeEstimateData(options = {}) { -- const { shouldUpdateState = true, networkClientId } = options; -- let ethQuery, isEIP1559Compatible, isLegacyGasAPICompatible, decimalChainId; -- if (networkClientId !== void 0) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- isLegacyGasAPICompatible = networkClient.configuration.chainId === "0x38"; -- decimalChainId = _controllerutils.convertHexToDecimal.call(void 0, networkClient.configuration.chainId); -- try { -- const result = await this.messagingSystem.call( -- "NetworkController:getEIP1559Compatibility", -- networkClientId -- ); -- isEIP1559Compatible = result || false; -- } catch { -- isEIP1559Compatible = false; -- } -- ethQuery = new (0, _ethquery2.default)(networkClient.provider); -- } -- ethQuery ?? (ethQuery = this.ethQuery); -- isLegacyGasAPICompatible ?? (isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility()); -- decimalChainId ?? (decimalChainId = _controllerutils.convertHexToDecimal.call(void 0, this.currentChainId)); -- try { -- isEIP1559Compatible ?? (isEIP1559Compatible = await this.getEIP1559Compatibility()); -- } catch (e) { -- console.error(e); -- isEIP1559Compatible ?? (isEIP1559Compatible = false); -- } -- const gasFeeCalculations = await determineGasFeeCalculations({ -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- fetchGasEstimates: _chunkQ2YPK5SLjs.fetchGasEstimates, -- fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchLegacyGasPriceEstimates: _chunkQ2YPK5SLjs.fetchLegacyGasPriceEstimates, -- fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchEthGasPriceEstimate: _chunkQ2YPK5SLjs.fetchEthGasPriceEstimate, -- calculateTimeEstimate: _chunkQ2YPK5SLjs.calculateTimeEstimate, -- clientId: this.clientId, -- ethQuery, -- infuraAPIKey: this.infuraAPIKey, -- nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled -- }); -- if (shouldUpdateState) { -- const chainId = _controllerutils.toHex.call(void 0, decimalChainId); -- this.update((state) => { -- if (this.currentChainId === chainId) { -- state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -- state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -- state.gasEstimateType = gasFeeCalculations.gasEstimateType; -- } -- state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -- state.gasFeeEstimatesByChainId[chainId] = { -- gasFeeEstimates: gasFeeCalculations.gasFeeEstimates, -- estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds, -- gasEstimateType: gasFeeCalculations.gasEstimateType -- }; -- }); -- } -- return gasFeeCalculations; -- } -- /** -- * Remove the poll token, and stop polling if the set of poll tokens is empty. -- * -- * @param pollToken - The poll token to disconnect. -- */ -- disconnectPoller(pollToken) { -- this.pollTokens.delete(pollToken); -- if (this.pollTokens.size === 0) { -- this.stopPolling(); -- } -- } -- stopPolling() { -- if (this.intervalId) { -- clearInterval(this.intervalId); -- } -- this.pollTokens.clear(); -- this.resetState(); -- } -- /** -- * Prepare to discard this controller. -- * -- * This stops any active polling. -- */ -- destroy() { -- super.destroy(); -- this.stopPolling(); -- } -- _poll() { -- if (this.intervalId) { -- clearInterval(this.intervalId); -- } -- this.intervalId = setInterval(async () => { -- await _controllerutils.safelyExecute.call(void 0, () => this._fetchGasFeeEstimateData()); -- }, this.intervalDelay); -- } -- /** -- * Fetching token list from the Token Service API. -- * -- * @private -- * @param networkClientId - The ID of the network client triggering the fetch. -- * @returns A promise that resolves when this operation completes. -- */ -- async _executePoll(networkClientId) { -- await this._fetchGasFeeEstimateData({ networkClientId }); -- } -- resetState() { -- this.update(() => { -- return defaultState; -- }); -- } -- async getEIP1559Compatibility() { -- const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility(); -- const currentAccountIsEIP1559Compatible = this.getCurrentAccountEIP1559Compatibility?.() ?? true; -- return currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible; -- } -- getTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { -- if (!this.state.gasFeeEstimates || this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) { -- return {}; -- } -- return _chunkQ2YPK5SLjs.calculateTimeEstimate.call(void 0, -- maxPriorityFeePerGas, -- maxFeePerGas, -- this.state.gasFeeEstimates -- ); -- } -- enableNonRPCGasFeeApis() { -- this.update((state) => { -- state.nonRPCGasFeeApisDisabled = false; -- }); -- } -- disableNonRPCGasFeeApis() { -- this.update((state) => { -- state.nonRPCGasFeeApisDisabled = true; -- }); -- } --}; --_getProvider = new WeakMap(); --_onNetworkControllerDidChange = new WeakSet(); --onNetworkControllerDidChange_fn = async function(networkControllerState) { -- const newChainId = networkControllerState.providerConfig.chainId; -- if (newChainId !== this.currentChainId) { -- this.ethQuery = new (0, _ethquery2.default)(_chunkQ2YPK5SLjs.__privateGet.call(void 0, this, _getProvider).call(this)); -- await this.resetPolling(); -- this.currentChainId = newChainId; -- } --}; --var GasFeeController_default = GasFeeController; -- --// src/determineGasFeeCalculations.ts --async function determineGasFeeCalculations(args) { -- try { -- return await getEstimatesUsingFallbacks(args); -- } catch (error) { -- if (error instanceof Error) { -- throw new Error( -- `Gas fee/price estimation failed. Message: ${error.message}` -- ); -- } -- throw error; -- } --} --async function getEstimatesUsingFallbacks(request) { -- const { -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- nonRPCGasFeeApisDisabled -- } = request; -- try { -- if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) { -- return await getEstimatesUsingFeeMarketEndpoint(request); -- } -- if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) { -- return await getEstimatesUsingLegacyEndpoint(request); -- } -- throw new Error("Main gas fee/price estimation failed. Use fallback"); -- } catch { -- return await getEstimatesUsingProvider(request); -- } --} --async function getEstimatesUsingFeeMarketEndpoint(request) { -- const { -- fetchGasEstimates: fetchGasEstimates2, -- fetchGasEstimatesUrl, -- infuraAPIKey, -- clientId, -- calculateTimeEstimate: calculateTimeEstimate2 -- } = request; -- const estimates = await fetchGasEstimates2( -- fetchGasEstimatesUrl, -- infuraAPIKey, -- clientId -- ); -- const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -- const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -- suggestedMaxPriorityFeePerGas, -- suggestedMaxFeePerGas, -- estimates -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds, -- gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -- }; --} --async function getEstimatesUsingLegacyEndpoint(request) { -- const { -- fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -- fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, -- clientId -- } = request; -- const estimates = await fetchLegacyGasPriceEstimates2( -- fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, -- clientId -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -- }; --} --async function getEstimatesUsingProvider(request) { -- const { ethQuery, fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2 } = request; -- const estimates = await fetchEthGasPriceEstimate2(ethQuery); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -- }; --} -- -- -- -- -- -- -- --exports.determineGasFeeCalculations = determineGasFeeCalculations; exports.GAS_API_BASE_URL = GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = GAS_ESTIMATE_TYPES; exports.GasFeeController = GasFeeController; exports.GasFeeController_default = GasFeeController_default; --//# sourceMappingURL=chunk-H5WHAYLI.js.map -\ No newline at end of file -diff --git a/dist/chunk-H5WHAYLI.js.map b/dist/chunk-H5WHAYLI.js.map -deleted file mode 100644 -index ed761f584584470a8176f029d9d860dc017428fc..0000000000000000000000000000000000000000 ---- a/dist/chunk-H5WHAYLI.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const GAS_API_BASE_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-KORLXV32.mjs b/dist/chunk-KORLXV32.mjs -deleted file mode 100644 -index a964582d5c161e6b650fe9447442ca9540f9fdb9..0000000000000000000000000000000000000000 ---- a/dist/chunk-KORLXV32.mjs -+++ /dev/null -@@ -1,165 +0,0 @@ --var __accessCheck = (obj, member, msg) => { -- if (!member.has(obj)) -- throw TypeError("Cannot " + msg); --}; --var __privateGet = (obj, member, getter) => { -- __accessCheck(obj, member, "read from private field"); -- return getter ? getter.call(obj) : member.get(obj); --}; --var __privateAdd = (obj, member, value) => { -- if (member.has(obj)) -- throw TypeError("Cannot add the same private member more than once"); -- member instanceof WeakSet ? member.add(obj) : member.set(obj, value); --}; --var __privateSet = (obj, member, value, setter) => { -- __accessCheck(obj, member, "write to private field"); -- setter ? setter.call(obj, value) : member.set(obj, value); -- return value; --}; --var __privateMethod = (obj, member, method) => { -- __accessCheck(obj, member, "access private method"); -- return method; --}; -- --// src/gas-util.ts --import { -- query, -- handleFetch, -- gweiDecToWEIBN, -- weiHexToGweiDec --} from "@metamask/controller-utils"; --import BN from "bn.js"; --var makeClientIdHeader = (clientId) => ({ "X-Client-Id": clientId }); --function normalizeGWEIDecimalNumbers(n) { -- const numberAsWEIHex = gweiDecToWEIBN(n).toString(16); -- const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex); -- return numberAsGWEI; --} --async function fetchGasEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -- const estimates = await handleFetch(url, { -- headers: getHeaders(infuraAuthToken, clientId) -- }); -- return { -- low: { -- ...estimates.low, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.low.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.low.suggestedMaxFeePerGas -- ) -- }, -- medium: { -- ...estimates.medium, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.medium.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.medium.suggestedMaxFeePerGas -- ) -- }, -- high: { -- ...estimates.high, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.high.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.high.suggestedMaxFeePerGas -- ) -- }, -- estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee), -- historicalBaseFeeRange: estimates.historicalBaseFeeRange, -- baseFeeTrend: estimates.baseFeeTrend, -- latestPriorityFeeRange: estimates.latestPriorityFeeRange, -- historicalPriorityFeeRange: estimates.historicalPriorityFeeRange, -- priorityFeeTrend: estimates.priorityFeeTrend, -- networkCongestion: estimates.networkCongestion -- }; --} --async function fetchLegacyGasPriceEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -- const result = await handleFetch(url, { -- referrer: url, -- referrerPolicy: "no-referrer-when-downgrade", -- method: "GET", -- mode: "cors", -- headers: getHeaders(infuraAuthToken, clientId) -- }); -- return { -- low: result.SafeGasPrice, -- medium: result.ProposeGasPrice, -- high: result.FastGasPrice -- }; --} --async function fetchEthGasPriceEstimate(ethQuery) { -- const gasPrice = await query(ethQuery, "gasPrice"); -- return { -- gasPrice: weiHexToGweiDec(gasPrice).toString() -- }; --} --function calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, gasFeeEstimates) { -- const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; -- const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas); -- const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas); -- const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee); -- const effectiveMaxPriorityFee = BN.min( -- maxPriorityFeePerGasInWEI, -- maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI) -- ); -- const lowMaxPriorityFeeInWEI = gweiDecToWEIBN( -- low.suggestedMaxPriorityFeePerGas -- ); -- const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN( -- medium.suggestedMaxPriorityFeePerGas -- ); -- const highMaxPriorityFeeInWEI = gweiDecToWEIBN( -- high.suggestedMaxPriorityFeePerGas -- ); -- let lowerTimeBound; -- let upperTimeBound; -- if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) { -- lowerTimeBound = null; -- upperTimeBound = "unknown"; -- } else if (effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)) { -- lowerTimeBound = low.minWaitTimeEstimate; -- upperTimeBound = low.maxWaitTimeEstimate; -- } else if (effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)) { -- lowerTimeBound = medium.minWaitTimeEstimate; -- upperTimeBound = medium.maxWaitTimeEstimate; -- } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) { -- lowerTimeBound = high.minWaitTimeEstimate; -- upperTimeBound = high.maxWaitTimeEstimate; -- } else { -- lowerTimeBound = 0; -- upperTimeBound = high.maxWaitTimeEstimate; -- } -- return { -- lowerTimeBound, -- upperTimeBound -- }; --} --function buildInfuraAuthToken(infuraAPIKey) { -- return Buffer.from(`${infuraAPIKey}:`).toString("base64"); --} --function getHeaders(infuraAuthToken, clientId) { -- return { -- "Content-Type": "application/json", -- Authorization: `Basic ${infuraAuthToken}`, -- // Only add the clientId header if clientId is a non-empty string -- ...clientId?.trim() ? makeClientIdHeader(clientId) : {} -- }; --} -- --export { -- __privateGet, -- __privateAdd, -- __privateSet, -- __privateMethod, -- normalizeGWEIDecimalNumbers, -- fetchGasEstimates, -- fetchLegacyGasPriceEstimates, -- fetchEthGasPriceEstimate, -- calculateTimeEstimate --}; --//# sourceMappingURL=chunk-KORLXV32.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-KORLXV32.mjs.map b/dist/chunk-KORLXV32.mjs.map -deleted file mode 100644 -index b95130543a5b39392214c5fe873ad367e6fa8af7..0000000000000000000000000000000000000000 ---- a/dist/chunk-KORLXV32.mjs.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/gas-util.ts"],"sourcesContent":["import {\n query,\n handleFetch,\n gweiDecToWEIBN,\n weiHexToGweiDec,\n} from '@metamask/controller-utils';\nimport type EthQuery from '@metamask/eth-query';\nimport BN from 'bn.js';\n\nimport type {\n GasFeeEstimates,\n EthGasPriceEstimate,\n EstimatedGasFeeTimeBounds,\n unknownString,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\n\nconst makeClientIdHeader = (clientId: string) => ({ 'X-Client-Id': clientId });\n\n/**\n * Convert a decimal GWEI value to a decimal string rounded to the nearest WEI.\n *\n * @param n - The input GWEI amount, as a decimal string or a number.\n * @returns The decimal string GWEI amount.\n */\nexport function normalizeGWEIDecimalNumbers(n: string | number) {\n const numberAsWEIHex = gweiDecToWEIBN(n).toString(16);\n const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex);\n return numberAsGWEI;\n}\n\n/**\n * Fetch gas estimates from the given URL.\n *\n * @param url - The gas estimate URL.\n * @param infuraAPIKey - The Infura API key used for infura API requests.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas estimates.\n */\nexport async function fetchGasEstimates(\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n): Promise {\n const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey);\n const estimates = await handleFetch(url, {\n headers: getHeaders(infuraAuthToken, clientId),\n });\n return {\n low: {\n ...estimates.low,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxFeePerGas,\n ),\n },\n medium: {\n ...estimates.medium,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxFeePerGas,\n ),\n },\n high: {\n ...estimates.high,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxFeePerGas,\n ),\n },\n estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),\n historicalBaseFeeRange: estimates.historicalBaseFeeRange,\n baseFeeTrend: estimates.baseFeeTrend,\n latestPriorityFeeRange: estimates.latestPriorityFeeRange,\n historicalPriorityFeeRange: estimates.historicalPriorityFeeRange,\n priorityFeeTrend: estimates.priorityFeeTrend,\n networkCongestion: estimates.networkCongestion,\n };\n}\n\n/**\n * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium\n * high values from that API.\n *\n * @param url - The URL to fetch gas price estimates from.\n * @param infuraAPIKey - The Infura API key used for infura API requests.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas price estimates.\n */\nexport async function fetchLegacyGasPriceEstimates(\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n): Promise {\n const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey);\n const result = await handleFetch(url, {\n referrer: url,\n referrerPolicy: 'no-referrer-when-downgrade',\n method: 'GET',\n mode: 'cors',\n headers: getHeaders(infuraAuthToken, clientId),\n });\n return {\n low: result.SafeGasPrice,\n medium: result.ProposeGasPrice,\n high: result.FastGasPrice,\n };\n}\n\n/**\n * Get a gas price estimate from the network using the `eth_gasPrice` method.\n *\n * @param ethQuery - The EthQuery instance to call the network with.\n * @returns A gas price estimate.\n */\nexport async function fetchEthGasPriceEstimate(\n ethQuery: EthQuery,\n): Promise {\n const gasPrice = await query(ethQuery, 'gasPrice');\n return {\n gasPrice: weiHexToGweiDec(gasPrice).toString(),\n };\n}\n\n/**\n * Estimate the time it will take for a transaction to be confirmed.\n *\n * @param maxPriorityFeePerGas - The max priority fee per gas.\n * @param maxFeePerGas - The max fee per gas.\n * @param gasFeeEstimates - The gas fee estimates.\n * @returns The estimated lower and upper bounds for when this transaction will be confirmed.\n */\nexport function calculateTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n): EstimatedGasFeeTimeBounds {\n const { low, medium, high, estimatedBaseFee } = gasFeeEstimates;\n\n const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas);\n const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas);\n const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee);\n\n const effectiveMaxPriorityFee = BN.min(\n maxPriorityFeePerGasInWEI,\n maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI),\n );\n\n const lowMaxPriorityFeeInWEI = gweiDecToWEIBN(\n low.suggestedMaxPriorityFeePerGas,\n );\n const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN(\n medium.suggestedMaxPriorityFeePerGas,\n );\n const highMaxPriorityFeeInWEI = gweiDecToWEIBN(\n high.suggestedMaxPriorityFeePerGas,\n );\n\n let lowerTimeBound;\n let upperTimeBound;\n\n if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) {\n lowerTimeBound = null;\n upperTimeBound = 'unknown' as unknownString;\n } else if (\n effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = low.minWaitTimeEstimate;\n upperTimeBound = low.maxWaitTimeEstimate;\n } else if (\n effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = medium.minWaitTimeEstimate;\n upperTimeBound = medium.maxWaitTimeEstimate;\n } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) {\n lowerTimeBound = high.minWaitTimeEstimate;\n upperTimeBound = high.maxWaitTimeEstimate;\n } else {\n lowerTimeBound = 0;\n upperTimeBound = high.maxWaitTimeEstimate;\n }\n\n return {\n lowerTimeBound,\n upperTimeBound,\n };\n}\n\n/**\n * Build an infura auth token from the given API key and secret.\n *\n * @param infuraAPIKey - The Infura API key.\n * @returns The base64 encoded auth token.\n */\nfunction buildInfuraAuthToken(infuraAPIKey: string) {\n // We intentionally leave the password empty, as Infura does not require one\n return Buffer.from(`${infuraAPIKey}:`).toString('base64');\n}\n\n/**\n * Get the headers for a request to the gas fee API.\n *\n * @param infuraAuthToken - The Infura auth token to use for the request.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The headers for the request.\n */\nfunction getHeaders(infuraAuthToken: string, clientId?: string) {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Basic ${infuraAuthToken}`,\n // Only add the clientId header if clientId is a non-empty string\n ...(clientId?.trim() ? makeClientIdHeader(clientId) : {}),\n };\n}\n"],"mappings":"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","names":[]} -\ No newline at end of file -diff --git a/dist/chunk-Q2YPK5SL.js b/dist/chunk-Q2YPK5SL.js -deleted file mode 100644 -index 154d3eafb98cdb84cda1a2ea60f6be8a75f4e7a6..0000000000000000000000000000000000000000 ---- a/dist/chunk-Q2YPK5SL.js -+++ /dev/null -@@ -1,165 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var __accessCheck = (obj, member, msg) => { -- if (!member.has(obj)) -- throw TypeError("Cannot " + msg); --}; --var __privateGet = (obj, member, getter) => { -- __accessCheck(obj, member, "read from private field"); -- return getter ? getter.call(obj) : member.get(obj); --}; --var __privateAdd = (obj, member, value) => { -- if (member.has(obj)) -- throw TypeError("Cannot add the same private member more than once"); -- member instanceof WeakSet ? member.add(obj) : member.set(obj, value); --}; --var __privateSet = (obj, member, value, setter) => { -- __accessCheck(obj, member, "write to private field"); -- setter ? setter.call(obj, value) : member.set(obj, value); -- return value; --}; --var __privateMethod = (obj, member, method) => { -- __accessCheck(obj, member, "access private method"); -- return method; --}; -- --// src/gas-util.ts -- -- -- -- -- --var _controllerutils = require('@metamask/controller-utils'); --var _bnjs = require('bn.js'); var _bnjs2 = _interopRequireDefault(_bnjs); --var makeClientIdHeader = (clientId) => ({ "X-Client-Id": clientId }); --function normalizeGWEIDecimalNumbers(n) { -- const numberAsWEIHex = _controllerutils.gweiDecToWEIBN.call(void 0, n).toString(16); -- const numberAsGWEI = _controllerutils.weiHexToGweiDec.call(void 0, numberAsWEIHex); -- return numberAsGWEI; --} --async function fetchGasEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -- const estimates = await _controllerutils.handleFetch.call(void 0, url, { -- headers: getHeaders(infuraAuthToken, clientId) -- }); -- return { -- low: { -- ...estimates.low, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.low.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.low.suggestedMaxFeePerGas -- ) -- }, -- medium: { -- ...estimates.medium, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.medium.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.medium.suggestedMaxFeePerGas -- ) -- }, -- high: { -- ...estimates.high, -- suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.high.suggestedMaxPriorityFeePerGas -- ), -- suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -- estimates.high.suggestedMaxFeePerGas -- ) -- }, -- estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee), -- historicalBaseFeeRange: estimates.historicalBaseFeeRange, -- baseFeeTrend: estimates.baseFeeTrend, -- latestPriorityFeeRange: estimates.latestPriorityFeeRange, -- historicalPriorityFeeRange: estimates.historicalPriorityFeeRange, -- priorityFeeTrend: estimates.priorityFeeTrend, -- networkCongestion: estimates.networkCongestion -- }; --} --async function fetchLegacyGasPriceEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -- const result = await _controllerutils.handleFetch.call(void 0, url, { -- referrer: url, -- referrerPolicy: "no-referrer-when-downgrade", -- method: "GET", -- mode: "cors", -- headers: getHeaders(infuraAuthToken, clientId) -- }); -- return { -- low: result.SafeGasPrice, -- medium: result.ProposeGasPrice, -- high: result.FastGasPrice -- }; --} --async function fetchEthGasPriceEstimate(ethQuery) { -- const gasPrice = await _controllerutils.query.call(void 0, ethQuery, "gasPrice"); -- return { -- gasPrice: _controllerutils.weiHexToGweiDec.call(void 0, gasPrice).toString() -- }; --} --function calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, gasFeeEstimates) { -- const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; -- const maxPriorityFeePerGasInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, maxPriorityFeePerGas); -- const maxFeePerGasInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, maxFeePerGas); -- const estimatedBaseFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, estimatedBaseFee); -- const effectiveMaxPriorityFee = _bnjs2.default.min( -- maxPriorityFeePerGasInWEI, -- maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI) -- ); -- const lowMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -- low.suggestedMaxPriorityFeePerGas -- ); -- const mediumMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -- medium.suggestedMaxPriorityFeePerGas -- ); -- const highMaxPriorityFeeInWEI = _controllerutils.gweiDecToWEIBN.call(void 0, -- high.suggestedMaxPriorityFeePerGas -- ); -- let lowerTimeBound; -- let upperTimeBound; -- if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) { -- lowerTimeBound = null; -- upperTimeBound = "unknown"; -- } else if (effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)) { -- lowerTimeBound = low.minWaitTimeEstimate; -- upperTimeBound = low.maxWaitTimeEstimate; -- } else if (effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)) { -- lowerTimeBound = medium.minWaitTimeEstimate; -- upperTimeBound = medium.maxWaitTimeEstimate; -- } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) { -- lowerTimeBound = high.minWaitTimeEstimate; -- upperTimeBound = high.maxWaitTimeEstimate; -- } else { -- lowerTimeBound = 0; -- upperTimeBound = high.maxWaitTimeEstimate; -- } -- return { -- lowerTimeBound, -- upperTimeBound -- }; --} --function buildInfuraAuthToken(infuraAPIKey) { -- return Buffer.from(`${infuraAPIKey}:`).toString("base64"); --} --function getHeaders(infuraAuthToken, clientId) { -- return { -- "Content-Type": "application/json", -- Authorization: `Basic ${infuraAuthToken}`, -- // Only add the clientId header if clientId is a non-empty string -- ...clientId?.trim() ? makeClientIdHeader(clientId) : {} -- }; --} -- -- -- -- -- -- -- -- -- -- -- --exports.__privateGet = __privateGet; exports.__privateAdd = __privateAdd; exports.__privateSet = __privateSet; exports.__privateMethod = __privateMethod; exports.normalizeGWEIDecimalNumbers = normalizeGWEIDecimalNumbers; exports.fetchGasEstimates = fetchGasEstimates; exports.fetchLegacyGasPriceEstimates = fetchLegacyGasPriceEstimates; exports.fetchEthGasPriceEstimate = fetchEthGasPriceEstimate; exports.calculateTimeEstimate = calculateTimeEstimate; --//# sourceMappingURL=chunk-Q2YPK5SL.js.map -\ No newline at end of file -diff --git a/dist/chunk-Q2YPK5SL.js.map b/dist/chunk-Q2YPK5SL.js.map -deleted file mode 100644 -index dc1a17f2cd5fdd749b46fbb546375ea3a1925081..0000000000000000000000000000000000000000 ---- a/dist/chunk-Q2YPK5SL.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/gas-util.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n query,\n handleFetch,\n gweiDecToWEIBN,\n weiHexToGweiDec,\n} from '@metamask/controller-utils';\nimport type EthQuery from '@metamask/eth-query';\nimport BN from 'bn.js';\n\nimport type {\n GasFeeEstimates,\n EthGasPriceEstimate,\n EstimatedGasFeeTimeBounds,\n unknownString,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\n\nconst makeClientIdHeader = (clientId: string) => ({ 'X-Client-Id': clientId });\n\n/**\n * Convert a decimal GWEI value to a decimal string rounded to the nearest WEI.\n *\n * @param n - The input GWEI amount, as a decimal string or a number.\n * @returns The decimal string GWEI amount.\n */\nexport function normalizeGWEIDecimalNumbers(n: string | number) {\n const numberAsWEIHex = gweiDecToWEIBN(n).toString(16);\n const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex);\n return numberAsGWEI;\n}\n\n/**\n * Fetch gas estimates from the given URL.\n *\n * @param url - The gas estimate URL.\n * @param infuraAPIKey - The Infura API key used for infura API requests.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas estimates.\n */\nexport async function fetchGasEstimates(\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n): Promise {\n const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey);\n const estimates = await handleFetch(url, {\n headers: getHeaders(infuraAuthToken, clientId),\n });\n return {\n low: {\n ...estimates.low,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxFeePerGas,\n ),\n },\n medium: {\n ...estimates.medium,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxFeePerGas,\n ),\n },\n high: {\n ...estimates.high,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxFeePerGas,\n ),\n },\n estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),\n historicalBaseFeeRange: estimates.historicalBaseFeeRange,\n baseFeeTrend: estimates.baseFeeTrend,\n latestPriorityFeeRange: estimates.latestPriorityFeeRange,\n historicalPriorityFeeRange: estimates.historicalPriorityFeeRange,\n priorityFeeTrend: estimates.priorityFeeTrend,\n networkCongestion: estimates.networkCongestion,\n };\n}\n\n/**\n * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium\n * high values from that API.\n *\n * @param url - The URL to fetch gas price estimates from.\n * @param infuraAPIKey - The Infura API key used for infura API requests.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas price estimates.\n */\nexport async function fetchLegacyGasPriceEstimates(\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n): Promise {\n const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey);\n const result = await handleFetch(url, {\n referrer: url,\n referrerPolicy: 'no-referrer-when-downgrade',\n method: 'GET',\n mode: 'cors',\n headers: getHeaders(infuraAuthToken, clientId),\n });\n return {\n low: result.SafeGasPrice,\n medium: result.ProposeGasPrice,\n high: result.FastGasPrice,\n };\n}\n\n/**\n * Get a gas price estimate from the network using the `eth_gasPrice` method.\n *\n * @param ethQuery - The EthQuery instance to call the network with.\n * @returns A gas price estimate.\n */\nexport async function fetchEthGasPriceEstimate(\n ethQuery: EthQuery,\n): Promise {\n const gasPrice = await query(ethQuery, 'gasPrice');\n return {\n gasPrice: weiHexToGweiDec(gasPrice).toString(),\n };\n}\n\n/**\n * Estimate the time it will take for a transaction to be confirmed.\n *\n * @param maxPriorityFeePerGas - The max priority fee per gas.\n * @param maxFeePerGas - The max fee per gas.\n * @param gasFeeEstimates - The gas fee estimates.\n * @returns The estimated lower and upper bounds for when this transaction will be confirmed.\n */\nexport function calculateTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n): EstimatedGasFeeTimeBounds {\n const { low, medium, high, estimatedBaseFee } = gasFeeEstimates;\n\n const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas);\n const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas);\n const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee);\n\n const effectiveMaxPriorityFee = BN.min(\n maxPriorityFeePerGasInWEI,\n maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI),\n );\n\n const lowMaxPriorityFeeInWEI = gweiDecToWEIBN(\n low.suggestedMaxPriorityFeePerGas,\n );\n const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN(\n medium.suggestedMaxPriorityFeePerGas,\n );\n const highMaxPriorityFeeInWEI = gweiDecToWEIBN(\n high.suggestedMaxPriorityFeePerGas,\n );\n\n let lowerTimeBound;\n let upperTimeBound;\n\n if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) {\n lowerTimeBound = null;\n upperTimeBound = 'unknown' as unknownString;\n } else if (\n effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = low.minWaitTimeEstimate;\n upperTimeBound = low.maxWaitTimeEstimate;\n } else if (\n effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = medium.minWaitTimeEstimate;\n upperTimeBound = medium.maxWaitTimeEstimate;\n } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) {\n lowerTimeBound = high.minWaitTimeEstimate;\n upperTimeBound = high.maxWaitTimeEstimate;\n } else {\n lowerTimeBound = 0;\n upperTimeBound = high.maxWaitTimeEstimate;\n }\n\n return {\n lowerTimeBound,\n upperTimeBound,\n };\n}\n\n/**\n * Build an infura auth token from the given API key and secret.\n *\n * @param infuraAPIKey - The Infura API key.\n * @returns The base64 encoded auth token.\n */\nfunction buildInfuraAuthToken(infuraAPIKey: string) {\n // We intentionally leave the password empty, as Infura does not require one\n return Buffer.from(`${infuraAPIKey}:`).toString('base64');\n}\n\n/**\n * Get the headers for a request to the gas fee API.\n *\n * @param infuraAuthToken - The Infura auth token to use for the request.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The headers for the request.\n */\nfunction getHeaders(infuraAuthToken: string, clientId?: string) {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Basic ${infuraAuthToken}`,\n // Only add the clientId header if clientId is a non-empty string\n ...(clientId?.trim() ? makeClientIdHeader(clientId) : {}),\n };\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-R3IOI7AK.mjs b/dist/chunk-R3IOI7AK.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..005f6ca08cfdc55aabcf1849e2a3b651ee7d9303 ---- /dev/null -+++ b/dist/chunk-R3IOI7AK.mjs -@@ -0,0 +1,156 @@ -+var __accessCheck = (obj, member, msg) => { -+ if (!member.has(obj)) -+ throw TypeError("Cannot " + msg); -+}; -+var __privateGet = (obj, member, getter) => { -+ __accessCheck(obj, member, "read from private field"); -+ return getter ? getter.call(obj) : member.get(obj); -+}; -+var __privateAdd = (obj, member, value) => { -+ if (member.has(obj)) -+ throw TypeError("Cannot add the same private member more than once"); -+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -+}; -+var __privateSet = (obj, member, value, setter) => { -+ __accessCheck(obj, member, "write to private field"); -+ setter ? setter.call(obj, value) : member.set(obj, value); -+ return value; -+}; -+var __privateMethod = (obj, member, method) => { -+ __accessCheck(obj, member, "access private method"); -+ return method; -+}; -+ -+// src/gas-util.ts -+import { -+ query, -+ handleFetch, -+ gweiDecToWEIBN, -+ weiHexToGweiDec -+} from "@metamask/controller-utils"; -+import BN from "bn.js"; -+var makeClientIdHeader = (clientId) => ({ "X-Client-Id": clientId }); -+function normalizeGWEIDecimalNumbers(n) { -+ const numberAsWEIHex = gweiDecToWEIBN(n).toString(16); -+ const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex); -+ return numberAsGWEI; -+} -+async function fetchGasEstimates(url, clientId) { -+ const estimates = await handleFetch( -+ url, -+ clientId ? { headers: makeClientIdHeader(clientId) } : void 0 -+ ); -+ return { -+ low: { -+ ...estimates.low, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.low.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.low.suggestedMaxFeePerGas -+ ) -+ }, -+ medium: { -+ ...estimates.medium, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.medium.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.medium.suggestedMaxFeePerGas -+ ) -+ }, -+ high: { -+ ...estimates.high, -+ suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.high.suggestedMaxPriorityFeePerGas -+ ), -+ suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers( -+ estimates.high.suggestedMaxFeePerGas -+ ) -+ }, -+ estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee), -+ historicalBaseFeeRange: estimates.historicalBaseFeeRange, -+ baseFeeTrend: estimates.baseFeeTrend, -+ latestPriorityFeeRange: estimates.latestPriorityFeeRange, -+ historicalPriorityFeeRange: estimates.historicalPriorityFeeRange, -+ priorityFeeTrend: estimates.priorityFeeTrend, -+ networkCongestion: estimates.networkCongestion -+ }; -+} -+async function fetchLegacyGasPriceEstimates(url, clientId) { -+ const result = await handleFetch(url, { -+ referrer: url, -+ referrerPolicy: "no-referrer-when-downgrade", -+ method: "GET", -+ mode: "cors", -+ headers: { -+ "Content-Type": "application/json", -+ ...clientId && makeClientIdHeader(clientId) -+ } -+ }); -+ return { -+ low: result.SafeGasPrice, -+ medium: result.ProposeGasPrice, -+ high: result.FastGasPrice -+ }; -+} -+async function fetchEthGasPriceEstimate(ethQuery) { -+ const gasPrice = await query(ethQuery, "gasPrice"); -+ return { -+ gasPrice: weiHexToGweiDec(gasPrice).toString() -+ }; -+} -+function calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, gasFeeEstimates) { -+ const { low, medium, high, estimatedBaseFee } = gasFeeEstimates; -+ const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas); -+ const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas); -+ const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee); -+ const effectiveMaxPriorityFee = BN.min( -+ maxPriorityFeePerGasInWEI, -+ maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI) -+ ); -+ const lowMaxPriorityFeeInWEI = gweiDecToWEIBN( -+ low.suggestedMaxPriorityFeePerGas -+ ); -+ const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN( -+ medium.suggestedMaxPriorityFeePerGas -+ ); -+ const highMaxPriorityFeeInWEI = gweiDecToWEIBN( -+ high.suggestedMaxPriorityFeePerGas -+ ); -+ let lowerTimeBound; -+ let upperTimeBound; -+ if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) { -+ lowerTimeBound = null; -+ upperTimeBound = "unknown"; -+ } else if (effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)) { -+ lowerTimeBound = low.minWaitTimeEstimate; -+ upperTimeBound = low.maxWaitTimeEstimate; -+ } else if (effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) && effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)) { -+ lowerTimeBound = medium.minWaitTimeEstimate; -+ upperTimeBound = medium.maxWaitTimeEstimate; -+ } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) { -+ lowerTimeBound = high.minWaitTimeEstimate; -+ upperTimeBound = high.maxWaitTimeEstimate; -+ } else { -+ lowerTimeBound = 0; -+ upperTimeBound = high.maxWaitTimeEstimate; -+ } -+ return { -+ lowerTimeBound, -+ upperTimeBound -+ }; -+} -+ -+export { -+ __privateGet, -+ __privateAdd, -+ __privateSet, -+ __privateMethod, -+ normalizeGWEIDecimalNumbers, -+ fetchGasEstimates, -+ fetchLegacyGasPriceEstimates, -+ fetchEthGasPriceEstimate, -+ calculateTimeEstimate -+}; -+//# sourceMappingURL=chunk-R3IOI7AK.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-R3IOI7AK.mjs.map b/dist/chunk-R3IOI7AK.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..151a4aa1146e106a273c2e7cbfe8e97c7e6ae6b6 ---- /dev/null -+++ b/dist/chunk-R3IOI7AK.mjs.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/gas-util.ts"],"sourcesContent":["import {\n query,\n handleFetch,\n gweiDecToWEIBN,\n weiHexToGweiDec,\n} from '@metamask/controller-utils';\nimport type EthQuery from '@metamask/eth-query';\nimport BN from 'bn.js';\n\nimport type {\n GasFeeEstimates,\n EthGasPriceEstimate,\n EstimatedGasFeeTimeBounds,\n unknownString,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\n\nconst makeClientIdHeader = (clientId: string) => ({ 'X-Client-Id': clientId });\n\n/**\n * Convert a decimal GWEI value to a decimal string rounded to the nearest WEI.\n *\n * @param n - The input GWEI amount, as a decimal string or a number.\n * @returns The decimal string GWEI amount.\n */\nexport function normalizeGWEIDecimalNumbers(n: string | number) {\n const numberAsWEIHex = gweiDecToWEIBN(n).toString(16);\n const numberAsGWEI = weiHexToGweiDec(numberAsWEIHex);\n return numberAsGWEI;\n}\n\n/**\n * Fetch gas estimates from the given URL.\n *\n * @param url - The gas estimate URL.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas estimates.\n */\nexport async function fetchGasEstimates(\n url: string,\n clientId?: string,\n): Promise {\n const estimates = await handleFetch(\n url,\n clientId ? { headers: makeClientIdHeader(clientId) } : undefined,\n );\n return {\n low: {\n ...estimates.low,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.low.suggestedMaxFeePerGas,\n ),\n },\n medium: {\n ...estimates.medium,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.medium.suggestedMaxFeePerGas,\n ),\n },\n high: {\n ...estimates.high,\n suggestedMaxPriorityFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxPriorityFeePerGas,\n ),\n suggestedMaxFeePerGas: normalizeGWEIDecimalNumbers(\n estimates.high.suggestedMaxFeePerGas,\n ),\n },\n estimatedBaseFee: normalizeGWEIDecimalNumbers(estimates.estimatedBaseFee),\n historicalBaseFeeRange: estimates.historicalBaseFeeRange,\n baseFeeTrend: estimates.baseFeeTrend,\n latestPriorityFeeRange: estimates.latestPriorityFeeRange,\n historicalPriorityFeeRange: estimates.historicalPriorityFeeRange,\n priorityFeeTrend: estimates.priorityFeeTrend,\n networkCongestion: estimates.networkCongestion,\n };\n}\n\n/**\n * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium\n * high values from that API.\n *\n * @param url - The URL to fetch gas price estimates from.\n * @param clientId - The client ID used to identify to the API who is asking for estimates.\n * @returns The gas price estimates.\n */\nexport async function fetchLegacyGasPriceEstimates(\n url: string,\n clientId?: string,\n): Promise {\n const result = await handleFetch(url, {\n referrer: url,\n referrerPolicy: 'no-referrer-when-downgrade',\n method: 'GET',\n mode: 'cors',\n headers: {\n 'Content-Type': 'application/json',\n ...(clientId && makeClientIdHeader(clientId)),\n },\n });\n return {\n low: result.SafeGasPrice,\n medium: result.ProposeGasPrice,\n high: result.FastGasPrice,\n };\n}\n\n/**\n * Get a gas price estimate from the network using the `eth_gasPrice` method.\n *\n * @param ethQuery - The EthQuery instance to call the network with.\n * @returns A gas price estimate.\n */\nexport async function fetchEthGasPriceEstimate(\n ethQuery: EthQuery,\n): Promise {\n const gasPrice = await query(ethQuery, 'gasPrice');\n return {\n gasPrice: weiHexToGweiDec(gasPrice).toString(),\n };\n}\n\n/**\n * Estimate the time it will take for a transaction to be confirmed.\n *\n * @param maxPriorityFeePerGas - The max priority fee per gas.\n * @param maxFeePerGas - The max fee per gas.\n * @param gasFeeEstimates - The gas fee estimates.\n * @returns The estimated lower and upper bounds for when this transaction will be confirmed.\n */\nexport function calculateTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n): EstimatedGasFeeTimeBounds {\n const { low, medium, high, estimatedBaseFee } = gasFeeEstimates;\n\n const maxPriorityFeePerGasInWEI = gweiDecToWEIBN(maxPriorityFeePerGas);\n const maxFeePerGasInWEI = gweiDecToWEIBN(maxFeePerGas);\n const estimatedBaseFeeInWEI = gweiDecToWEIBN(estimatedBaseFee);\n\n const effectiveMaxPriorityFee = BN.min(\n maxPriorityFeePerGasInWEI,\n maxFeePerGasInWEI.sub(estimatedBaseFeeInWEI),\n );\n\n const lowMaxPriorityFeeInWEI = gweiDecToWEIBN(\n low.suggestedMaxPriorityFeePerGas,\n );\n const mediumMaxPriorityFeeInWEI = gweiDecToWEIBN(\n medium.suggestedMaxPriorityFeePerGas,\n );\n const highMaxPriorityFeeInWEI = gweiDecToWEIBN(\n high.suggestedMaxPriorityFeePerGas,\n );\n\n let lowerTimeBound;\n let upperTimeBound;\n\n if (effectiveMaxPriorityFee.lt(lowMaxPriorityFeeInWEI)) {\n lowerTimeBound = null;\n upperTimeBound = 'unknown' as unknownString;\n } else if (\n effectiveMaxPriorityFee.gte(lowMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(mediumMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = low.minWaitTimeEstimate;\n upperTimeBound = low.maxWaitTimeEstimate;\n } else if (\n effectiveMaxPriorityFee.gte(mediumMaxPriorityFeeInWEI) &&\n effectiveMaxPriorityFee.lt(highMaxPriorityFeeInWEI)\n ) {\n lowerTimeBound = medium.minWaitTimeEstimate;\n upperTimeBound = medium.maxWaitTimeEstimate;\n } else if (effectiveMaxPriorityFee.eq(highMaxPriorityFeeInWEI)) {\n lowerTimeBound = high.minWaitTimeEstimate;\n upperTimeBound = high.maxWaitTimeEstimate;\n } else {\n lowerTimeBound = 0;\n upperTimeBound = high.maxWaitTimeEstimate;\n }\n\n return {\n lowerTimeBound,\n upperTimeBound,\n };\n}\n"],"mappings":"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","names":[]} -\ No newline at end of file -diff --git a/dist/chunk-X74LQX2Y.js b/dist/chunk-X74LQX2Y.js -new file mode 100644 -index 0000000000000000000000000000000000000000..11d4e4b36c51872b5b529eb4e7d4f7eea91e87c7 ---- /dev/null -+++ b/dist/chunk-X74LQX2Y.js -@@ -0,0 +1,390 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -+ -+ -+ -+ -+ -+ -+ -+ -+var _chunk2MFVV2BXjs = require('./chunk-2MFVV2BX.js'); -+ -+// src/GasFeeController.ts -+ -+ -+ -+ -+var _controllerutils = require('@metamask/controller-utils'); -+var _ethquery = require('@metamask/eth-query'); var _ethquery2 = _interopRequireDefault(_ethquery); -+var _pollingcontroller = require('@metamask/polling-controller'); -+var _uuid = require('uuid'); -+var LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`; -+var GAS_ESTIMATE_TYPES = { -+ FEE_MARKET: "fee-market", -+ LEGACY: "legacy", -+ ETH_GASPRICE: "eth_gasPrice", -+ NONE: "none" -+}; -+var metadata = { -+ gasFeeEstimatesByChainId: { -+ persist: true, -+ anonymous: false -+ }, -+ gasFeeEstimates: { persist: true, anonymous: false }, -+ estimatedGasFeeTimeBounds: { persist: true, anonymous: false }, -+ gasEstimateType: { persist: true, anonymous: false }, -+ nonRPCGasFeeApisDisabled: { persist: true, anonymous: false } -+}; -+var name = "GasFeeController"; -+var defaultState = { -+ gasFeeEstimatesByChainId: {}, -+ gasFeeEstimates: {}, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.NONE, -+ nonRPCGasFeeApisDisabled: false -+}; -+var _getProvider, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn; -+var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingController { -+ /** -+ * Creates a GasFeeController instance. -+ * -+ * @param options - The controller options. -+ * @param options.interval - The time in milliseconds to wait between polls. -+ * @param options.messenger - The controller messenger. -+ * @param options.state - The initial state. -+ * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current -+ * network is EIP-1559 compatible. -+ * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the -+ * current network is compatible with the legacy gas price API. -+ * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current -+ * account is EIP-1559 compatible. -+ * @param options.getChainId - Returns the current chain ID. -+ * @param options.getProvider - Returns a network provider for the current network. -+ * @param options.onNetworkDidChange - A function for registering an event handler for the -+ * network state change event. -+ * @param options.legacyAPIEndpoint - The legacy gas price API URL. This option is primarily for -+ * testing purposes. -+ * @param options.EIP1559APIEndpoint - The EIP-1559 gas price API URL. -+ * @param options.clientId - The client ID used to identify to the gas estimation API who is -+ * asking for estimates. -+ */ -+ constructor({ -+ interval = 15e3, -+ messenger, -+ state, -+ getCurrentNetworkEIP1559Compatibility, -+ getCurrentAccountEIP1559Compatibility, -+ getChainId, -+ getCurrentNetworkLegacyGasAPICompatibility, -+ getProvider, -+ onNetworkDidChange, -+ legacyAPIEndpoint = LEGACY_GAS_PRICES_API_URL, -+ EIP1559APIEndpoint, -+ clientId -+ }) { -+ super({ -+ name, -+ metadata, -+ messenger, -+ state: { ...defaultState, ...state } -+ }); -+ _chunk2MFVV2BXjs.__privateAdd.call(void 0, this, _onNetworkControllerDidChange); -+ _chunk2MFVV2BXjs.__privateAdd.call(void 0, this, _getProvider, void 0); -+ this.intervalDelay = interval; -+ this.setIntervalLength(interval); -+ this.pollTokens = /* @__PURE__ */ new Set(); -+ this.getCurrentNetworkEIP1559Compatibility = getCurrentNetworkEIP1559Compatibility; -+ this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; -+ this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; -+ _chunk2MFVV2BXjs.__privateSet.call(void 0, this, _getProvider, getProvider); -+ this.EIP1559APIEndpoint = EIP1559APIEndpoint; -+ this.legacyAPIEndpoint = legacyAPIEndpoint; -+ this.clientId = clientId; -+ this.ethQuery = new (0, _ethquery2.default)(_chunk2MFVV2BXjs.__privateGet.call(void 0, this, _getProvider).call(this)); -+ if (onNetworkDidChange && getChainId) { -+ this.currentChainId = getChainId(); -+ onNetworkDidChange(async (networkControllerState) => { -+ await _chunk2MFVV2BXjs.__privateMethod.call(void 0, this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -+ }); -+ } else { -+ this.currentChainId = this.messagingSystem.call( -+ "NetworkController:getState" -+ ).providerConfig.chainId; -+ this.messagingSystem.subscribe( -+ "NetworkController:networkDidChange", -+ async (networkControllerState) => { -+ await _chunk2MFVV2BXjs.__privateMethod.call(void 0, this, _onNetworkControllerDidChange, onNetworkControllerDidChange_fn).call(this, networkControllerState); -+ } -+ ); -+ } -+ } -+ async resetPolling() { -+ if (this.pollTokens.size !== 0) { -+ const tokens = Array.from(this.pollTokens); -+ this.stopPolling(); -+ await this.getGasFeeEstimatesAndStartPolling(tokens[0]); -+ tokens.slice(1).forEach((token) => { -+ this.pollTokens.add(token); -+ }); -+ } -+ } -+ async fetchGasFeeEstimates(options) { -+ return await this._fetchGasFeeEstimateData(options); -+ } -+ async getGasFeeEstimatesAndStartPolling(pollToken) { -+ const _pollToken = pollToken || _uuid.v1.call(void 0, ); -+ this.pollTokens.add(_pollToken); -+ if (this.pollTokens.size === 1) { -+ await this._fetchGasFeeEstimateData(); -+ this._poll(); -+ } -+ return _pollToken; -+ } -+ /** -+ * Gets and sets gasFeeEstimates in state. -+ * -+ * @param options - The gas fee estimate options. -+ * @param options.shouldUpdateState - Determines whether the state should be updated with the -+ * updated gas estimates. -+ * @returns The gas fee estimates. -+ */ -+ async _fetchGasFeeEstimateData(options = {}) { -+ const { shouldUpdateState = true, networkClientId } = options; -+ let ethQuery, isEIP1559Compatible, isLegacyGasAPICompatible, decimalChainId; -+ if (networkClientId !== void 0) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ isLegacyGasAPICompatible = networkClient.configuration.chainId === "0x38"; -+ decimalChainId = _controllerutils.convertHexToDecimal.call(void 0, networkClient.configuration.chainId); -+ try { -+ const result = await this.messagingSystem.call( -+ "NetworkController:getEIP1559Compatibility", -+ networkClientId -+ ); -+ isEIP1559Compatible = result || false; -+ } catch { -+ isEIP1559Compatible = false; -+ } -+ ethQuery = new (0, _ethquery2.default)(networkClient.provider); -+ } -+ ethQuery ?? (ethQuery = this.ethQuery); -+ isLegacyGasAPICompatible ?? (isLegacyGasAPICompatible = this.getCurrentNetworkLegacyGasAPICompatibility()); -+ decimalChainId ?? (decimalChainId = _controllerutils.convertHexToDecimal.call(void 0, this.currentChainId)); -+ try { -+ isEIP1559Compatible ?? (isEIP1559Compatible = await this.getEIP1559Compatibility()); -+ } catch (e) { -+ console.error(e); -+ isEIP1559Compatible ?? (isEIP1559Compatible = false); -+ } -+ const gasFeeCalculations = await determineGasFeeCalculations({ -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ fetchGasEstimates: _chunk2MFVV2BXjs.fetchGasEstimates, -+ fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchLegacyGasPriceEstimates: _chunk2MFVV2BXjs.fetchLegacyGasPriceEstimates, -+ fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchEthGasPriceEstimate: _chunk2MFVV2BXjs.fetchEthGasPriceEstimate, -+ calculateTimeEstimate: _chunk2MFVV2BXjs.calculateTimeEstimate, -+ clientId: this.clientId, -+ ethQuery, -+ nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled -+ }); -+ if (shouldUpdateState) { -+ const chainId = _controllerutils.toHex.call(void 0, decimalChainId); -+ this.update((state) => { -+ if (this.currentChainId === chainId) { -+ state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -+ state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -+ state.gasEstimateType = gasFeeCalculations.gasEstimateType; -+ } -+ state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -+ state.gasFeeEstimatesByChainId[chainId] = { -+ gasFeeEstimates: gasFeeCalculations.gasFeeEstimates, -+ estimatedGasFeeTimeBounds: gasFeeCalculations.estimatedGasFeeTimeBounds, -+ gasEstimateType: gasFeeCalculations.gasEstimateType -+ }; -+ }); -+ } -+ return gasFeeCalculations; -+ } -+ /** -+ * Remove the poll token, and stop polling if the set of poll tokens is empty. -+ * -+ * @param pollToken - The poll token to disconnect. -+ */ -+ disconnectPoller(pollToken) { -+ this.pollTokens.delete(pollToken); -+ if (this.pollTokens.size === 0) { -+ this.stopPolling(); -+ } -+ } -+ stopPolling() { -+ if (this.intervalId) { -+ clearInterval(this.intervalId); -+ } -+ this.pollTokens.clear(); -+ this.resetState(); -+ } -+ /** -+ * Prepare to discard this controller. -+ * -+ * This stops any active polling. -+ */ -+ destroy() { -+ super.destroy(); -+ this.stopPolling(); -+ } -+ _poll() { -+ if (this.intervalId) { -+ clearInterval(this.intervalId); -+ } -+ this.intervalId = setInterval(async () => { -+ await _controllerutils.safelyExecute.call(void 0, () => this._fetchGasFeeEstimateData()); -+ }, this.intervalDelay); -+ } -+ /** -+ * Fetching token list from the Token Service API. -+ * -+ * @private -+ * @param networkClientId - The ID of the network client triggering the fetch. -+ * @returns A promise that resolves when this operation completes. -+ */ -+ async _executePoll(networkClientId) { -+ await this._fetchGasFeeEstimateData({ networkClientId }); -+ } -+ resetState() { -+ this.update(() => { -+ return defaultState; -+ }); -+ } -+ async getEIP1559Compatibility() { -+ const currentNetworkIsEIP1559Compatible = await this.getCurrentNetworkEIP1559Compatibility(); -+ const currentAccountIsEIP1559Compatible = this.getCurrentAccountEIP1559Compatibility?.() ?? true; -+ return currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible; -+ } -+ getTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { -+ if (!this.state.gasFeeEstimates || this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) { -+ return {}; -+ } -+ return _chunk2MFVV2BXjs.calculateTimeEstimate.call(void 0, -+ maxPriorityFeePerGas, -+ maxFeePerGas, -+ this.state.gasFeeEstimates -+ ); -+ } -+ enableNonRPCGasFeeApis() { -+ this.update((state) => { -+ state.nonRPCGasFeeApisDisabled = false; -+ }); -+ } -+ disableNonRPCGasFeeApis() { -+ this.update((state) => { -+ state.nonRPCGasFeeApisDisabled = true; -+ }); -+ } -+}; -+_getProvider = new WeakMap(); -+_onNetworkControllerDidChange = new WeakSet(); -+onNetworkControllerDidChange_fn = async function(networkControllerState) { -+ const newChainId = networkControllerState.providerConfig.chainId; -+ if (newChainId !== this.currentChainId) { -+ this.ethQuery = new (0, _ethquery2.default)(_chunk2MFVV2BXjs.__privateGet.call(void 0, this, _getProvider).call(this)); -+ await this.resetPolling(); -+ this.currentChainId = newChainId; -+ } -+}; -+var GasFeeController_default = GasFeeController; -+ -+// src/determineGasFeeCalculations.ts -+async function determineGasFeeCalculations(args) { -+ try { -+ return await getEstimatesUsingFallbacks(args); -+ } catch (error) { -+ if (error instanceof Error) { -+ throw new Error( -+ `Gas fee/price estimation failed. Message: ${error.message}` -+ ); -+ } -+ throw error; -+ } -+} -+async function getEstimatesUsingFallbacks(request) { -+ const { -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ nonRPCGasFeeApisDisabled -+ } = request; -+ try { -+ if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) { -+ return await getEstimatesUsingFeeMarketEndpoint(request); -+ } -+ if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) { -+ return await getEstimatesUsingLegacyEndpoint(request); -+ } -+ throw new Error("Main gas fee/price estimation failed. Use fallback"); -+ } catch { -+ return await getEstimatesUsingProvider(request); -+ } -+} -+async function getEstimatesUsingFeeMarketEndpoint(request) { -+ const { -+ fetchGasEstimates: fetchGasEstimates2, -+ fetchGasEstimatesUrl, -+ clientId, -+ calculateTimeEstimate: calculateTimeEstimate2 -+ } = request; -+ const estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -+ const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -+ const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -+ suggestedMaxPriorityFeePerGas, -+ suggestedMaxFeePerGas, -+ estimates -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds, -+ gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -+ }; -+} -+async function getEstimatesUsingLegacyEndpoint(request) { -+ const { -+ fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ } = request; -+ const estimates = await fetchLegacyGasPriceEstimates2( -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -+ }; -+} -+async function getEstimatesUsingProvider(request) { -+ const { ethQuery, fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2 } = request; -+ const estimates = await fetchEthGasPriceEstimate2(ethQuery); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -+ }; -+} -+ -+ -+ -+ -+ -+ -+ -+exports.determineGasFeeCalculations = determineGasFeeCalculations; exports.LEGACY_GAS_PRICES_API_URL = LEGACY_GAS_PRICES_API_URL; exports.GAS_ESTIMATE_TYPES = GAS_ESTIMATE_TYPES; exports.GasFeeController = GasFeeController; exports.GasFeeController_default = GasFeeController_default; -+//# sourceMappingURL=chunk-X74LQX2Y.js.map -\ No newline at end of file -diff --git a/dist/chunk-X74LQX2Y.js.map b/dist/chunk-X74LQX2Y.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..c330267c1f74907a70d669611fb5d8eff71305aa ---- /dev/null -+++ b/dist/chunk-X74LQX2Y.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"],"mappings":"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","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n} from './gas-util';\n\nexport const LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`;\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.legacyAPIEndpoint - The legacy gas price API URL. This option is primarily for\n * testing purposes.\n * @param options.EIP1559APIEndpoint - The EIP-1559 gas price API URL.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n legacyAPIEndpoint = LEGACY_GAS_PRICES_API_URL,\n EIP1559APIEndpoint,\n clientId,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n legacyAPIEndpoint?: string;\n // eslint-disable-next-line @typescript-eslint/naming-convention\n EIP1559APIEndpoint: string;\n clientId?: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = EIP1559APIEndpoint;\n this.legacyAPIEndpoint = legacyAPIEndpoint;\n this.clientId = clientId;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"]} -\ No newline at end of file -diff --git a/dist/determineGasFeeCalculations.js b/dist/determineGasFeeCalculations.js -index 87f583d091e574fc81b84ce35ee4b9979dbabdf6..ccfd460054f0affdec3857d8ec0015fe5c0814cb 100644 ---- a/dist/determineGasFeeCalculations.js -+++ b/dist/determineGasFeeCalculations.js -@@ -1,8 +1,8 @@ - "use strict";Object.defineProperty(exports, "__esModule", {value: true}); - --var _chunkH5WHAYLIjs = require('./chunk-H5WHAYLI.js'); --require('./chunk-Q2YPK5SL.js'); -+var _chunkX74LQX2Yjs = require('./chunk-X74LQX2Y.js'); -+require('./chunk-2MFVV2BX.js'); - - --exports.default = _chunkH5WHAYLIjs.determineGasFeeCalculations; -+exports.default = _chunkX74LQX2Yjs.determineGasFeeCalculations; - //# sourceMappingURL=determineGasFeeCalculations.js.map -\ No newline at end of file -diff --git a/dist/determineGasFeeCalculations.mjs b/dist/determineGasFeeCalculations.mjs -index b372041262f1d8372632922919f52aa2c6c5ee89..e5b349a8f349076b85481dceec5795b07aeb19ca 100644 ---- a/dist/determineGasFeeCalculations.mjs -+++ b/dist/determineGasFeeCalculations.mjs -@@ -1,7 +1,7 @@ - import { - determineGasFeeCalculations --} from "./chunk-BEVZS3YV.mjs"; --import "./chunk-KORLXV32.mjs"; -+} from "./chunk-A7NHJBXX.mjs"; -+import "./chunk-R3IOI7AK.mjs"; - export { - determineGasFeeCalculations as default - }; -diff --git a/dist/gas-util.js b/dist/gas-util.js -index 74c93749878df29b40eea3d998d26d734112d75c..aad155e1a969ac01a09f8e18becac39a79860199 100644 ---- a/dist/gas-util.js -+++ b/dist/gas-util.js -@@ -4,12 +4,12 @@ - - - --var _chunkQ2YPK5SLjs = require('./chunk-Q2YPK5SL.js'); -+var _chunk2MFVV2BXjs = require('./chunk-2MFVV2BX.js'); - - - - - - --exports.calculateTimeEstimate = _chunkQ2YPK5SLjs.calculateTimeEstimate; exports.fetchEthGasPriceEstimate = _chunkQ2YPK5SLjs.fetchEthGasPriceEstimate; exports.fetchGasEstimates = _chunkQ2YPK5SLjs.fetchGasEstimates; exports.fetchLegacyGasPriceEstimates = _chunkQ2YPK5SLjs.fetchLegacyGasPriceEstimates; exports.normalizeGWEIDecimalNumbers = _chunkQ2YPK5SLjs.normalizeGWEIDecimalNumbers; -+exports.calculateTimeEstimate = _chunk2MFVV2BXjs.calculateTimeEstimate; exports.fetchEthGasPriceEstimate = _chunk2MFVV2BXjs.fetchEthGasPriceEstimate; exports.fetchGasEstimates = _chunk2MFVV2BXjs.fetchGasEstimates; exports.fetchLegacyGasPriceEstimates = _chunk2MFVV2BXjs.fetchLegacyGasPriceEstimates; exports.normalizeGWEIDecimalNumbers = _chunk2MFVV2BXjs.normalizeGWEIDecimalNumbers; - //# sourceMappingURL=gas-util.js.map -\ No newline at end of file -diff --git a/dist/gas-util.mjs b/dist/gas-util.mjs -index c0846d32a7fe2598691c5c0bcc19ce2b8af67bc3..60f38a1b3899f0223fa7651988e57fa659e713e8 100644 ---- a/dist/gas-util.mjs -+++ b/dist/gas-util.mjs -@@ -4,7 +4,7 @@ import { - fetchGasEstimates, - fetchLegacyGasPriceEstimates, - normalizeGWEIDecimalNumbers --} from "./chunk-KORLXV32.mjs"; -+} from "./chunk-R3IOI7AK.mjs"; - export { - calculateTimeEstimate, - fetchEthGasPriceEstimate, -diff --git a/dist/index.js b/dist/index.js -index 499a7d57b68212db4402be72ab8d3384a841dba0..f172d463bef0093dd477dfad74ff50d30e42a7f0 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -2,11 +2,11 @@ - - - --var _chunkH5WHAYLIjs = require('./chunk-H5WHAYLI.js'); --require('./chunk-Q2YPK5SL.js'); -+var _chunkX74LQX2Yjs = require('./chunk-X74LQX2Y.js'); -+require('./chunk-2MFVV2BX.js'); - - - - --exports.GAS_API_BASE_URL = _chunkH5WHAYLIjs.GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; -+exports.GAS_ESTIMATE_TYPES = _chunkX74LQX2Yjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkX74LQX2Yjs.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkX74LQX2Yjs.LEGACY_GAS_PRICES_API_URL; - //# sourceMappingURL=index.js.map -\ No newline at end of file -diff --git a/dist/index.mjs b/dist/index.mjs -index 47fbe488932996ec192dd04a6b92d72c038681cc..71847186d44947ec3b2d75d142afbeede0aa64fc 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -1,12 +1,12 @@ - import { -- GAS_API_BASE_URL, - GAS_ESTIMATE_TYPES, -- GasFeeController --} from "./chunk-BEVZS3YV.mjs"; --import "./chunk-KORLXV32.mjs"; -+ GasFeeController, -+ LEGACY_GAS_PRICES_API_URL -+} from "./chunk-A7NHJBXX.mjs"; -+import "./chunk-R3IOI7AK.mjs"; - export { -- GAS_API_BASE_URL, - GAS_ESTIMATE_TYPES, -- GasFeeController -+ GasFeeController, -+ LEGACY_GAS_PRICES_API_URL - }; - //# sourceMappingURL=index.mjs.map -\ No newline at end of file -diff --git a/dist/tsconfig.build.tsbuildinfo b/dist/tsconfig.build.tsbuildinfo -index c226cf271a32a00bf6054fba381ed5f846ed9752..008d925dcefc659662cbfa15bd507ce773b8fc56 100644 ---- a/dist/tsconfig.build.tsbuildinfo -+++ b/dist/tsconfig.build.tsbuildinfo -@@ -1 +1 @@ --{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createProvider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../base-controller/dist/types/BaseControllerV1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/dom-events.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asyncEventEmitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacyTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/baseTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionFactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerClass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/RestrictedControllerMessenger.d.ts","../../base-controller/dist/types/ControllerMessenger.d.ts","../../base-controller/dist/types/BaseControllerV2.d.ts","../../base-controller/dist/types/index.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/types.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createEventEmitterProxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createSwappableProxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/index.d.ts","../../network-controller/dist/types/constants.d.ts","../../../node_modules/@metamask/safe-event-emitter/dist/cjs/index.d.ts","../../json-rpc-engine/dist/types/JsonRpcEngine.d.ts","../../json-rpc-engine/dist/types/createAsyncMiddleware.d.ts","../../json-rpc-engine/dist/types/createScaffoldMiddleware.d.ts","../../json-rpc-engine/dist/types/getUniqueId.d.ts","../../json-rpc-engine/dist/types/idRemapMiddleware.d.ts","../../json-rpc-engine/dist/types/mergeMiddleware.d.ts","../../json-rpc-engine/dist/types/index.d.ts","../../eth-json-rpc-provider/dist/types/safe-event-emitter-provider.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-engine.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-middleware.d.ts","../../eth-json-rpc-provider/dist/types/index.d.ts","../../../node_modules/eth-block-tracker/dist/BlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/PollingBlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/SubscribeBlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/index.d.ts","../../network-controller/dist/types/types.d.ts","../../network-controller/dist/types/create-auto-managed-network-client.d.ts","../../network-controller/dist/types/NetworkController.d.ts","../../network-controller/dist/types/create-network-client.d.ts","../../network-controller/dist/types/index.d.ts","../../polling-controller/dist/types/types.d.ts","../../polling-controller/dist/types/BlockTrackerPollingController.d.ts","../../polling-controller/dist/types/StaticIntervalPollingController.d.ts","../../polling-controller/dist/types/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../src/determineGasFeeCalculations.ts","../src/gas-util.ts","../src/GasFeeController.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupSemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/diffLines.d.ts","../../../node_modules/jest-diff/build/printDiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","5615ccf831db2ffc82145243081ebdb60ea8e1005ee8f975d1c0c1401a9c894e","38682ed3630bb6ecdace80d5a9adc811fc20a419f1940446e306c3a020d083b9","cc182e6e4f691cd6f7bf7cb491247a4c7818f9f1cb2db1d45c65ff906e3f741b","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","e0eda929c6b9b628cdeb0e54cd3582cb97e64f28aab34612fc1431c545899584","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","5b3cd03ae354ea96eff1f74d7c410fe4852e6382227e8b0ecf87ab5e3a5bbcd4","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"056097110efd16869ec118cedb44ecbac9a019576eee808d61304ca6d5cb2cbe","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"6fb8358e10ed92a7f515b7d79da3904c955a3ffd4e14aa9df6f0ea113041f1cf","affectsGlobalScope":true},"45c831238c6dac21c72da5f335747736a56a3847192bf03c84b958a7e9ec93e2","661a11d16ad2e3543a77c53bcd4017ee9a450f47ab7def3ab493a86eae4d550c",{"version":"8cdc646cec7819581ef343b83855b1bfe4fe674f2c84f4fb8dc90d82fb56bd3a","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","9dd56225cc2d8cb8fe5ceb0043ff386987637e12fecc6078896058a99deae284","2375ed4b439215aa3b6d0c6fd175c78a4384b30cb43cbadaecbf0a18954c98cb","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","41231da15bb5e3e806a8395bd15c7befd2ec90f9f4e3c9d0ae1356bccb76dbb0","fccfef201d057cb407fa515311bd608549bab6c7b8adcf8f2df31f5d3b796478",{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true},"5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"464762c6213566d072f1ced5e8e9a954785ec5e53883b7397198abb5ef5b8f71","affectsGlobalScope":true},"6387920dc3e18927335b086deec75bf8e50f879a5e273d32ee7bb7a55ba50572","9bba37424094688c4663c177a1379b229f919b8912889a472f32fdc5f08ddb4d","29a4be13b3a30d3e66667b75c58ec61fb2df8fa0422534fdee3cfb30c5dbf450","83366d901beda79d6eb37aaaf6ca248dcd88946302b2a7d975590783be51e88e","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","43ec77c369473e92e2ecebf0554a0fdaa9c256644a6070f28228dfcceec77351",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"95ed02bacb4502c985b69742ec82a4576d4ff4a6620ecc91593f611d502ae546","bf755525c4e6f85a970b98c4755d98e8aa1b6dbd83a5d8fcc57d3d497351b936","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"28084e15b63e6211769db2fe646d8bc5c4c6776321e0deffe2d12eefd52cb6b9","affectsGlobalScope":true},{"version":"aed37dabf86c99d6c8508700576ecede86688397bc12523541858705a0c737c2","affectsGlobalScope":true},"cc6ef5733d4ea6d2e06310a32dffd2c16418b467c5033d49cecc4f3a25de7497","94768454c3348b6ebe48e45fbad8c92e2bb7af4a35243edbe2b90823d0bd7f9a","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","84da3c28344e621fd1d591f2c09e9595292d2b70018da28a553268ac122597d4","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","6e191fea1db6e9e4fa828259cf489e820ec9170effff57fb081a2f3295db4722","aed943465fbce1efe49ee16b5ea409050f15cd8eaf116f6fadb64ef0772e7d95","70d08483a67bf7050dbedace398ef3fee9f436fcd60517c97c4c1e22e3c6f3e8","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"e933de8143e1d12dd51d89b398760fd5a9081896be366dad88a922d0b29f3c69","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b38d55d08708c2410a3039687db70b4a5bfa69fc4845617c313b5a10d9c5c637","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"a05e2d784c9be7051c4ac87a407c66d2106e23490c18c038bbd0712bde7602fd","affectsGlobalScope":true},{"version":"df90b9d0e9980762da8daf8adf6ffa0c853e76bfd269c377be0d07a9ad87acd2","affectsGlobalScope":true},"cf434b5c04792f62d6f4bdd5e2c8673f36e638e910333c172614d5def9b17f98","1d65d4798df9c2df008884035c41d3e67731f29db5ecb64cd7378797c7c53a2f","0faee6b555890a1cb106e2adc5d3ffd89545b1da894d474e9d436596d654998f","c6c01ea1c42508edf11a36d13b70f6e35774f74355ba5d358354d4a77cc67ea1","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b0297b09e607bec9698cac7cf55463d6731406efb1161ee4d448293b47397c84","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","68cc8d6fcc2f270d7108f02f3ebc59480a54615be3e09a47e14527f349e9d53e","3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","563fa27fdaec8f195b84f71a7af0ef48d30d5cc830575db86da86a63a470c8e6","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","d6e6620a30d582182acc3f0a992a0c311adc589f111096aea11ab83fc09a5ccc","6213b8f686f56beab22b59a0f468590fd3a4c5fa931236a017efeca91d7c9584","c451cec9a588b1f105a5ea2c6063d4fca112b9d70105cacdadda0e1ef67e9379","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","03460a54d0e0481d1e11097f66ad43f054bc95efdafe5f81bbc7a82be181af75","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","8129a34006218a6f3cdc81bbd438d5429eb18b08b4338a26977ac3b4df129d75","30d2170e1a718b5035611af55e3618b4ba8f42f0749bb52ee593da6082c4e2ce","98ef38666d88ec9699a722053e07ede65d3042f693fe7ff8c786e53dbb6fd43b","a3b8b6be7620897d1e481e8650c980a210a138fceb6e710eaf95fd9dd0dfe94a","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","14bd47270e654c8eb3b1489fa8c095912ee62a0a29bb92743393203722347c53","3d9297165e67fd59d9821cc93a9808213e33c56a8ac1c4273171f6afaaa2d4d5","e7af7d288b89287ad031b19583c597fcd9f5edc0b0d579b7b492f06cf57e058c","92cb686a9ca5eb5dd7d5d8d43a3707194c1e91ea07a027b3bcb60b6011b24632","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e",{"version":"f3e418819a6765dc6715ab2a51771c798543815d8499e29a857229bbf74ff419","signature":"b146c2211fb74ff8f672aa026d3992562e1c9c0d282cc712708b740c8f4cfb1a"},{"version":"d320adcc9e016b24e5873a7efb081d684a7990fbe23fe49c1c5354ba89bd129b","signature":"56c727d4c937865b6e7b114f58aa30f7678d5a9d924c0adb42368c403efb337d"},{"version":"fd1051fabcbc5740a5b3db8582c0bfee17883217121619fd765fbdd171f5d068","signature":"e4a433dbb39db571df95fa9677c8dede9cc6e21eb91a5a23e89ce2725cbd97da"},"d021f18758b28bda32bdaf0a987e0804cec074a9a4cfab8232ed81d96e75dfae","4489c6a9fde8934733aa7df6f7911461ee6e9e4ad092736bd416f6b2cc20b2c6","2c8e55457aaf4902941dfdba4061935922e8ee6e120539c9801cd7b400fae050","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","9d38964b57191567a14b396422c87488cecd48f405c642daa734159875ee81d9","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","8c95f96ccd4be0674944077aec1e4f2cccd515ca06d4327562dd017250e7d3fc",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","bc3cba7b0af2d52e7425299aee518db479d44004eff6fbbd206d1ee7e5ec3fb5","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","035a5df183489c2e22f3cf59fc1ed2b043d27f357eecc0eb8d8e840059d44245","a4809f4d92317535e6b22b01019437030077a76fec1d93b9881c9ed4738fcc54","5f53fa0bd22096d2a78533f94e02c899143b8f0f9891a46965294ee8b91a9434","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","b8442e9db28157344d1bc5d8a5a256f1692de213f0c0ddeb84359834015a008c","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","da2b6356b84a40111aaecb18304ea4e4fcb43d70efb1c13ca7d7a906445ee0d3","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","6f294731b495c65ecf46a5694f0082954b961cf05463bea823f8014098eaffa0","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","68a0d0c508e1b6d8d23a519a8a0a3303dc5baa4849ca049f21e5bad41945e3fc","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","b03afe4bec768ae333582915146f48b161e567a81b5ebc31c4d78af089770ac9","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","916be7d770b0ae0406be9486ac12eb9825f21514961dd050594c4b250617d5a8","254d9fb8c872d73d34594be8a200fd7311dbfa10a4116bfc465fba408052f2b3","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","2ccea88888048bbfcacbc9531a5596ea48a3e7dcd0a25f531a81bb717903ba4f","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","d8f7109e14f20eb735225a62fd3f8366da1a8349e90331cdad57f4b04caf6c5a","cf3d384d082b933d987c4e2fe7bfb8710adfd9dc8155190056ed6695a25a559e","9871b7ee672bc16c78833bdab3052615834b08375cb144e4d2cba74473f4a589","c863198dae89420f3c552b5a03da6ed6d0acfa3807a64772b895db624b0de707","8b03a5e327d7db67112ebbc93b4f744133eda2c1743dbb0a990c61a8007823ef","86c73f2ee1752bac8eeeece234fd05dfcf0637a4fbd8032e4f5f43102faa8eec","42fad1f540271e35ca37cecda12c4ce2eef27f0f5cf0f8dd761d723c744d3159","ff3743a5de32bee10906aff63d1de726f6a7fd6ee2da4b8229054dfa69de2c34","83acd370f7f84f203e71ebba33ba61b7f1291ca027d7f9a662c6307d74e4ac22","1445cec898f90bdd18b2949b9590b3c012f5b7e1804e6e329fb0fe053946d5ec","0e5318ec2275d8da858b541920d9306650ae6ac8012f0e872fe66eb50321a669","cf530297c3fb3a92ec9591dd4fa229d58b5981e45fe6702a0bd2bea53a5e59be","c1f6f7d08d42148ddfe164d36d7aba91f467dbcb3caa715966ff95f55048b3a4","f4e9bf9103191ef3b3612d3ec0044ca4044ca5be27711fe648ada06fad4bcc85","0c1ee27b8f6a00097c2d6d91a21ee4d096ab52c1e28350f6362542b55380059a","7677d5b0db9e020d3017720f853ba18f415219fb3a9597343b1b1012cfd699f7","bc1c6bc119c1784b1a2be6d9c47addec0d83ef0d52c8fbe1f14a51b4dfffc675","52cf2ce99c2a23de70225e252e9822a22b4e0adb82643ab0b710858810e00bf1","770625067bb27a20b9826255a8d47b6b5b0a2d3dfcbd21f89904c731f671ba77","d1ed6765f4d7906a05968fb5cd6d1db8afa14dbe512a4884e8ea5c0f5e142c80","799c0f1b07c092626cf1efd71d459997635911bb5f7fc1196efe449bba87e965","2a184e4462b9914a30b1b5c41cf80c6d3428f17b20d3afb711fff3f0644001fd","9eabde32a3aa5d80de34af2c2206cdc3ee094c6504a8d0c2d6d20c7c179503cc","397c8051b6cfcb48aa22656f0faca2553c5f56187262135162ee79d2b2f6c966","a8ead142e0c87dcd5dc130eba1f8eeed506b08952d905c47621dc2f583b1bff9","a02f10ea5f73130efca046429254a4e3c06b5475baecc8f7b99a0014731be8b3","c2576a4083232b0e2d9bd06875dd43d371dee2e090325a9eac0133fd5650c1cb","4c9a0564bb317349de6a24eb4efea8bb79898fa72ad63a1809165f5bd42970dd","f40ac11d8859092d20f953aae14ba967282c3bb056431a37fced1866ec7a2681","cc11e9e79d4746cc59e0e17473a59d6f104692fd0eeea1bdb2e206eabed83b03","b444a410d34fb5e98aa5ee2b381362044f4884652e8bc8a11c8fe14bbd85518e","c35808c1f5e16d2c571aa65067e3cb95afeff843b259ecfa2fc107a9519b5392","14d5dc055143e941c8743c6a21fa459f961cbc3deedf1bfe47b11587ca4b3ef5","a3ad4e1fc542751005267d50a6298e6765928c0c3a8dce1572f2ba6ca518661c","f237e7c97a3a89f4591afd49ecb3bd8d14f51a1c4adc8fcae3430febedff5eb6","3ffdfbec93b7aed71082af62b8c3e0cc71261cc68d796665faa1e91604fbae8f","662201f943ed45b1ad600d03a90dffe20841e725203ced8b708c91fcd7f9379a","c9ef74c64ed051ea5b958621e7fb853fe3b56e8787c1587aefc6ea988b3c7e79","2462ccfac5f3375794b861abaa81da380f1bbd9401de59ffa43119a0b644253d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","7d8ddf0f021c53099e34ee831a06c394d50371816caa98684812f089b4c6b3d4","7d2a0ba1297be385a89b5515b88cd31b4a1eeef5236f710166dc1b36b1741e1b","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","ab82804a14454734010dcdcd43f564ff7b0389bee4c5692eec76ff5b30d4cf66","bae8d023ef6b23df7da26f51cea44321f95817c190342a36882e93b80d07a960","ae271d475b632ce7b03fea6d9cf6da72439e57a109672671cbc79f54e1386938"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[234],[92,128,129,130,145],[129,130,146,147],[128,129],[128,145,148,151],[128,148,151,152],[149,150,151,153,154],[128,151],[128,145,148,149,150,153],[128,136],[128],[92,128],[80,128],[132,133,134,135,136,137,138,139,140,141,142,143,144],[128,134,135],[128,134,136],[199],[199,200,201],[64],[67],[64,67],[65,66,67,68,69,70,71,72,73,74,75,156,159,160,161,162,163,164,165,166],[58,64,65],[67,73,75,155],[158],[67,68],[64,162],[194,195],[234,235,236,237,238],[234,236],[157],[241,242,243],[93,128],[246],[247],[258],[252,257],[261,263,264,265,266,267,268,269,270,271,272,273],[261,262,264,265,266,267,268,269,270,271,272,273],[262,263,264,265,266,267,268,269,270,271,272,273],[261,262,263,265,266,267,268,269,270,271,272,273],[261,262,263,264,266,267,268,269,270,271,272,273],[261,262,263,264,265,267,268,269,270,271,272,273],[261,262,263,264,265,266,268,269,270,271,272,273],[261,262,263,264,265,266,267,269,270,271,272,273],[261,262,263,264,265,266,267,268,270,271,272,273],[261,262,263,264,265,266,267,268,269,271,272,273],[261,262,263,264,265,266,267,268,269,270,272,273],[261,262,263,264,265,266,267,268,269,270,271,273],[261,262,263,264,265,266,267,268,269,270,271,272],[76],[79],[80,85,112],[81,92,93,100,109,120],[81,82,92,100],[83,121],[84,85,93,101],[85,109,117],[86,88,92,100],[87],[88,89],[92],[91,92],[79,92],[92,93,94,109,120],[92,93,94,109],[92,95,100,109,120],[92,93,95,96,100,109,117,120],[95,97,109,117,120],[76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127],[92,98],[99,120,125],[88,92,100,109],[101],[102],[79,103],[104,119,125],[105],[106],[92,107],[107,108,121,123],[80,92,109,110,111],[80,109,111],[109,110],[112],[113],[92,115,116],[115,116],[85,100,109,117],[118],[100,119],[80,95,106,120],[85,121],[109,122],[123],[124],[80,85,92,94,103,109,120,123,125],[109,126],[128,279],[282,321],[282,306,321],[321],[282],[282,307,321],[282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320],[307,321],[322],[325],[204],[204,215,216],[216,217,218],[179],[179,180,181,182,183],[168,169,170,171,172,173,174,175,176,177,178],[250,253],[250,253,254,255],[252],[249,256],[251],[57,59,60,61,62,63],[57,58],[59],[58,59],[57,59],[167,184,185,186],[185],[186],[56,185,186,187],[189],[189,190,193,197],[196],[167,191,192],[212,213,214],[211,212],[167,211,212],[167,204,211],[167,188,191,198,224,228,229,230,231],[232],[191,192,198,232],[167,204],[167,205],[205,206,207,208,209,210],[167,188,191,198,202,203,220,221],[220],[203,220,222,223],[167,198,215,219],[167,188,224,225],[128,167,188,224,225],[225,226,227],[167,224],[167,188,224,228],[191,232]],"referencedMap":[[236,1],[146,2],[148,3],[130,4],[152,5],[153,6],[149,6],[155,7],[150,6],[154,8],[151,9],[137,10],[134,11],[141,12],[135,10],[132,13],[145,14],[139,11],[136,15],[138,16],[204,12],[200,17],[201,17],[202,18],[65,19],[66,19],[68,20],[69,19],[70,19],[71,21],[67,19],[167,22],[75,23],[156,24],[159,25],[165,26],[166,27],[196,28],[239,29],[235,1],[237,30],[238,1],[192,11],[158,31],[244,32],[245,33],[247,34],[248,35],[259,36],[258,37],[262,38],[263,39],[261,40],[264,41],[265,42],[266,43],[267,44],[268,45],[269,46],[270,47],[271,48],[272,49],[273,50],[76,51],[77,51],[79,52],[80,53],[81,54],[82,55],[83,56],[84,57],[85,58],[86,59],[87,60],[88,61],[89,61],[90,62],[91,63],[92,64],[93,65],[94,66],[95,67],[96,68],[97,69],[128,70],[98,71],[99,72],[100,73],[101,74],[102,75],[103,76],[104,77],[105,78],[106,79],[107,80],[108,81],[109,82],[111,83],[110,84],[112,85],[113,86],[115,87],[116,88],[117,89],[118,90],[119,91],[120,92],[121,93],[122,94],[123,95],[124,96],[125,97],[126,98],[276,11],[280,99],[281,11],[306,100],[307,101],[282,102],[285,102],[304,100],[305,100],[295,100],[294,103],[292,100],[287,100],[300,100],[298,100],[302,100],[286,100],[299,100],[303,100],[288,100],[289,100],[301,100],[283,100],[290,100],[291,100],[293,100],[297,100],[308,104],[296,100],[284,100],[321,105],[315,104],[317,106],[316,104],[309,104],[310,104],[312,104],[314,104],[318,106],[319,106],[311,106],[313,106],[323,107],[326,108],[216,109],[217,110],[218,110],[219,111],[175,112],[177,112],[176,112],[174,112],[184,113],[179,114],[170,112],[171,112],[172,112],[173,112],[254,115],[256,116],[255,115],[253,117],[257,118],[252,119],[64,120],[59,121],[60,122],[61,122],[62,123],[63,123],[58,124],[187,125],[186,126],[185,127],[188,128],[190,129],[198,130],[197,131],[193,132],[215,133],[213,134],[214,135],[212,136],[232,137],[230,138],[231,139],[233,138],[205,140],[206,141],[207,141],[209,141],[211,142],[210,141],[222,143],[221,144],[223,144],[224,145],[220,146],[226,147],[227,148],[228,149],[225,150]],"exportedModulesMap":[[236,1],[146,2],[148,3],[130,4],[152,5],[153,6],[149,6],[155,7],[150,6],[154,8],[151,9],[137,10],[134,11],[141,12],[135,10],[132,13],[145,14],[139,11],[136,15],[138,16],[204,12],[200,17],[201,17],[202,18],[65,19],[66,19],[68,20],[69,19],[70,19],[71,21],[67,19],[167,22],[75,23],[156,24],[159,25],[165,26],[166,27],[196,28],[239,29],[235,1],[237,30],[238,1],[192,11],[158,31],[244,32],[245,33],[247,34],[248,35],[259,36],[258,37],[262,38],[263,39],[261,40],[264,41],[265,42],[266,43],[267,44],[268,45],[269,46],[270,47],[271,48],[272,49],[273,50],[76,51],[77,51],[79,52],[80,53],[81,54],[82,55],[83,56],[84,57],[85,58],[86,59],[87,60],[88,61],[89,61],[90,62],[91,63],[92,64],[93,65],[94,66],[95,67],[96,68],[97,69],[128,70],[98,71],[99,72],[100,73],[101,74],[102,75],[103,76],[104,77],[105,78],[106,79],[107,80],[108,81],[109,82],[111,83],[110,84],[112,85],[113,86],[115,87],[116,88],[117,89],[118,90],[119,91],[120,92],[121,93],[122,94],[123,95],[124,96],[125,97],[126,98],[276,11],[280,99],[281,11],[306,100],[307,101],[282,102],[285,102],[304,100],[305,100],[295,100],[294,103],[292,100],[287,100],[300,100],[298,100],[302,100],[286,100],[299,100],[303,100],[288,100],[289,100],[301,100],[283,100],[290,100],[291,100],[293,100],[297,100],[308,104],[296,100],[284,100],[321,105],[315,104],[317,106],[316,104],[309,104],[310,104],[312,104],[314,104],[318,106],[319,106],[311,106],[313,106],[323,107],[326,108],[216,109],[217,110],[218,110],[219,111],[175,112],[177,112],[176,112],[174,112],[184,113],[179,114],[170,112],[171,112],[172,112],[173,112],[254,115],[256,116],[255,115],[253,117],[257,118],[252,119],[64,120],[59,121],[60,122],[61,122],[62,123],[63,123],[58,124],[187,125],[186,126],[185,127],[188,128],[190,129],[198,130],[197,131],[193,132],[215,133],[213,134],[214,135],[212,136],[232,151],[230,138],[231,152],[233,138],[205,140],[206,141],[207,141],[209,141],[211,142],[210,141],[222,143],[221,144],[223,144],[224,145],[220,146],[226,147],[227,148],[228,149],[225,150]],"semanticDiagnosticsPerFile":[236,234,146,129,148,130,147,152,153,149,155,150,154,151,137,134,141,135,132,140,145,142,143,144,139,136,133,138,191,204,200,201,202,199,65,66,68,69,70,71,72,73,74,67,167,75,156,159,160,161,162,163,164,165,166,194,196,195,239,235,237,238,192,158,240,241,244,242,245,246,247,248,259,258,243,260,262,263,261,264,265,266,267,268,269,270,271,272,273,274,157,76,77,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,78,127,95,96,97,128,98,99,100,101,102,103,104,105,106,107,108,109,111,110,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,275,276,277,278,280,279,281,306,307,282,285,304,305,295,294,292,287,300,298,302,286,299,303,288,289,301,283,290,291,293,297,308,296,284,321,320,315,317,316,309,310,312,314,318,319,311,313,323,322,324,229,325,326,131,249,216,217,218,219,178,175,177,176,174,184,179,183,180,182,181,170,171,172,168,169,173,250,254,256,255,253,257,252,251,57,64,59,60,61,62,63,58,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,56,187,186,185,188,190,198,197,189,193,215,213,214,212,232,230,231,233,205,206,207,208,209,211,210,222,203,221,223,224,220,226,227,228,225,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -+{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createprovider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../base-controller/dist/types/basecontrollerv1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/dom-events.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asynceventemitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacytransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/basetransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionfactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerclass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/restrictedcontrollermessenger.d.ts","../../base-controller/dist/types/controllermessenger.d.ts","../../base-controller/dist/types/basecontrollerv2.d.ts","../../base-controller/dist/types/index.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/types.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createeventemitterproxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createswappableproxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/index.d.ts","../../network-controller/dist/types/constants.d.ts","../../../node_modules/@metamask/safe-event-emitter/dist/cjs/index.d.ts","../../json-rpc-engine/dist/types/jsonrpcengine.d.ts","../../json-rpc-engine/dist/types/createasyncmiddleware.d.ts","../../json-rpc-engine/dist/types/createscaffoldmiddleware.d.ts","../../json-rpc-engine/dist/types/getuniqueid.d.ts","../../json-rpc-engine/dist/types/idremapmiddleware.d.ts","../../json-rpc-engine/dist/types/mergemiddleware.d.ts","../../json-rpc-engine/dist/types/index.d.ts","../../eth-json-rpc-provider/dist/types/safe-event-emitter-provider.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-engine.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-middleware.d.ts","../../eth-json-rpc-provider/dist/types/index.d.ts","../../../node_modules/eth-block-tracker/dist/blocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/pollingblocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/subscribeblocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/index.d.ts","../../network-controller/dist/types/types.d.ts","../../network-controller/dist/types/create-auto-managed-network-client.d.ts","../../network-controller/dist/types/networkcontroller.d.ts","../../network-controller/dist/types/create-network-client.d.ts","../../network-controller/dist/types/index.d.ts","../../polling-controller/dist/types/types.d.ts","../../polling-controller/dist/types/blocktrackerpollingcontroller.d.ts","../../polling-controller/dist/types/staticintervalpollingcontroller.d.ts","../../polling-controller/dist/types/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../src/determinegasfeecalculations.ts","../src/gas-util.ts","../src/gasfeecontroller.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupsemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/difflines.d.ts","../../../node_modules/jest-diff/build/printdiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","5615ccf831db2ffc82145243081ebdb60ea8e1005ee8f975d1c0c1401a9c894e","38682ed3630bb6ecdace80d5a9adc811fc20a419f1940446e306c3a020d083b9","cc182e6e4f691cd6f7bf7cb491247a4c7818f9f1cb2db1d45c65ff906e3f741b","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","e0eda929c6b9b628cdeb0e54cd3582cb97e64f28aab34612fc1431c545899584","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","5b3cd03ae354ea96eff1f74d7c410fe4852e6382227e8b0ecf87ab5e3a5bbcd4","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"056097110efd16869ec118cedb44ecbac9a019576eee808d61304ca6d5cb2cbe","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"6fb8358e10ed92a7f515b7d79da3904c955a3ffd4e14aa9df6f0ea113041f1cf","affectsGlobalScope":true},"45c831238c6dac21c72da5f335747736a56a3847192bf03c84b958a7e9ec93e2","661a11d16ad2e3543a77c53bcd4017ee9a450f47ab7def3ab493a86eae4d550c",{"version":"8cdc646cec7819581ef343b83855b1bfe4fe674f2c84f4fb8dc90d82fb56bd3a","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","9dd56225cc2d8cb8fe5ceb0043ff386987637e12fecc6078896058a99deae284","2375ed4b439215aa3b6d0c6fd175c78a4384b30cb43cbadaecbf0a18954c98cb","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","41231da15bb5e3e806a8395bd15c7befd2ec90f9f4e3c9d0ae1356bccb76dbb0","fccfef201d057cb407fa515311bd608549bab6c7b8adcf8f2df31f5d3b796478",{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true},"5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"464762c6213566d072f1ced5e8e9a954785ec5e53883b7397198abb5ef5b8f71","affectsGlobalScope":true},"6387920dc3e18927335b086deec75bf8e50f879a5e273d32ee7bb7a55ba50572","9bba37424094688c4663c177a1379b229f919b8912889a472f32fdc5f08ddb4d","29a4be13b3a30d3e66667b75c58ec61fb2df8fa0422534fdee3cfb30c5dbf450","83366d901beda79d6eb37aaaf6ca248dcd88946302b2a7d975590783be51e88e","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","43ec77c369473e92e2ecebf0554a0fdaa9c256644a6070f28228dfcceec77351",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"95ed02bacb4502c985b69742ec82a4576d4ff4a6620ecc91593f611d502ae546","bf755525c4e6f85a970b98c4755d98e8aa1b6dbd83a5d8fcc57d3d497351b936","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"28084e15b63e6211769db2fe646d8bc5c4c6776321e0deffe2d12eefd52cb6b9","affectsGlobalScope":true},{"version":"aed37dabf86c99d6c8508700576ecede86688397bc12523541858705a0c737c2","affectsGlobalScope":true},"cc6ef5733d4ea6d2e06310a32dffd2c16418b467c5033d49cecc4f3a25de7497","94768454c3348b6ebe48e45fbad8c92e2bb7af4a35243edbe2b90823d0bd7f9a","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","84da3c28344e621fd1d591f2c09e9595292d2b70018da28a553268ac122597d4","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","6e191fea1db6e9e4fa828259cf489e820ec9170effff57fb081a2f3295db4722","aed943465fbce1efe49ee16b5ea409050f15cd8eaf116f6fadb64ef0772e7d95","70d08483a67bf7050dbedace398ef3fee9f436fcd60517c97c4c1e22e3c6f3e8","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"e933de8143e1d12dd51d89b398760fd5a9081896be366dad88a922d0b29f3c69","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b38d55d08708c2410a3039687db70b4a5bfa69fc4845617c313b5a10d9c5c637","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"a05e2d784c9be7051c4ac87a407c66d2106e23490c18c038bbd0712bde7602fd","affectsGlobalScope":true},{"version":"df90b9d0e9980762da8daf8adf6ffa0c853e76bfd269c377be0d07a9ad87acd2","affectsGlobalScope":true},"cf434b5c04792f62d6f4bdd5e2c8673f36e638e910333c172614d5def9b17f98","1d65d4798df9c2df008884035c41d3e67731f29db5ecb64cd7378797c7c53a2f","0faee6b555890a1cb106e2adc5d3ffd89545b1da894d474e9d436596d654998f","c6c01ea1c42508edf11a36d13b70f6e35774f74355ba5d358354d4a77cc67ea1","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b0297b09e607bec9698cac7cf55463d6731406efb1161ee4d448293b47397c84","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","68cc8d6fcc2f270d7108f02f3ebc59480a54615be3e09a47e14527f349e9d53e","3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","563fa27fdaec8f195b84f71a7af0ef48d30d5cc830575db86da86a63a470c8e6","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","d6e6620a30d582182acc3f0a992a0c311adc589f111096aea11ab83fc09a5ccc","6213b8f686f56beab22b59a0f468590fd3a4c5fa931236a017efeca91d7c9584","c451cec9a588b1f105a5ea2c6063d4fca112b9d70105cacdadda0e1ef67e9379","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","03460a54d0e0481d1e11097f66ad43f054bc95efdafe5f81bbc7a82be181af75","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","8129a34006218a6f3cdc81bbd438d5429eb18b08b4338a26977ac3b4df129d75","30d2170e1a718b5035611af55e3618b4ba8f42f0749bb52ee593da6082c4e2ce","98ef38666d88ec9699a722053e07ede65d3042f693fe7ff8c786e53dbb6fd43b","a3b8b6be7620897d1e481e8650c980a210a138fceb6e710eaf95fd9dd0dfe94a","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","14bd47270e654c8eb3b1489fa8c095912ee62a0a29bb92743393203722347c53","3d9297165e67fd59d9821cc93a9808213e33c56a8ac1c4273171f6afaaa2d4d5","e7af7d288b89287ad031b19583c597fcd9f5edc0b0d579b7b492f06cf57e058c","92cb686a9ca5eb5dd7d5d8d43a3707194c1e91ea07a027b3bcb60b6011b24632","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e",{"version":"d1a5f486914e3ead50534f48742c4e5e885d28909bec274017189d3284bcb833","signature":"b943b90c649678cec7987a6d56f4917db1712079294cffdf650699235ff9f37d"},{"version":"15433a2e97dcee760d97f8e56afe908fe932611ef00711aed9b246ba058ce0be","signature":"c5fc52cba80e6ba6fde33b0f9370b8ba6866831070cbc84b20a7c6e9e1163160"},{"version":"674d97345e4d79afd1248bdf3a032c9280f2ef163b1a074d14a1a927d93397a4","signature":"6991b558e53d59bb2160b64ee154678e069bd9bfe5131c628dcb6ac0363ca00d"},"d021f18758b28bda32bdaf0a987e0804cec074a9a4cfab8232ed81d96e75dfae","4489c6a9fde8934733aa7df6f7911461ee6e9e4ad092736bd416f6b2cc20b2c6","2c8e55457aaf4902941dfdba4061935922e8ee6e120539c9801cd7b400fae050","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","9d38964b57191567a14b396422c87488cecd48f405c642daa734159875ee81d9","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","8c95f96ccd4be0674944077aec1e4f2cccd515ca06d4327562dd017250e7d3fc",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","bc3cba7b0af2d52e7425299aee518db479d44004eff6fbbd206d1ee7e5ec3fb5","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","035a5df183489c2e22f3cf59fc1ed2b043d27f357eecc0eb8d8e840059d44245","a4809f4d92317535e6b22b01019437030077a76fec1d93b9881c9ed4738fcc54","5f53fa0bd22096d2a78533f94e02c899143b8f0f9891a46965294ee8b91a9434","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","b8442e9db28157344d1bc5d8a5a256f1692de213f0c0ddeb84359834015a008c","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","da2b6356b84a40111aaecb18304ea4e4fcb43d70efb1c13ca7d7a906445ee0d3","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","6f294731b495c65ecf46a5694f0082954b961cf05463bea823f8014098eaffa0","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","68a0d0c508e1b6d8d23a519a8a0a3303dc5baa4849ca049f21e5bad41945e3fc","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","b03afe4bec768ae333582915146f48b161e567a81b5ebc31c4d78af089770ac9","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","916be7d770b0ae0406be9486ac12eb9825f21514961dd050594c4b250617d5a8","254d9fb8c872d73d34594be8a200fd7311dbfa10a4116bfc465fba408052f2b3","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","2ccea88888048bbfcacbc9531a5596ea48a3e7dcd0a25f531a81bb717903ba4f","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","d8f7109e14f20eb735225a62fd3f8366da1a8349e90331cdad57f4b04caf6c5a","cf3d384d082b933d987c4e2fe7bfb8710adfd9dc8155190056ed6695a25a559e","9871b7ee672bc16c78833bdab3052615834b08375cb144e4d2cba74473f4a589","c863198dae89420f3c552b5a03da6ed6d0acfa3807a64772b895db624b0de707","8b03a5e327d7db67112ebbc93b4f744133eda2c1743dbb0a990c61a8007823ef","86c73f2ee1752bac8eeeece234fd05dfcf0637a4fbd8032e4f5f43102faa8eec","42fad1f540271e35ca37cecda12c4ce2eef27f0f5cf0f8dd761d723c744d3159","ff3743a5de32bee10906aff63d1de726f6a7fd6ee2da4b8229054dfa69de2c34","83acd370f7f84f203e71ebba33ba61b7f1291ca027d7f9a662c6307d74e4ac22","1445cec898f90bdd18b2949b9590b3c012f5b7e1804e6e329fb0fe053946d5ec","0e5318ec2275d8da858b541920d9306650ae6ac8012f0e872fe66eb50321a669","cf530297c3fb3a92ec9591dd4fa229d58b5981e45fe6702a0bd2bea53a5e59be","c1f6f7d08d42148ddfe164d36d7aba91f467dbcb3caa715966ff95f55048b3a4","f4e9bf9103191ef3b3612d3ec0044ca4044ca5be27711fe648ada06fad4bcc85","0c1ee27b8f6a00097c2d6d91a21ee4d096ab52c1e28350f6362542b55380059a","7677d5b0db9e020d3017720f853ba18f415219fb3a9597343b1b1012cfd699f7","bc1c6bc119c1784b1a2be6d9c47addec0d83ef0d52c8fbe1f14a51b4dfffc675","52cf2ce99c2a23de70225e252e9822a22b4e0adb82643ab0b710858810e00bf1","770625067bb27a20b9826255a8d47b6b5b0a2d3dfcbd21f89904c731f671ba77","d1ed6765f4d7906a05968fb5cd6d1db8afa14dbe512a4884e8ea5c0f5e142c80","799c0f1b07c092626cf1efd71d459997635911bb5f7fc1196efe449bba87e965","2a184e4462b9914a30b1b5c41cf80c6d3428f17b20d3afb711fff3f0644001fd","9eabde32a3aa5d80de34af2c2206cdc3ee094c6504a8d0c2d6d20c7c179503cc","397c8051b6cfcb48aa22656f0faca2553c5f56187262135162ee79d2b2f6c966","a8ead142e0c87dcd5dc130eba1f8eeed506b08952d905c47621dc2f583b1bff9","a02f10ea5f73130efca046429254a4e3c06b5475baecc8f7b99a0014731be8b3","c2576a4083232b0e2d9bd06875dd43d371dee2e090325a9eac0133fd5650c1cb","4c9a0564bb317349de6a24eb4efea8bb79898fa72ad63a1809165f5bd42970dd","f40ac11d8859092d20f953aae14ba967282c3bb056431a37fced1866ec7a2681","cc11e9e79d4746cc59e0e17473a59d6f104692fd0eeea1bdb2e206eabed83b03","b444a410d34fb5e98aa5ee2b381362044f4884652e8bc8a11c8fe14bbd85518e","c35808c1f5e16d2c571aa65067e3cb95afeff843b259ecfa2fc107a9519b5392","14d5dc055143e941c8743c6a21fa459f961cbc3deedf1bfe47b11587ca4b3ef5","a3ad4e1fc542751005267d50a6298e6765928c0c3a8dce1572f2ba6ca518661c","f237e7c97a3a89f4591afd49ecb3bd8d14f51a1c4adc8fcae3430febedff5eb6","3ffdfbec93b7aed71082af62b8c3e0cc71261cc68d796665faa1e91604fbae8f","662201f943ed45b1ad600d03a90dffe20841e725203ced8b708c91fcd7f9379a","c9ef74c64ed051ea5b958621e7fb853fe3b56e8787c1587aefc6ea988b3c7e79","2462ccfac5f3375794b861abaa81da380f1bbd9401de59ffa43119a0b644253d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","7d8ddf0f021c53099e34ee831a06c394d50371816caa98684812f089b4c6b3d4","7d2a0ba1297be385a89b5515b88cd31b4a1eeef5236f710166dc1b36b1741e1b","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","ab82804a14454734010dcdcd43f564ff7b0389bee4c5692eec76ff5b30d4cf66","bae8d023ef6b23df7da26f51cea44321f95817c190342a36882e93b80d07a960","ae271d475b632ce7b03fea6d9cf6da72439e57a109672671cbc79f54e1386938"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[234],[92,128,129,130,145],[129,130,146,147],[128,129],[128,145,148,151],[128,148,151,152],[149,150,151,153,154],[128,151],[128,145,148,149,150,153],[128,136],[128],[92,128],[80,128],[132,133,134,135,136,137,138,139,140,141,142,143,144],[128,134,135],[128,134,136],[199],[199,200,201],[64],[67],[64,67],[65,66,67,68,69,70,71,72,73,74,75,156,159,160,161,162,163,164,165,166],[58,64,65],[67,73,75,155],[158],[67,68],[64,162],[194,195],[234,235,236,237,238],[234,236],[157],[241,242,243],[93,128],[246],[247],[258],[252,257],[261,263,264,265,266,267,268,269,270,271,272,273],[261,262,264,265,266,267,268,269,270,271,272,273],[262,263,264,265,266,267,268,269,270,271,272,273],[261,262,263,265,266,267,268,269,270,271,272,273],[261,262,263,264,266,267,268,269,270,271,272,273],[261,262,263,264,265,267,268,269,270,271,272,273],[261,262,263,264,265,266,268,269,270,271,272,273],[261,262,263,264,265,266,267,269,270,271,272,273],[261,262,263,264,265,266,267,268,270,271,272,273],[261,262,263,264,265,266,267,268,269,271,272,273],[261,262,263,264,265,266,267,268,269,270,272,273],[261,262,263,264,265,266,267,268,269,270,271,273],[261,262,263,264,265,266,267,268,269,270,271,272],[76],[79],[80,85,112],[81,92,93,100,109,120],[81,82,92,100],[83,121],[84,85,93,101],[85,109,117],[86,88,92,100],[87],[88,89],[92],[91,92],[79,92],[92,93,94,109,120],[92,93,94,109],[92,95,100,109,120],[92,93,95,96,100,109,117,120],[95,97,109,117,120],[76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127],[92,98],[99,120,125],[88,92,100,109],[101],[102],[79,103],[104,119,125],[105],[106],[92,107],[107,108,121,123],[80,92,109,110,111],[80,109,111],[109,110],[112],[113],[92,115,116],[115,116],[85,100,109,117],[118],[100,119],[80,95,106,120],[85,121],[109,122],[123],[124],[80,85,92,94,103,109,120,123,125],[109,126],[128,279],[282,321],[282,306,321],[321],[282],[282,307,321],[282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320],[307,321],[322],[325],[204],[216,217,218],[204,215,216],[179],[179,180,181,182,183],[168,169,170,171,172,173,174,175,176,177,178],[250,253],[250,253,254,255],[252],[249,256],[251],[57,59,60,61,62,63],[57,58],[59],[58,59],[57,59],[167,184,185,186],[185],[56,185,186,187],[186],[189],[189,190,193,197],[196],[167,191,192],[212,213,214],[211,212],[167,211,212],[167,204,211],[232],[191,192,198,232],[167,188,191,198,224,228,229,230,231],[167,205],[205,206,207,208,209,210],[167,204],[220],[203,220,222,223],[167,188,191,198,202,203,220,221],[167,198,215,219],[167,188,224,225],[225,226,227],[128,167,188,224,225],[167,224],[191,232],[167,188,224,228]],"referencedMap":[[236,1],[146,2],[148,3],[130,4],[152,5],[153,6],[149,6],[155,7],[150,6],[154,8],[151,9],[137,10],[134,11],[141,12],[135,10],[132,13],[145,14],[139,11],[136,15],[138,16],[204,12],[200,17],[201,17],[202,18],[65,19],[66,19],[68,20],[69,19],[70,19],[71,21],[67,19],[167,22],[75,23],[156,24],[159,25],[165,26],[166,27],[196,28],[239,29],[235,1],[237,30],[238,1],[192,11],[158,31],[244,32],[245,33],[247,34],[248,35],[259,36],[258,37],[262,38],[263,39],[261,40],[264,41],[265,42],[266,43],[267,44],[268,45],[269,46],[270,47],[271,48],[272,49],[273,50],[76,51],[77,51],[79,52],[80,53],[81,54],[82,55],[83,56],[84,57],[85,58],[86,59],[87,60],[88,61],[89,61],[90,62],[91,63],[92,64],[93,65],[94,66],[95,67],[96,68],[97,69],[128,70],[98,71],[99,72],[100,73],[101,74],[102,75],[103,76],[104,77],[105,78],[106,79],[107,80],[108,81],[109,82],[111,83],[110,84],[112,85],[113,86],[115,87],[116,88],[117,89],[118,90],[119,91],[120,92],[121,93],[122,94],[123,95],[124,96],[125,97],[126,98],[276,11],[280,99],[281,11],[306,100],[307,101],[282,102],[285,102],[304,100],[305,100],[295,100],[294,103],[292,100],[287,100],[300,100],[298,100],[302,100],[286,100],[299,100],[303,100],[288,100],[289,100],[301,100],[283,100],[290,100],[291,100],[293,100],[297,100],[308,104],[296,100],[284,100],[321,105],[315,104],[317,106],[316,104],[309,104],[310,104],[312,104],[314,104],[318,106],[319,106],[311,106],[313,106],[323,107],[326,108],[216,109],[219,110],[217,111],[218,111],[175,112],[177,112],[176,112],[174,112],[184,113],[179,114],[170,112],[171,112],[172,112],[173,112],[254,115],[256,116],[255,115],[253,117],[257,118],[252,119],[64,120],[59,121],[60,122],[61,122],[62,123],[63,123],[58,124],[187,125],[186,126],[188,127],[185,128],[190,129],[198,130],[197,131],[193,132],[215,133],[213,134],[214,135],[212,136],[230,137],[231,138],[232,139],[233,137],[206,140],[207,140],[209,140],[211,141],[205,142],[210,140],[221,143],[223,143],[224,144],[222,145],[220,146],[226,147],[228,148],[227,149],[225,150]],"exportedModulesMap":[[236,1],[146,2],[148,3],[130,4],[152,5],[153,6],[149,6],[155,7],[150,6],[154,8],[151,9],[137,10],[134,11],[141,12],[135,10],[132,13],[145,14],[139,11],[136,15],[138,16],[204,12],[200,17],[201,17],[202,18],[65,19],[66,19],[68,20],[69,19],[70,19],[71,21],[67,19],[167,22],[75,23],[156,24],[159,25],[165,26],[166,27],[196,28],[239,29],[235,1],[237,30],[238,1],[192,11],[158,31],[244,32],[245,33],[247,34],[248,35],[259,36],[258,37],[262,38],[263,39],[261,40],[264,41],[265,42],[266,43],[267,44],[268,45],[269,46],[270,47],[271,48],[272,49],[273,50],[76,51],[77,51],[79,52],[80,53],[81,54],[82,55],[83,56],[84,57],[85,58],[86,59],[87,60],[88,61],[89,61],[90,62],[91,63],[92,64],[93,65],[94,66],[95,67],[96,68],[97,69],[128,70],[98,71],[99,72],[100,73],[101,74],[102,75],[103,76],[104,77],[105,78],[106,79],[107,80],[108,81],[109,82],[111,83],[110,84],[112,85],[113,86],[115,87],[116,88],[117,89],[118,90],[119,91],[120,92],[121,93],[122,94],[123,95],[124,96],[125,97],[126,98],[276,11],[280,99],[281,11],[306,100],[307,101],[282,102],[285,102],[304,100],[305,100],[295,100],[294,103],[292,100],[287,100],[300,100],[298,100],[302,100],[286,100],[299,100],[303,100],[288,100],[289,100],[301,100],[283,100],[290,100],[291,100],[293,100],[297,100],[308,104],[296,100],[284,100],[321,105],[315,104],[317,106],[316,104],[309,104],[310,104],[312,104],[314,104],[318,106],[319,106],[311,106],[313,106],[323,107],[326,108],[216,109],[219,110],[217,111],[218,111],[175,112],[177,112],[176,112],[174,112],[184,113],[179,114],[170,112],[171,112],[172,112],[173,112],[254,115],[256,116],[255,115],[253,117],[257,118],[252,119],[64,120],[59,121],[60,122],[61,122],[62,123],[63,123],[58,124],[187,125],[186,126],[188,127],[185,128],[190,129],[198,130],[197,131],[193,132],[215,133],[213,134],[214,135],[212,136],[230,137],[231,151],[232,152],[233,137],[206,140],[207,140],[209,140],[211,141],[205,142],[210,140],[221,143],[223,143],[224,144],[222,145],[220,146],[226,147],[228,148],[227,149],[225,150]],"semanticDiagnosticsPerFile":[236,234,146,129,148,130,147,152,153,149,155,150,154,151,137,134,141,135,132,140,145,142,143,144,139,136,133,138,191,204,200,201,202,199,65,66,68,69,70,71,72,73,74,67,167,75,156,159,160,161,162,163,164,165,166,194,196,195,239,235,237,238,192,158,240,241,244,242,245,246,247,248,259,258,243,260,262,263,261,264,265,266,267,268,269,270,271,272,273,274,157,76,77,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,78,127,95,96,97,128,98,99,100,101,102,103,104,105,106,107,108,109,111,110,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,275,276,277,278,280,279,281,306,307,282,285,304,305,295,294,292,287,300,298,302,286,299,303,288,289,301,283,290,291,293,297,308,296,284,321,320,315,317,316,309,310,312,314,318,319,311,313,323,322,324,229,325,326,131,249,216,219,217,218,178,175,177,176,174,184,179,183,180,182,181,170,171,172,168,169,173,250,254,256,255,253,257,252,251,57,64,59,60,61,62,63,58,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,56,187,186,188,185,190,198,197,189,193,215,213,214,212,230,231,232,233,206,207,208,209,211,205,210,203,221,223,224,222,220,226,228,227,225,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -diff --git a/dist/types/GasFeeController.d.ts b/dist/types/GasFeeController.d.ts -index b6ffa7cd5b356ef6c5df6a79876308d21749bd06..56d9420b539cc8d98b8aeb0a84ba541fc3beae1b 100644 ---- a/dist/types/GasFeeController.d.ts -+++ b/dist/types/GasFeeController.d.ts -@@ -2,7 +2,7 @@ import type { ControllerGetStateAction, ControllerStateChangeEvent, RestrictedCo - import type { NetworkClientId, NetworkControllerGetEIP1559CompatibilityAction, NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, NetworkControllerNetworkDidChangeEvent, NetworkState, ProviderProxy } from '@metamask/network-controller'; - import { StaticIntervalPollingController } from '@metamask/polling-controller'; - import type { Hex } from '@metamask/utils'; --export declare const GAS_API_BASE_URL = "https://gas.api.infura.io"; -+export declare const LEGACY_GAS_PRICES_API_URL = "https://api.metaswap.codefi.network/gasPrices"; - export type unknownString = 'unknown'; - export type FeeMarketEstimateType = 'fee-market'; - export type LegacyEstimateType = 'legacy'; -@@ -160,7 +160,6 @@ export declare class GasFeeController extends StaticIntervalPollingController Hex; - getProvider: () => ProviderProxy; - onNetworkDidChange?: (listener: (state: NetworkState) => void) => void; -+ legacyAPIEndpoint?: string; -+ EIP1559APIEndpoint: string; - clientId?: string; -- infuraAPIKey: string; - }); - resetPolling(): Promise; - fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions): Promise; -diff --git a/dist/types/GasFeeController.d.ts.map b/dist/types/GasFeeController.d.ts.map -index 0dd80816d0083029fcc5ef3a85a47f188a5b4c02..127cdbe0c0fbfb37938403190c49d2a5687cf9c4 100644 ---- a/dist/types/GasFeeController.d.ts.map -+++ b/dist/types/GasFeeController.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"GasFeeController.d.ts","sourceRoot":"","sources":["../../src/GasFeeController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,0BAA0B,EAC1B,6BAA6B,EAC9B,MAAM,2BAA2B,CAAC;AAOnC,OAAO,KAAK,EACV,eAAe,EACf,8CAA8C,EAC9C,2CAA2C,EAC3C,+BAA+B,EAC/B,sCAAsC,EACtC,YAAY,EACZ,aAAa,EACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC;AAC/E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAW3C,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAE5D,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC;AAItC,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC;AAIjD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;AAK1C,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAGrD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB,qBAAqB,GACrB,uBAAuB,GACvB,kBAAkB,GAClB,cAAc,CAAC;AAEnB,MAAM,MAAM,yBAAyB,GAAG;IACtC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AAEH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6BAA6B,EAAE,MAAM,CAAC;IACtC,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GAAG,sBAAsB,GAAG,uBAAuB,CAAC;AAE/E,KAAK,sBAAsB,GAAG;IAC5B,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,YAAY,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;IACtC,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,gBAAgB,EAAE,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,KAAK,uBAAuB,GAAG;IAC7B,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,IAAI,CAAC;IAC7B,YAAY,EAAE,IAAI,CAAC;IACnB,sBAAsB,EAAE,IAAI,CAAC;IAC7B,0BAA0B,EAAE,IAAI,CAAC;IACjC,gBAAgB,EAAE,IAAI,CAAC;IACvB,iBAAiB,EAAE,IAAI,CAAC;CACzB,CAAC;AAaF,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,mBAAmB,CAAC;IACrC,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjD,eAAe,EAAE,uBAAuB,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,EAAE,eAAe,CAAC;IACjC,yBAAyB,EAAE,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7E,eAAe,EAAE,qBAAqB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,sBAAsB,CAAC;IACxC,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjD,eAAe,EAAE,kBAAkB,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACvC,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACjD,eAAe,EAAE,cAAc,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAC9B,sBAAsB,GACtB,oBAAoB,GACpB,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B,MAAM,MAAM,wBAAwB,GAAG;IACrC,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CACnE,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,wBAAwB,GAChD,sBAAsB,GAAG;IACvB,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC,CAAC;AAEJ,QAAA,MAAM,IAAI,qBAAqB,CAAC;AAEhC,MAAM,MAAM,iBAAiB,GAAG,0BAA0B,CACxD,OAAO,IAAI,EACX,WAAW,CACZ,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,wBAAwB,CAAC,OAAO,IAAI,EAAE,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,CAAC;AAEvD,KAAK,cAAc,GACf,+BAA+B,GAC/B,2CAA2C,GAC3C,8CAA8C,CAAC;AAEnD,KAAK,eAAe,GAAG,6BAA6B,CAClD,OAAO,IAAI,EACX,uBAAuB,GAAG,cAAc,EACxC,sBAAsB,GAAG,sCAAsC,EAC/D,cAAc,CAAC,MAAM,CAAC,EACtB,sCAAsC,CAAC,MAAM,CAAC,CAC/C,CAAC;AAUF;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,+BAA+B,CACnE,OAAO,IAAI,EACX,WAAW,EACX,eAAe,CAChB;;IACC,OAAO,CAAC,UAAU,CAAC,CAAgC;IAEnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAc;IAEzC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAE5C,OAAO,CAAC,QAAQ,CAAC,qCAAqC,CAAC;IAEvD,OAAO,CAAC,QAAQ,CAAC,0CAA0C,CAAC;IAE5D,OAAO,CAAC,QAAQ,CAAC,qCAAqC,CAAC;IAEvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,cAAc,CAAC;IAEvB,OAAO,CAAC,QAAQ,CAAC,CAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IAInC;;;;;;;;;;;;;;;;;;;;OAoBG;gBACS,EACV,QAAgB,EAChB,SAAS,EACT,KAAK,EACL,qCAAqC,EACrC,qCAAqC,EACrC,UAAU,EACV,0CAA0C,EAC1C,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,YAAY,GACb,EAAE;QACD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,eAAe,CAAC;QAC3B,KAAK,CAAC,EAAE,WAAW,CAAC;QACpB,qCAAqC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9D,0CAA0C,EAAE,MAAM,OAAO,CAAC;QAC1D,qCAAqC,CAAC,EAAE,MAAM,OAAO,CAAC;QACtD,UAAU,CAAC,EAAE,MAAM,GAAG,CAAC;QACvB,WAAW,EAAE,MAAM,aAAa,CAAC;QACjC,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,KAAK,IAAI,CAAC;QACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB;IA0CK,YAAY;IAWZ,oBAAoB,CAAC,OAAO,CAAC,EAAE,0BAA0B;IAIzD,iCAAiC,CACrC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,MAAM,CAAC;IAalB;;;;;;;OAOG;IACG,wBAAwB,CAC5B,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,WAAW,CAAC;IAsFvB;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM;IAOlC,WAAW;IAQX;;;;OAIG;IACM,OAAO;IAKhB,OAAO,CAAC,KAAK;IAUb;;;;;;OAMG;IACG,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,OAAO,CAAC,UAAU;YAMJ,uBAAuB;IAWrC,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,YAAY,EAAE,MAAM,GACnB,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAyBpD,sBAAsB;IAMtB,uBAAuB;CAKxB;AAED,eAAe,gBAAgB,CAAC"} -\ No newline at end of file -+{"version":3,"file":"GasFeeController.d.ts","sourceRoot":"","sources":["../../src/GasFeeController.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -diff --git a/dist/types/determineGasFeeCalculations.d.ts b/dist/types/determineGasFeeCalculations.d.ts -index 8091a4d4290d6a3ca9ecbecc9ab8c6764a58cac9..6f4b2758a99e4d0d3fd2c8dce4064a2a2fc69c0a 100644 ---- a/dist/types/determineGasFeeCalculations.d.ts -+++ b/dist/types/determineGasFeeCalculations.d.ts -@@ -2,15 +2,14 @@ import type { EstimatedGasFeeTimeBounds, EthGasPriceEstimate, GasFeeEstimates, G - type DetermineGasFeeCalculationsRequest = { - isEIP1559Compatible: boolean; - isLegacyGasAPICompatible: boolean; -- fetchGasEstimates: (url: string, infuraAPIKey: string, clientId?: string) => Promise; -+ fetchGasEstimates: (url: string, clientId?: string) => Promise; - fetchGasEstimatesUrl: string; -- fetchLegacyGasPriceEstimates: (url: string, infuraAPIKey: string, clientId?: string) => Promise; -+ fetchLegacyGasPriceEstimates: (url: string, clientId?: string) => Promise; - fetchLegacyGasPriceEstimatesUrl: string; - fetchEthGasPriceEstimate: (ethQuery: any) => Promise; - calculateTimeEstimate: (maxPriorityFeePerGas: string, maxFeePerGas: string, gasFeeEstimates: GasFeeEstimates) => EstimatedGasFeeTimeBounds; - clientId: string | undefined; - ethQuery: any; -- infuraAPIKey: string; - nonRPCGasFeeApisDisabled?: boolean; - }; - /** -@@ -35,7 +34,6 @@ type DetermineGasFeeCalculationsRequest = { - * @param args.calculateTimeEstimate - A function that determine time estimate bounds. - * @param args.clientId - An identifier that an API can use to know who is asking for estimates. - * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly. -- * @param args.infuraAPIKey - Infura API key to use for requests to Infura. - * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint - * @returns The gas fee calculations. - */ -diff --git a/dist/types/determineGasFeeCalculations.d.ts.map b/dist/types/determineGasFeeCalculations.d.ts.map -index 44ce4dc7b691ea0316163952f99d426d417afd1a..17cec260fc0836f59929a053270164e5c10e4b89 100644 ---- a/dist/types/determineGasFeeCalculations.d.ts.map -+++ b/dist/types/determineGasFeeCalculations.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"determineGasFeeCalculations.d.ts","sourceRoot":"","sources":["../../src/determineGasFeeCalculations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,eAAe,EACf,WAAW,IAAI,kBAAkB,EACjC,sBAAsB,EACvB,MAAM,oBAAoB,CAAC;AAG5B,KAAK,kCAAkC,GAAG;IACxC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,wBAAwB,EAAE,OAAO,CAAC;IAClC,iBAAiB,EAAE,CACjB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,4BAA4B,EAAE,CAC5B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACrC,+BAA+B,EAAE,MAAM,CAAC;IAGxC,wBAAwB,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC1E,qBAAqB,EAAE,CACrB,oBAAoB,EAAE,MAAM,EAC5B,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,eAAe,KAC7B,yBAAyB,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAG7B,QAAQ,EAAE,GAAG,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAA8B,2BAA2B,CACvD,IAAI,EAAE,kCAAkC,GACvC,OAAO,CAAC,kBAAkB,CAAC,CAY7B"} -\ No newline at end of file -+{"version":3,"file":"determineGasFeeCalculations.d.ts","sourceRoot":"","sources":["../../src/determineGasFeeCalculations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,mBAAmB,EACnB,eAAe,EACf,WAAW,IAAI,kBAAkB,EACjC,sBAAsB,EACvB,MAAM,oBAAoB,CAAC;AAG5B,KAAK,kCAAkC,GAAG;IACxC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,wBAAwB,EAAE,OAAO,CAAC;IAClC,iBAAiB,EAAE,CACjB,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,4BAA4B,EAAE,CAC5B,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,KACd,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACrC,+BAA+B,EAAE,MAAM,CAAC;IAGxC,wBAAwB,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC1E,qBAAqB,EAAE,CACrB,oBAAoB,EAAE,MAAM,EAC5B,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,eAAe,KAC7B,yBAAyB,CAAC;IAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAG7B,QAAQ,EAAE,GAAG,CAAC;IACd,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAA8B,2BAA2B,CACvD,IAAI,EAAE,kCAAkC,GACvC,OAAO,CAAC,kBAAkB,CAAC,CAY7B"} -\ No newline at end of file -diff --git a/dist/types/gas-util.d.ts b/dist/types/gas-util.d.ts -index 3739e0ffc0f895d20b5ab1e706b893e8606ef62b..661c3a87fbb2160075801e676d216809b01ecf6e 100644 ---- a/dist/types/gas-util.d.ts -+++ b/dist/types/gas-util.d.ts -@@ -11,21 +11,19 @@ export declare function normalizeGWEIDecimalNumbers(n: string | number): any; - * Fetch gas estimates from the given URL. - * - * @param url - The gas estimate URL. -- * @param infuraAPIKey - The Infura API key used for infura API requests. - * @param clientId - The client ID used to identify to the API who is asking for estimates. - * @returns The gas estimates. - */ --export declare function fetchGasEstimates(url: string, infuraAPIKey: string, clientId?: string): Promise; -+export declare function fetchGasEstimates(url: string, clientId?: string): Promise; - /** - * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium - * high values from that API. - * - * @param url - The URL to fetch gas price estimates from. -- * @param infuraAPIKey - The Infura API key used for infura API requests. - * @param clientId - The client ID used to identify to the API who is asking for estimates. - * @returns The gas price estimates. - */ --export declare function fetchLegacyGasPriceEstimates(url: string, infuraAPIKey: string, clientId?: string): Promise; -+export declare function fetchLegacyGasPriceEstimates(url: string, clientId?: string): Promise; - /** - * Get a gas price estimate from the network using the `eth_gasPrice` method. - * -diff --git a/dist/types/gas-util.d.ts.map b/dist/types/gas-util.d.ts.map -index 8a24af1980fc16f7ecbe97222cab534b3c0f6c2c..ab417991781db0ad097218682c264d7aa738bf12 100644 ---- a/dist/types/gas-util.d.ts.map -+++ b/dist/types/gas-util.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"gas-util.d.ts","sourceRoot":"","sources":["../../src/gas-util.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAGhD,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAEzB,sBAAsB,EACvB,MAAM,oBAAoB,CAAC;AAI5B;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,OAI7D;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAyC1B;AAED;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC,CAcjC;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAK9B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,oBAAoB,EAAE,MAAM,EAC5B,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,eAAe,GAC/B,yBAAyB,CAoD3B"} -\ No newline at end of file -+{"version":3,"file":"gas-util.d.ts","sourceRoot":"","sources":["../../src/gas-util.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAGhD,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAEzB,sBAAsB,EACvB,MAAM,oBAAoB,CAAC;AAI5B;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,OAI7D;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAyC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC,CAgBjC;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAK9B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,oBAAoB,EAAE,MAAM,EAC5B,YAAY,EAAE,MAAM,EACpB,eAAe,EAAE,eAAe,GAC/B,yBAAyB,CAoD3B"} -\ No newline at end of file diff --git a/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch b/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch deleted file mode 100644 index 79a007d20474..000000000000 --- a/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch +++ /dev/null @@ -1,6158 +0,0 @@ -diff --git a/dist/KeyringController.js b/dist/KeyringController.js -index 03a6cecff820613ada02f40d3f88edb93c3fe6ed..a690f825be4d66eb48aec945c8e853a66c68ab94 100644 ---- a/dist/KeyringController.js -+++ b/dist/KeyringController.js -@@ -7,7 +7,7 @@ - - - --var _chunkBRS27QHFjs = require('./chunk-BRS27QHF.js'); -+var _chunkL4UUWIZAjs = require('./chunk-L4UUWIZA.js'); - require('./chunk-NOCGQCUM.js'); - - -@@ -18,5 +18,5 @@ require('./chunk-NOCGQCUM.js'); - - - --exports.AccountImportStrategy = _chunkBRS27QHFjs.AccountImportStrategy; exports.KeyringController = _chunkBRS27QHFjs.KeyringController; exports.KeyringTypes = _chunkBRS27QHFjs.KeyringTypes; exports.SignTypedDataVersion = _chunkBRS27QHFjs.SignTypedDataVersion; exports.default = _chunkBRS27QHFjs.KeyringController_default; exports.getDefaultKeyringState = _chunkBRS27QHFjs.getDefaultKeyringState; exports.isCustodyKeyring = _chunkBRS27QHFjs.isCustodyKeyring; exports.keyringBuilderFactory = _chunkBRS27QHFjs.keyringBuilderFactory; -+exports.AccountImportStrategy = _chunkL4UUWIZAjs.AccountImportStrategy; exports.KeyringController = _chunkL4UUWIZAjs.KeyringController; exports.KeyringTypes = _chunkL4UUWIZAjs.KeyringTypes; exports.SignTypedDataVersion = _chunkL4UUWIZAjs.SignTypedDataVersion; exports.default = _chunkL4UUWIZAjs.KeyringController_default; exports.getDefaultKeyringState = _chunkL4UUWIZAjs.getDefaultKeyringState; exports.isCustodyKeyring = _chunkL4UUWIZAjs.isCustodyKeyring; exports.keyringBuilderFactory = _chunkL4UUWIZAjs.keyringBuilderFactory; - //# sourceMappingURL=KeyringController.js.map -\ No newline at end of file -diff --git a/dist/KeyringController.mjs b/dist/KeyringController.mjs -index 58ef8b875e01d8b09ec3880af35f43acdc40f406..7d315f91917002b37edabe91ab7c710e202a4e4e 100644 ---- a/dist/KeyringController.mjs -+++ b/dist/KeyringController.mjs -@@ -7,7 +7,7 @@ import { - getDefaultKeyringState, - isCustodyKeyring, - keyringBuilderFactory --} from "./chunk-STFS4REY.mjs"; -+} from "./chunk-7A7D7THR.mjs"; - import "./chunk-F64I344Z.mjs"; - export { - AccountImportStrategy, -diff --git a/dist/chunk-7A7D7THR.mjs b/dist/chunk-7A7D7THR.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..42d34b71f245ca1a9e2e019a945df795a6a603c2 ---- /dev/null -+++ b/dist/chunk-7A7D7THR.mjs -@@ -0,0 +1,1506 @@ -+import { -+ __privateAdd, -+ __privateGet, -+ __privateMethod, -+ __privateSet -+} from "./chunk-F64I344Z.mjs"; -+ -+// src/KeyringController.ts -+import { isValidPrivate, toBuffer, getBinarySize } from "@ethereumjs/util"; -+import { BaseController } from "@metamask/base-controller"; -+import * as encryptorUtils from "@metamask/browser-passworder"; -+import HDKeyring from "@metamask/eth-hd-keyring"; -+import { normalize as ethNormalize } from "@metamask/eth-sig-util"; -+import SimpleKeyring from "@metamask/eth-simple-keyring"; -+import { -+ add0x, -+ assertIsStrictHexString, -+ bytesToHex, -+ hasProperty, -+ isObject, -+ isStrictHexString, -+ isValidHexAddress, -+ isValidJson, -+ remove0x -+} from "@metamask/utils"; -+import { Mutex } from "async-mutex"; -+import Wallet, { thirdparty as importers } from "ethereumjs-wallet"; -+var name = "KeyringController"; -+var KeyringTypes = /* @__PURE__ */ ((KeyringTypes2) => { -+ KeyringTypes2["simple"] = "Simple Key Pair"; -+ KeyringTypes2["hd"] = "HD Key Tree"; -+ KeyringTypes2["qr"] = "QR Hardware Wallet Device"; -+ KeyringTypes2["trezor"] = "Trezor Hardware"; -+ KeyringTypes2["ledger"] = "Ledger Hardware"; -+ KeyringTypes2["lattice"] = "Lattice Hardware"; -+ KeyringTypes2["snap"] = "Snap Keyring"; -+ return KeyringTypes2; -+})(KeyringTypes || {}); -+var isCustodyKeyring = (keyringType) => { -+ return keyringType.startsWith("Custody"); -+}; -+var AccountImportStrategy = /* @__PURE__ */ ((AccountImportStrategy2) => { -+ AccountImportStrategy2["privateKey"] = "privateKey"; -+ AccountImportStrategy2["json"] = "json"; -+ return AccountImportStrategy2; -+})(AccountImportStrategy || {}); -+var SignTypedDataVersion = /* @__PURE__ */ ((SignTypedDataVersion2) => { -+ SignTypedDataVersion2["V1"] = "V1"; -+ SignTypedDataVersion2["V3"] = "V3"; -+ SignTypedDataVersion2["V4"] = "V4"; -+ return SignTypedDataVersion2; -+})(SignTypedDataVersion || {}); -+function keyringBuilderFactory(KeyringConstructor) { -+ const builder = () => new KeyringConstructor(); -+ builder.type = KeyringConstructor.type; -+ return builder; -+} -+var defaultKeyringBuilders = [ -+ keyringBuilderFactory(SimpleKeyring), -+ keyringBuilderFactory(HDKeyring) -+]; -+var getDefaultKeyringState = () => { -+ return { -+ isUnlocked: false, -+ keyrings: [] -+ }; -+}; -+function assertHasUint8ArrayMnemonic(keyring) { -+ if (!(hasProperty(keyring, "mnemonic") && keyring.mnemonic instanceof Uint8Array)) { -+ throw new Error("Can't get mnemonic bytes from keyring"); -+ } -+} -+function assertIsExportableKeyEncryptor(encryptor) { -+ if (!("importKey" in encryptor && typeof encryptor.importKey === "function" && "decryptWithKey" in encryptor && typeof encryptor.decryptWithKey === "function" && "encryptWithKey" in encryptor && typeof encryptor.encryptWithKey === "function")) { -+ throw new Error("KeyringController - The encryptor does not support encryption key export." /* UnsupportedEncryptionKeyExport */); -+ } -+} -+function assertIsValidPassword(password) { -+ if (typeof password !== "string") { -+ throw new Error("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ if (!password || !password.length) { -+ throw new Error("KeyringController - Password cannot be empty." /* InvalidEmptyPassword */); -+ } -+} -+function isSerializedKeyringsArray(array) { -+ return typeof array === "object" && Array.isArray(array) && array.every((value) => value.type && isValidJson(value.data)); -+} -+async function displayForKeyring(keyring) { -+ const accounts = await keyring.getAccounts(); -+ return { -+ type: keyring.type, -+ // Cast to `string[]` here is safe here because `accounts` has no nullish -+ // values, and `normalize` returns `string` unless given a nullish value -+ accounts: accounts.map(normalize) -+ }; -+} -+function isEthAddress(address) { -+ return ( -+ // NOTE: This function only checks for lowercased strings -+ isStrictHexString(address.toLowerCase()) && // This checks for lowercased addresses and checksum addresses too -+ isValidHexAddress(address) -+ ); -+} -+function normalize(address) { -+ return isEthAddress(address) ? ethNormalize(address) : address; -+} -+var _controllerOperationMutex, _vaultOperationMutex, _keyringBuilders, _keyrings, _unsupportedKeyrings, _password, _encryptor, _cacheEncryptionKey, _qrKeyringStateListener, _registerMessageHandlers, registerMessageHandlers_fn, _getKeyringBuilderForType, getKeyringBuilderForType_fn, _addQRKeyring, addQRKeyring_fn, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn, _getUpdatedKeyrings, getUpdatedKeyrings_fn, _getSerializedKeyrings, getSerializedKeyrings_fn, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn, _unlockKeyrings, unlockKeyrings_fn, _updateVault, updateVault_fn, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn, _newKeyring, newKeyring_fn, _clearKeyrings, clearKeyrings_fn, _restoreKeyring, restoreKeyring_fn, _destroyKeyring, destroyKeyring_fn, _removeEmptyKeyrings, removeEmptyKeyrings_fn, _checkForDuplicate, checkForDuplicate_fn, _setUnlocked, setUnlocked_fn, _persistOrRollback, persistOrRollback_fn, _withRollback, withRollback_fn, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn, _withControllerLock, withControllerLock_fn, _withVaultLock, withVaultLock_fn; -+var KeyringController = class extends BaseController { -+ /** -+ * Creates a KeyringController instance. -+ * -+ * @param options - Initial options used to configure this controller -+ * @param options.encryptor - An optional object for defining encryption schemes. -+ * @param options.keyringBuilders - Set a new name for account. -+ * @param options.cacheEncryptionKey - Whether to cache or not encryption key. -+ * @param options.messenger - A restricted controller messenger. -+ * @param options.state - Initial state to set on this controller. -+ */ -+ constructor(options) { -+ const { -+ encryptor = encryptorUtils, -+ keyringBuilders, -+ messenger, -+ state -+ } = options; -+ super({ -+ name, -+ metadata: { -+ vault: { persist: true, anonymous: false }, -+ isUnlocked: { persist: false, anonymous: true }, -+ keyrings: { persist: false, anonymous: false }, -+ encryptionKey: { persist: false, anonymous: false }, -+ encryptionSalt: { persist: false, anonymous: false } -+ }, -+ messenger, -+ state: { -+ ...getDefaultKeyringState(), -+ ...state -+ } -+ }); -+ /** -+ * Constructor helper for registering this controller's messaging system -+ * actions. -+ */ -+ __privateAdd(this, _registerMessageHandlers); -+ /** -+ * Get the keyring builder for the given `type`. -+ * -+ * @param type - The type of keyring to get the builder for. -+ * @returns The keyring builder, or undefined if none exists. -+ */ -+ __privateAdd(this, _getKeyringBuilderForType); -+ /** -+ * Add qr hardware keyring. -+ * -+ * @returns The added keyring -+ * @throws If a QRKeyring builder is not provided -+ * when initializing the controller -+ */ -+ __privateAdd(this, _addQRKeyring); -+ /** -+ * Subscribe to a QRKeyring state change events and -+ * forward them through the messaging system. -+ * -+ * @param qrKeyring - The QRKeyring instance to subscribe to -+ */ -+ __privateAdd(this, _subscribeToQRKeyringEvents); -+ __privateAdd(this, _unsubscribeFromQRKeyringsEvents); -+ /** -+ * Create new vault with an initial keyring -+ * -+ * Destroys any old encrypted storage, -+ * creates a new encrypted store with the given password, -+ * creates a new wallet with 1 account. -+ * -+ * @fires KeyringController:unlock -+ * @param password - The password to encrypt the vault with. -+ * @param keyring - A object containing the params to instantiate a new keyring. -+ * @param keyring.type - The keyring type. -+ * @param keyring.opts - Optional parameters required to instantiate the keyring. -+ * @returns A promise that resolves to the state. -+ */ -+ __privateAdd(this, _createNewVaultWithKeyring); -+ /** -+ * Get the updated array of each keyring's type and -+ * accounts list. -+ * -+ * @returns A promise resolving to the updated keyrings array. -+ */ -+ __privateAdd(this, _getUpdatedKeyrings); -+ /** -+ * Serialize the current array of keyring instances, -+ * including unsupported keyrings by default. -+ * -+ * @param options - Method options. -+ * @param options.includeUnsupported - Whether to include unsupported keyrings. -+ * @returns The serialized keyrings. -+ */ -+ __privateAdd(this, _getSerializedKeyrings); -+ /** -+ * Restore a serialized keyrings array. -+ * -+ * @param serializedKeyrings - The serialized keyrings array. -+ */ -+ __privateAdd(this, _restoreSerializedKeyrings); -+ /** -+ * Unlock Keyrings, decrypting the vault and deserializing all -+ * keyrings contained in it, using a password or an encryption key with salt. -+ * -+ * @param password - The keyring controller password. -+ * @param encryptionKey - An exported key string to unlock keyrings with. -+ * @param encryptionSalt - The salt used to encrypt the vault. -+ * @returns A promise resolving to the deserialized keyrings array. -+ */ -+ __privateAdd(this, _unlockKeyrings); -+ /** -+ * Update the vault with the current keyrings. -+ * -+ * @returns A promise resolving to `true` if the operation is successful. -+ */ -+ __privateAdd(this, _updateVault); -+ /** -+ * Retrieves all the accounts from keyrings instances -+ * that are currently in memory. -+ * -+ * @returns A promise resolving to an array of accounts. -+ */ -+ __privateAdd(this, _getAccountsFromKeyrings); -+ /** -+ * Create a new keyring, ensuring that the first account is -+ * also created. -+ * -+ * @param type - Keyring type to instantiate. -+ * @param opts - Optional parameters required to instantiate the keyring. -+ * @returns A promise that resolves if the operation is successful. -+ */ -+ __privateAdd(this, _createKeyringWithFirstAccount); -+ /** -+ * Instantiate, initialize and return a new keyring of the given `type`, -+ * using the given `opts`. The keyring is built using the keyring builder -+ * registered for the given `type`. -+ * -+ * -+ * @param type - The type of keyring to add. -+ * @param data - The data to restore a previously serialized keyring. -+ * @returns The new keyring. -+ * @throws If the keyring includes duplicated accounts. -+ */ -+ __privateAdd(this, _newKeyring); -+ /** -+ * Remove all managed keyrings, destroying all their -+ * instances in memory. -+ */ -+ __privateAdd(this, _clearKeyrings); -+ /** -+ * Restore a Keyring from a provided serialized payload. -+ * On success, returns the resulting keyring instance. -+ * -+ * @param serialized - The serialized keyring. -+ * @returns The deserialized keyring or undefined if the keyring type is unsupported. -+ */ -+ __privateAdd(this, _restoreKeyring); -+ /** -+ * Destroy Keyring -+ * -+ * Some keyrings support a method called `destroy`, that destroys the -+ * keyring along with removing all its event listeners and, in some cases, -+ * clears the keyring bridge iframe from the DOM. -+ * -+ * @param keyring - The keyring to destroy. -+ */ -+ __privateAdd(this, _destroyKeyring); -+ /** -+ * Remove empty keyrings. -+ * -+ * Loops through the keyrings and removes the ones with empty accounts -+ * (usually after removing the last / only account) from a keyring. -+ */ -+ __privateAdd(this, _removeEmptyKeyrings); -+ /** -+ * Checks for duplicate keypairs, using the the first account in the given -+ * array. Rejects if a duplicate is found. -+ * -+ * Only supports 'Simple Key Pair'. -+ * -+ * @param type - The key pair type to check for. -+ * @param newAccountArray - Array of new accounts. -+ * @returns The account, if no duplicate is found. -+ */ -+ __privateAdd(this, _checkForDuplicate); -+ /** -+ * Set the `isUnlocked` to true and notify listeners -+ * through the messenger. -+ * -+ * @fires KeyringController:unlock -+ */ -+ __privateAdd(this, _setUnlocked); -+ /** -+ * Execute the given function after acquiring the controller lock -+ * and save the keyrings to state after it, or rollback to their -+ * previous state in case of error. -+ * -+ * @param fn - The function to execute. -+ * @returns The result of the function. -+ */ -+ __privateAdd(this, _persistOrRollback); -+ /** -+ * Execute the given function after acquiring the controller lock -+ * and rollback keyrings and password states in case of error. -+ * -+ * @param fn - The function to execute atomically. -+ * @returns The result of the function. -+ */ -+ __privateAdd(this, _withRollback); -+ /** -+ * Assert that the controller mutex is locked. -+ * -+ * @throws If the controller mutex is not locked. -+ */ -+ __privateAdd(this, _assertControllerMutexIsLocked); -+ /** -+ * Lock the controller mutex before executing the given function, -+ * and release it after the function is resolved or after an -+ * error is thrown. -+ * -+ * This wrapper ensures that each mutable operation that interacts with the -+ * controller and that changes its state is executed in a mutually exclusive way, -+ * preventing unsafe concurrent access that could lead to unpredictable behavior. -+ * -+ * @param fn - The function to execute while the controller mutex is locked. -+ * @returns The result of the function. -+ */ -+ __privateAdd(this, _withControllerLock); -+ /** -+ * Lock the vault mutex before executing the given function, -+ * and release it after the function is resolved or after an -+ * error is thrown. -+ * -+ * This ensures that each operation that interacts with the vault -+ * is executed in a mutually exclusive way. -+ * -+ * @param fn - The function to execute while the vault mutex is locked. -+ * @returns The result of the function. -+ */ -+ __privateAdd(this, _withVaultLock); -+ __privateAdd(this, _controllerOperationMutex, new Mutex()); -+ __privateAdd(this, _vaultOperationMutex, new Mutex()); -+ __privateAdd(this, _keyringBuilders, void 0); -+ __privateAdd(this, _keyrings, void 0); -+ __privateAdd(this, _unsupportedKeyrings, void 0); -+ __privateAdd(this, _password, void 0); -+ __privateAdd(this, _encryptor, void 0); -+ __privateAdd(this, _cacheEncryptionKey, void 0); -+ __privateAdd(this, _qrKeyringStateListener, void 0); -+ __privateSet(this, _keyringBuilders, keyringBuilders ? defaultKeyringBuilders.concat(keyringBuilders) : defaultKeyringBuilders); -+ __privateSet(this, _encryptor, encryptor); -+ __privateSet(this, _keyrings, []); -+ __privateSet(this, _unsupportedKeyrings, []); -+ __privateSet(this, _cacheEncryptionKey, Boolean(options.cacheEncryptionKey)); -+ if (__privateGet(this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(encryptor); -+ } -+ __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -+ } -+ /** -+ * Adds a new account to the default (first) HD seed phrase keyring. -+ * -+ * @param accountCount - Number of accounts before adding a new one, used to -+ * make the method idempotent. -+ * @returns Promise resolving to the added account address. -+ */ -+ async addNewAccount(accountCount) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found"); -+ } -+ const oldAccounts = await primaryKeyring.getAccounts(); -+ if (accountCount && oldAccounts.length !== accountCount) { -+ if (accountCount > oldAccounts.length) { -+ throw new Error("Account out of sequence"); -+ } -+ const existingAccount = oldAccounts[accountCount]; -+ if (!existingAccount) { -+ throw new Error(`Can't find account at index ${accountCount}`); -+ } -+ return existingAccount; -+ } -+ const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -+ await this.verifySeedPhrase(); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Adds a new account to the specified keyring. -+ * -+ * @param keyring - Keyring to add the account to. -+ * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent. -+ * @returns Promise resolving to the added account address -+ */ -+ async addNewAccountForKeyring(keyring, accountCount) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const oldAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ if (accountCount && oldAccounts.length !== accountCount) { -+ if (accountCount > oldAccounts.length) { -+ throw new Error("Account out of sequence"); -+ } -+ const existingAccount = oldAccounts[accountCount]; -+ assertIsStrictHexString(existingAccount); -+ return existingAccount; -+ } -+ await keyring.addAccounts(1); -+ const addedAccountAddress = (await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this)).find( -+ (selectedAddress) => !oldAccounts.includes(selectedAddress) -+ ); -+ assertIsStrictHexString(addedAccountAddress); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences. -+ * -+ * @returns Promise resolving to the added account address. -+ */ -+ async addNewAccountWithoutUpdate() { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found"); -+ } -+ const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -+ await this.verifySeedPhrase(); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Effectively the same as creating a new keychain then populating it -+ * using the given seed phrase. -+ * -+ * @param password - Password to unlock keychain. -+ * @param seed - A BIP39-compliant seed phrase as Uint8Array, -+ * either as a string or an array of UTF-8 bytes that represent the string. -+ * @returns Promise resolving when the operation ends successfully. -+ */ -+ async createNewVaultAndRestore(password, seed) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ assertIsValidPassword(password); -+ await __privateMethod(this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -+ type: "HD Key Tree" /* hd */, -+ opts: { -+ mnemonic: seed, -+ numberOfAccounts: 1 -+ } -+ }); -+ }); -+ } -+ /** -+ * Create a new primary keychain and wipe any previous keychains. -+ * -+ * @param password - Password to unlock the new vault. -+ * @returns Promise resolving when the operation ends successfully. -+ */ -+ async createNewVaultAndKeychain(password) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const accounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ if (!accounts.length) { -+ await __privateMethod(this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -+ type: "HD Key Tree" /* hd */ -+ }); -+ } -+ }); -+ } -+ /** -+ * Adds a new keyring of the given `type`. -+ * -+ * @param type - Keyring type name. -+ * @param opts - Keyring options. -+ * @throws If a builder for the given `type` does not exist. -+ * @returns Promise resolving to the added keyring. -+ */ -+ async addNewKeyring(type, opts) { -+ if (type === "QR Hardware Wallet Device" /* qr */) { -+ return this.getOrAddQRKeyring(); -+ } -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, opts)); -+ } -+ /** -+ * Method to verify a given password validity. Throws an -+ * error if the password is invalid. -+ * -+ * @param password - Password of the keyring. -+ */ -+ async verifyPassword(password) { -+ if (!this.state.vault) { -+ throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -+ } -+ await __privateGet(this, _encryptor).decrypt(password, this.state.vault); -+ } -+ /** -+ * Returns the status of the vault. -+ * -+ * @returns Boolean returning true if the vault is unlocked. -+ */ -+ isUnlocked() { -+ return this.state.isUnlocked; -+ } -+ /** -+ * Gets the seed phrase of the HD keyring. -+ * -+ * @param password - Password of the keyring. -+ * @returns Promise resolving to the seed phrase. -+ */ -+ async exportSeedPhrase(password) { -+ await this.verifyPassword(password); -+ assertHasUint8ArrayMnemonic(__privateGet(this, _keyrings)[0]); -+ return __privateGet(this, _keyrings)[0].mnemonic; -+ } -+ /** -+ * Gets the private key from the keyring controlling an address. -+ * -+ * @param password - Password of the keyring. -+ * @param address - Address to export. -+ * @returns Promise resolving to the private key for an address. -+ */ -+ async exportAccount(password, address) { -+ await this.verifyPassword(password); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.exportAccount) { -+ throw new Error("`KeyringController - The keyring for the current address does not support the method exportAccount" /* UnsupportedExportAccount */); -+ } -+ return await keyring.exportAccount(normalize(address)); -+ } -+ /** -+ * Returns the public addresses of all accounts from every keyring. -+ * -+ * @returns A promise resolving to an array of addresses. -+ */ -+ async getAccounts() { -+ return this.state.keyrings.reduce( -+ (accounts, keyring) => accounts.concat(keyring.accounts), -+ [] -+ ); -+ } -+ /** -+ * Get encryption public key. -+ * -+ * @param account - An account address. -+ * @param opts - Additional encryption options. -+ * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method -+ * @returns Promise resolving to encyption public key of the `account` if one exists. -+ */ -+ async getEncryptionPublicKey(account, opts) { -+ const address = ethNormalize(account); -+ const keyring = await this.getKeyringForAccount( -+ account -+ ); -+ if (!keyring.getEncryptionPublicKey) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey." /* UnsupportedGetEncryptionPublicKey */); -+ } -+ return await keyring.getEncryptionPublicKey(address, opts); -+ } -+ /** -+ * Attempts to decrypt the provided message parameters. -+ * -+ * @param messageParams - The decryption message parameters. -+ * @param messageParams.from - The address of the account you want to use to decrypt the message. -+ * @param messageParams.data - The encrypted data that you want to decrypt. -+ * @returns The raw decryption result. -+ */ -+ async decryptMessage(messageParams) { -+ const address = ethNormalize(messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.decryptMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method decryptMessage." /* UnsupportedDecryptMessage */); -+ } -+ return keyring.decryptMessage(address, messageParams.data); -+ } -+ /** -+ * Returns the currently initialized keyring that manages -+ * the specified `address` if one exists. -+ * -+ * @deprecated Use of this method is discouraged as actions executed directly on -+ * keyrings are not being reflected in the KeyringController state and not -+ * persisted in the vault. Use `withKeyring` instead. -+ * @param account - An account address. -+ * @returns Promise resolving to keyring of the `account` if one exists. -+ */ -+ async getKeyringForAccount(account) { -+ const address = normalize(account); -+ const candidates = await Promise.all( -+ __privateGet(this, _keyrings).map(async (keyring) => { -+ return Promise.all([keyring, keyring.getAccounts()]); -+ }) -+ ); -+ const winners = candidates.filter((candidate) => { -+ const accounts = candidate[1].map(normalize); -+ return accounts.includes(address); -+ }); -+ if (winners.length && winners[0]?.length) { -+ return winners[0][0]; -+ } -+ let errorInfo = ""; -+ if (!candidates.length) { -+ errorInfo = "There are no keyrings"; -+ } else if (!winners.length) { -+ errorInfo = "There are keyrings, but none match the address"; -+ } -+ throw new Error( -+ `${"KeyringController - No keyring found" /* NoKeyring */}. Error info: ${errorInfo}` -+ ); -+ } -+ /** -+ * Returns all keyrings of the given type. -+ * -+ * @deprecated Use of this method is discouraged as actions executed directly on -+ * keyrings are not being reflected in the KeyringController state and not -+ * persisted in the vault. Use `withKeyring` instead. -+ * @param type - Keyring type name. -+ * @returns An array of keyrings of the given type. -+ */ -+ getKeyringsByType(type) { -+ return __privateGet(this, _keyrings).filter((keyring) => keyring.type === type); -+ } -+ /** -+ * Persist all serialized keyrings in the vault. -+ * -+ * @deprecated This method is being phased out in favor of `withKeyring`. -+ * @returns Promise resolving with `true` value when the -+ * operation completes. -+ */ -+ async persistAllKeyrings() { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => true); -+ } -+ /** -+ * Imports an account with the specified import strategy. -+ * -+ * @param strategy - Import strategy name. -+ * @param args - Array of arguments to pass to the underlying stategy. -+ * @throws Will throw when passed an unrecognized strategy. -+ * @returns Promise resolving to the imported account address. -+ */ -+ async importAccountWithStrategy(strategy, args) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ let privateKey; -+ switch (strategy) { -+ case "privateKey": -+ const [importedKey] = args; -+ if (!importedKey) { -+ throw new Error("Cannot import an empty key."); -+ } -+ const prefixed = add0x(importedKey); -+ let bufferedPrivateKey; -+ try { -+ bufferedPrivateKey = toBuffer(prefixed); -+ } catch { -+ throw new Error("Cannot import invalid private key."); -+ } -+ if (!isValidPrivate(bufferedPrivateKey) || // ensures that the key is 64 bytes long -+ getBinarySize(prefixed) !== 64 + "0x".length) { -+ throw new Error("Cannot import invalid private key."); -+ } -+ privateKey = remove0x(prefixed); -+ break; -+ case "json": -+ let wallet; -+ const [input, password] = args; -+ try { -+ wallet = importers.fromEtherWallet(input, password); -+ } catch (e) { -+ wallet = wallet || await Wallet.fromV3(input, password, true); -+ } -+ privateKey = bytesToHex(wallet.getPrivateKey()); -+ break; -+ default: -+ throw new Error(`Unexpected import strategy: '${strategy}'`); -+ } -+ const newKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "Simple Key Pair" /* simple */, [ -+ privateKey -+ ]); -+ const accounts = await newKeyring.getAccounts(); -+ return accounts[0]; -+ }); -+ } -+ /** -+ * Removes an account from keyring state. -+ * -+ * @param address - Address of the account to remove. -+ * @fires KeyringController:accountRemoved -+ * @returns Promise resolving when the account is removed. -+ */ -+ async removeAccount(address) { -+ await __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.removeAccount) { -+ throw new Error("`KeyringController - The keyring for the current address does not support the method removeAccount" /* UnsupportedRemoveAccount */); -+ } -+ await keyring.removeAccount(address); -+ const accounts = await keyring.getAccounts(); -+ if (accounts.length === 0) { -+ await __privateMethod(this, _removeEmptyKeyrings, removeEmptyKeyrings_fn).call(this); -+ } -+ }); -+ this.messagingSystem.publish(`${name}:accountRemoved`, address); -+ } -+ /** -+ * Deallocates all secrets and locks the wallet. -+ * -+ * @returns Promise resolving when the operation completes. -+ */ -+ async setLocked() { -+ return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -+ __privateMethod(this, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn).call(this); -+ __privateSet(this, _password, void 0); -+ await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -+ this.update((state) => { -+ state.isUnlocked = false; -+ state.keyrings = []; -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ this.messagingSystem.publish(`${name}:lock`); -+ }); -+ } -+ /** -+ * Signs message by calling down into a specific keyring. -+ * -+ * @param messageParams - PersonalMessageParams object to sign. -+ * @returns Promise resolving to a signed message string. -+ */ -+ async signMessage(messageParams) { -+ if (!messageParams.data) { -+ throw new Error("Can't sign an empty message"); -+ } -+ const address = ethNormalize(messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signMessage." /* UnsupportedSignMessage */); -+ } -+ return await keyring.signMessage(address, messageParams.data); -+ } -+ /** -+ * Signs personal message by calling down into a specific keyring. -+ * -+ * @param messageParams - PersonalMessageParams object to sign. -+ * @returns Promise resolving to a signed message string. -+ */ -+ async signPersonalMessage(messageParams) { -+ const address = ethNormalize(messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signPersonalMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signPersonalMessage." /* UnsupportedSignPersonalMessage */); -+ } -+ const normalizedData = normalize(messageParams.data); -+ return await keyring.signPersonalMessage(address, normalizedData); -+ } -+ /** -+ * Signs typed message by calling down into a specific keyring. -+ * -+ * @param messageParams - TypedMessageParams object to sign. -+ * @param version - Compatibility version EIP712. -+ * @throws Will throw when passed an unrecognized version. -+ * @returns Promise resolving to a signed message string or an error if any. -+ */ -+ async signTypedMessage(messageParams, version) { -+ try { -+ if (![ -+ "V1" /* V1 */, -+ "V3" /* V3 */, -+ "V4" /* V4 */ -+ ].includes(version)) { -+ throw new Error(`Unexpected signTypedMessage version: '${version}'`); -+ } -+ const address = ethNormalize(messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signTypedData) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signTypedMessage." /* UnsupportedSignTypedMessage */); -+ } -+ return await keyring.signTypedData( -+ address, -+ version !== "V1" /* V1 */ && typeof messageParams.data === "string" ? JSON.parse(messageParams.data) : messageParams.data, -+ { version } -+ ); -+ } catch (error) { -+ throw new Error(`Keyring Controller signTypedMessage: ${error}`); -+ } -+ } -+ /** -+ * Signs a transaction by calling down into a specific keyring. -+ * -+ * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance. -+ * @param from - Address to sign from, should be in keychain. -+ * @param opts - An optional options object. -+ * @returns Promise resolving to a signed transaction string. -+ */ -+ async signTransaction(transaction, from, opts) { -+ const address = ethNormalize(from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signTransaction) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signTransaction." /* UnsupportedSignTransaction */); -+ } -+ return await keyring.signTransaction(address, transaction, opts); -+ } -+ /** -+ * Convert a base transaction to a base UserOperation. -+ * -+ * @param from - Address of the sender. -+ * @param transactions - Base transactions to include in the UserOperation. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns A pseudo-UserOperation that can be used to construct a real. -+ */ -+ async prepareUserOperation(from, transactions, executionContext) { -+ const address = ethNormalize(from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.prepareUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method prepareUserOperation." /* UnsupportedPrepareUserOperation */); -+ } -+ return await keyring.prepareUserOperation( -+ address, -+ transactions, -+ executionContext -+ ); -+ } -+ /** -+ * Patches properties of a UserOperation. Currently, only the -+ * `paymasterAndData` can be patched. -+ * -+ * @param from - Address of the sender. -+ * @param userOp - UserOperation to patch. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns A patch to apply to the UserOperation. -+ */ -+ async patchUserOperation(from, userOp, executionContext) { -+ const address = ethNormalize(from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.patchUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method patchUserOperation." /* UnsupportedPatchUserOperation */); -+ } -+ return await keyring.patchUserOperation(address, userOp, executionContext); -+ } -+ /** -+ * Signs an UserOperation. -+ * -+ * @param from - Address of the sender. -+ * @param userOp - UserOperation to sign. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns The signature of the UserOperation. -+ */ -+ async signUserOperation(from, userOp, executionContext) { -+ const address = ethNormalize(from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signUserOperation." /* UnsupportedSignUserOperation */); -+ } -+ return await keyring.signUserOperation(address, userOp, executionContext); -+ } -+ /** -+ * Changes the password used to encrypt the vault. -+ * -+ * @param password - The new password. -+ * @returns Promise resolving when the operation completes. -+ */ -+ changePassword(password) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ if (!this.state.isUnlocked) { -+ throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -+ } -+ assertIsValidPassword(password); -+ __privateSet(this, _password, password); -+ if (__privateGet(this, _cacheEncryptionKey)) { -+ this.update((state) => { -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ } -+ }); -+ } -+ /** -+ * Attempts to decrypt the current vault and load its keyrings, -+ * using the given encryption key and salt. -+ * -+ * @param encryptionKey - Key to unlock the keychain. -+ * @param encryptionSalt - Salt to unlock the keychain. -+ * @returns Promise resolving when the operation completes. -+ */ -+ async submitEncryptionKey(encryptionKey, encryptionSalt) { -+ return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -+ __privateSet(this, _keyrings, await __privateMethod(this, _unlockKeyrings, unlockKeyrings_fn).call(this, void 0, encryptionKey, encryptionSalt)); -+ __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); -+ }); -+ } -+ /** -+ * Attempts to decrypt the current vault and load its keyrings, -+ * using the given password. -+ * -+ * @param password - Password to unlock the keychain. -+ * @returns Promise resolving when the operation completes. -+ */ -+ async submitPassword(password) { -+ return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -+ __privateSet(this, _keyrings, await __privateMethod(this, _unlockKeyrings, unlockKeyrings_fn).call(this, password)); -+ __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); -+ }); -+ } -+ /** -+ * Verifies the that the seed phrase restores the current keychain's accounts. -+ * -+ * @returns Promise resolving to the seed phrase as Uint8Array. -+ */ -+ async verifySeedPhrase() { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree" /* hd */)[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found."); -+ } -+ assertHasUint8ArrayMnemonic(primaryKeyring); -+ const seedWords = primaryKeyring.mnemonic; -+ const accounts = await primaryKeyring.getAccounts(); -+ if (accounts.length === 0) { -+ throw new Error("Cannot verify an empty keyring."); -+ } -+ const hdKeyringBuilder = __privateMethod(this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, "HD Key Tree" /* hd */); -+ const hdKeyring = hdKeyringBuilder(); -+ await hdKeyring.deserialize({ -+ mnemonic: seedWords, -+ numberOfAccounts: accounts.length -+ }); -+ const testAccounts = await hdKeyring.getAccounts(); -+ if (testAccounts.length !== accounts.length) { -+ throw new Error("Seed phrase imported incorrect number of accounts."); -+ } -+ testAccounts.forEach((account, i) => { -+ if (account.toLowerCase() !== accounts[i].toLowerCase()) { -+ throw new Error("Seed phrase imported different accounts."); -+ } -+ }); -+ return seedWords; -+ } -+ async withKeyring(selector, operation, options = { -+ createIfMissing: false -+ }) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ let keyring; -+ if ("address" in selector) { -+ keyring = await this.getKeyringForAccount(selector.address); -+ } else { -+ keyring = this.getKeyringsByType(selector.type)[selector.index || 0]; -+ if (!keyring && options.createIfMissing) { -+ keyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, selector.type, options.createWithData); -+ } -+ } -+ if (!keyring) { -+ throw new Error("KeyringController - Keyring not found." /* KeyringNotFound */); -+ } -+ const result = await operation(keyring); -+ if (Object.is(result, keyring)) { -+ throw new Error("KeyringController - Returning keyring instances is unsafe" /* UnsafeDirectKeyringAccess */); -+ } -+ return result; -+ }); -+ } -+ // QR Hardware related methods -+ /** -+ * Get QR Hardware keyring. -+ * -+ * @returns The QR Keyring if defined, otherwise undefined -+ */ -+ getQRKeyring() { -+ return this.getKeyringsByType("QR Hardware Wallet Device" /* qr */)[0]; -+ } -+ /** -+ * Get QR hardware keyring. If it doesn't exist, add it. -+ * -+ * @returns The added keyring -+ */ -+ async getOrAddQRKeyring() { -+ return this.getQRKeyring() || await __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this)); -+ } -+ // TODO: Replace `any` with type -+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -+ async restoreQRKeyring(serialized) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -+ keyring.deserialize(serialized); -+ }); -+ } -+ async resetQRKeyringState() { -+ (await this.getOrAddQRKeyring()).resetStore(); -+ } -+ async getQRKeyringState() { -+ return (await this.getOrAddQRKeyring()).getMemStore(); -+ } -+ async submitQRCryptoHDKey(cryptoHDKey) { -+ (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey); -+ } -+ async submitQRCryptoAccount(cryptoAccount) { -+ (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount); -+ } -+ async submitQRSignature(requestId, ethSignature) { -+ (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature); -+ } -+ async cancelQRSignRequest() { -+ (await this.getOrAddQRKeyring()).cancelSignRequest(); -+ } -+ /** -+ * Cancels qr keyring sync. -+ */ -+ async cancelQRSynchronization() { -+ (await this.getOrAddQRKeyring()).cancelSync(); -+ } -+ async connectQRHardware(page) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ try { -+ const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -+ let accounts; -+ switch (page) { -+ case -1: -+ accounts = await keyring.getPreviousPage(); -+ break; -+ case 1: -+ accounts = await keyring.getNextPage(); -+ break; -+ default: -+ accounts = await keyring.getFirstPage(); -+ } -+ return accounts.map((account) => { -+ return { -+ ...account, -+ balance: "0x0" -+ }; -+ }); -+ } catch (e) { -+ throw new Error(`Unspecified error when connect QR Hardware, ${e}`); -+ } -+ }); -+ } -+ async unlockQRHardwareWalletAccount(index) { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -+ keyring.setAccountToUnlock(index); -+ await keyring.addAccounts(1); -+ }); -+ } -+ async getAccountKeyringType(account) { -+ const keyring = await this.getKeyringForAccount( -+ account -+ ); -+ return keyring.type; -+ } -+ async forgetQRDevice() { -+ return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring(); -+ if (!keyring) { -+ return { removedAccounts: [], remainingAccounts: [] }; -+ } -+ const allAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ keyring.forgetDevice(); -+ const remainingAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ const removedAccounts = allAccounts.filter( -+ (address) => !remainingAccounts.includes(address) -+ ); -+ return { removedAccounts, remainingAccounts }; -+ }); -+ } -+}; -+_controllerOperationMutex = new WeakMap(); -+_vaultOperationMutex = new WeakMap(); -+_keyringBuilders = new WeakMap(); -+_keyrings = new WeakMap(); -+_unsupportedKeyrings = new WeakMap(); -+_password = new WeakMap(); -+_encryptor = new WeakMap(); -+_cacheEncryptionKey = new WeakMap(); -+_qrKeyringStateListener = new WeakMap(); -+_registerMessageHandlers = new WeakSet(); -+registerMessageHandlers_fn = function() { -+ this.messagingSystem.registerActionHandler( -+ `${name}:signMessage`, -+ this.signMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signPersonalMessage`, -+ this.signPersonalMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signTypedMessage`, -+ this.signTypedMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:decryptMessage`, -+ this.decryptMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getEncryptionPublicKey`, -+ this.getEncryptionPublicKey.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getAccounts`, -+ this.getAccounts.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getKeyringsByType`, -+ this.getKeyringsByType.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getKeyringForAccount`, -+ this.getKeyringForAccount.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:persistAllKeyrings`, -+ this.persistAllKeyrings.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:prepareUserOperation`, -+ this.prepareUserOperation.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:patchUserOperation`, -+ this.patchUserOperation.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signUserOperation`, -+ this.signUserOperation.bind(this) -+ ); -+}; -+_getKeyringBuilderForType = new WeakSet(); -+getKeyringBuilderForType_fn = function(type) { -+ return __privateGet(this, _keyringBuilders).find( -+ (keyringBuilder) => keyringBuilder.type === type -+ ); -+}; -+_addQRKeyring = new WeakSet(); -+addQRKeyring_fn = async function() { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ return await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */); -+}; -+_subscribeToQRKeyringEvents = new WeakSet(); -+subscribeToQRKeyringEvents_fn = function(qrKeyring) { -+ __privateSet(this, _qrKeyringStateListener, (state) => { -+ this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state); -+ }); -+ qrKeyring.getMemStore().subscribe(__privateGet(this, _qrKeyringStateListener)); -+}; -+_unsubscribeFromQRKeyringsEvents = new WeakSet(); -+unsubscribeFromQRKeyringsEvents_fn = function() { -+ const qrKeyrings = this.getKeyringsByType( -+ "QR Hardware Wallet Device" /* qr */ -+ ); -+ qrKeyrings.forEach((qrKeyring) => { -+ if (__privateGet(this, _qrKeyringStateListener)) { -+ qrKeyring.getMemStore().unsubscribe(__privateGet(this, _qrKeyringStateListener)); -+ } -+ }); -+}; -+_createNewVaultWithKeyring = new WeakSet(); -+createNewVaultWithKeyring_fn = async function(password, keyring) { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ if (typeof password !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ this.update((state) => { -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ __privateSet(this, _password, password); -+ await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -+ await __privateMethod(this, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn).call(this, keyring.type, keyring.opts); -+ __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); -+}; -+_getUpdatedKeyrings = new WeakSet(); -+getUpdatedKeyrings_fn = async function() { -+ return Promise.all(__privateGet(this, _keyrings).map(displayForKeyring)); -+}; -+_getSerializedKeyrings = new WeakSet(); -+getSerializedKeyrings_fn = async function({ includeUnsupported } = { -+ includeUnsupported: true -+}) { -+ const serializedKeyrings = await Promise.all( -+ __privateGet(this, _keyrings).map(async (keyring) => { -+ const [type, data] = await Promise.all([ -+ keyring.type, -+ keyring.serialize() -+ ]); -+ return { type, data }; -+ }) -+ ); -+ if (includeUnsupported) { -+ serializedKeyrings.push(...__privateGet(this, _unsupportedKeyrings)); -+ } -+ return serializedKeyrings; -+}; -+_restoreSerializedKeyrings = new WeakSet(); -+restoreSerializedKeyrings_fn = async function(serializedKeyrings) { -+ await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -+ for (const serializedKeyring of serializedKeyrings) { -+ await __privateMethod(this, _restoreKeyring, restoreKeyring_fn).call(this, serializedKeyring); -+ } -+}; -+_unlockKeyrings = new WeakSet(); -+unlockKeyrings_fn = async function(password, encryptionKey, encryptionSalt) { -+ return __privateMethod(this, _withVaultLock, withVaultLock_fn).call(this, async ({ releaseLock }) => { -+ const encryptedVault = this.state.vault; -+ if (!encryptedVault) { -+ throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -+ } -+ let vault; -+ const updatedState = {}; -+ if (__privateGet(this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(__privateGet(this, _encryptor)); -+ if (password) { -+ const result = await __privateGet(this, _encryptor).decryptWithDetail( -+ password, -+ encryptedVault -+ ); -+ vault = result.vault; -+ __privateSet(this, _password, password); -+ updatedState.encryptionKey = result.exportedKeyString; -+ updatedState.encryptionSalt = result.salt; -+ } else { -+ const parsedEncryptedVault = JSON.parse(encryptedVault); -+ if (encryptionSalt !== parsedEncryptedVault.salt) { -+ throw new Error("KeyringController - Encryption key and salt provided are expired" /* ExpiredCredentials */); -+ } -+ if (typeof encryptionKey !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ const key = await __privateGet(this, _encryptor).importKey(encryptionKey); -+ vault = await __privateGet(this, _encryptor).decryptWithKey( -+ key, -+ parsedEncryptedVault -+ ); -+ updatedState.encryptionKey = encryptionKey; -+ updatedState.encryptionSalt = encryptionSalt; -+ } -+ } else { -+ if (typeof password !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ vault = await __privateGet(this, _encryptor).decrypt(password, encryptedVault); -+ __privateSet(this, _password, password); -+ } -+ if (!isSerializedKeyringsArray(vault)) { -+ throw new Error("KeyringController - The decrypted vault has an unexpected shape." /* VaultDataError */); -+ } -+ await __privateMethod(this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, vault); -+ const updatedKeyrings = await __privateMethod(this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -+ this.update((state) => { -+ state.keyrings = updatedKeyrings; -+ if (updatedState.encryptionKey || updatedState.encryptionSalt) { -+ state.encryptionKey = updatedState.encryptionKey; -+ state.encryptionSalt = updatedState.encryptionSalt; -+ } -+ }); -+ if (__privateGet(this, _password) && (!__privateGet(this, _cacheEncryptionKey) || !encryptionKey) && __privateGet(this, _encryptor).isVaultUpdated && !__privateGet(this, _encryptor).isVaultUpdated(encryptedVault)) { -+ releaseLock(); -+ await __privateMethod(this, _updateVault, updateVault_fn).call(this); -+ } -+ return __privateGet(this, _keyrings); -+ }); -+}; -+_updateVault = new WeakSet(); -+updateVault_fn = function() { -+ return __privateMethod(this, _withVaultLock, withVaultLock_fn).call(this, async () => { -+ const { encryptionKey, encryptionSalt } = this.state; -+ if (!__privateGet(this, _password) && !encryptionKey) { -+ throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -+ } -+ const serializedKeyrings = await __privateMethod(this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -+ if (!serializedKeyrings.some((keyring) => keyring.type === "HD Key Tree" /* hd */)) { -+ throw new Error("KeyringController - No HD Keyring found" /* NoHdKeyring */); -+ } -+ const updatedState = {}; -+ if (__privateGet(this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(__privateGet(this, _encryptor)); -+ if (encryptionKey) { -+ const key = await __privateGet(this, _encryptor).importKey(encryptionKey); -+ const vaultJSON = await __privateGet(this, _encryptor).encryptWithKey( -+ key, -+ serializedKeyrings -+ ); -+ vaultJSON.salt = encryptionSalt; -+ updatedState.vault = JSON.stringify(vaultJSON); -+ } else if (__privateGet(this, _password)) { -+ const { vault: newVault, exportedKeyString } = await __privateGet(this, _encryptor).encryptWithDetail( -+ __privateGet(this, _password), -+ serializedKeyrings -+ ); -+ updatedState.vault = newVault; -+ updatedState.encryptionKey = exportedKeyString; -+ } -+ } else { -+ assertIsValidPassword(__privateGet(this, _password)); -+ updatedState.vault = await __privateGet(this, _encryptor).encrypt( -+ __privateGet(this, _password), -+ serializedKeyrings -+ ); -+ } -+ if (!updatedState.vault) { -+ throw new Error("KeyringController - Cannot persist vault without vault information" /* MissingVaultData */); -+ } -+ const updatedKeyrings = await __privateMethod(this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -+ this.update((state) => { -+ state.vault = updatedState.vault; -+ state.keyrings = updatedKeyrings; -+ if (updatedState.encryptionKey) { -+ state.encryptionKey = updatedState.encryptionKey; -+ state.encryptionSalt = JSON.parse(updatedState.vault).salt; -+ } -+ }); -+ return true; -+ }); -+}; -+_getAccountsFromKeyrings = new WeakSet(); -+getAccountsFromKeyrings_fn = async function() { -+ const keyrings = __privateGet(this, _keyrings); -+ const keyringArrays = await Promise.all( -+ keyrings.map(async (keyring) => keyring.getAccounts()) -+ ); -+ const addresses = keyringArrays.reduce((res, arr) => { -+ return res.concat(arr); -+ }, []); -+ return addresses.map(normalize); -+}; -+_createKeyringWithFirstAccount = new WeakSet(); -+createKeyringWithFirstAccount_fn = async function(type, opts) { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const keyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, opts); -+ const [firstAccount] = await keyring.getAccounts(); -+ if (!firstAccount) { -+ throw new Error("KeyringController - First Account not found." /* NoFirstAccount */); -+ } -+}; -+_newKeyring = new WeakSet(); -+newKeyring_fn = async function(type, data) { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const keyringBuilder = __privateMethod(this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, type); -+ if (!keyringBuilder) { -+ throw new Error( -+ `${"KeyringController - No keyringBuilder found for keyring" /* NoKeyringBuilder */}. Keyring type: ${type}` -+ ); -+ } -+ const keyring = keyringBuilder(); -+ await keyring.deserialize(data); -+ if (keyring.init) { -+ await keyring.init(); -+ } -+ if (type === "HD Key Tree" /* hd */ && (!isObject(data) || !data.mnemonic)) { -+ if (!keyring.generateRandomMnemonic) { -+ throw new Error( -+ "KeyringController - The current keyring does not support the method generateRandomMnemonic." /* UnsupportedGenerateRandomMnemonic */ -+ ); -+ } -+ keyring.generateRandomMnemonic(); -+ await keyring.addAccounts(1); -+ } -+ await __privateMethod(this, _checkForDuplicate, checkForDuplicate_fn).call(this, type, await keyring.getAccounts()); -+ if (type === "QR Hardware Wallet Device" /* qr */) { -+ __privateMethod(this, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn).call(this, keyring); -+ } -+ __privateGet(this, _keyrings).push(keyring); -+ return keyring; -+}; -+_clearKeyrings = new WeakSet(); -+clearKeyrings_fn = async function() { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ for (const keyring of __privateGet(this, _keyrings)) { -+ await __privateMethod(this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -+ } -+ __privateSet(this, _keyrings, []); -+}; -+_restoreKeyring = new WeakSet(); -+restoreKeyring_fn = async function(serialized) { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ try { -+ const { type, data } = serialized; -+ return await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, data); -+ } catch (_) { -+ __privateGet(this, _unsupportedKeyrings).push(serialized); -+ return void 0; -+ } -+}; -+_destroyKeyring = new WeakSet(); -+destroyKeyring_fn = async function(keyring) { -+ await keyring.destroy?.(); -+}; -+_removeEmptyKeyrings = new WeakSet(); -+removeEmptyKeyrings_fn = async function() { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const validKeyrings = []; -+ await Promise.all( -+ __privateGet(this, _keyrings).map(async (keyring) => { -+ const accounts = await keyring.getAccounts(); -+ if (accounts.length > 0) { -+ validKeyrings.push(keyring); -+ } else { -+ await __privateMethod(this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -+ } -+ }) -+ ); -+ __privateSet(this, _keyrings, validKeyrings); -+}; -+_checkForDuplicate = new WeakSet(); -+checkForDuplicate_fn = async function(type, newAccountArray) { -+ const accounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ switch (type) { -+ case "Simple Key Pair" /* simple */: { -+ const isIncluded = Boolean( -+ accounts.find( -+ (key) => newAccountArray[0] && (key === newAccountArray[0] || key === remove0x(newAccountArray[0])) -+ ) -+ ); -+ if (isIncluded) { -+ throw new Error("KeyringController - The account you are trying to import is a duplicate" /* DuplicatedAccount */); -+ } -+ return newAccountArray; -+ } -+ default: { -+ return newAccountArray; -+ } -+ } -+}; -+_setUnlocked = new WeakSet(); -+setUnlocked_fn = function() { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ this.update((state) => { -+ state.isUnlocked = true; -+ }); -+ this.messagingSystem.publish(`${name}:unlock`); -+}; -+_persistOrRollback = new WeakSet(); -+persistOrRollback_fn = async function(fn) { -+ return __privateMethod(this, _withRollback, withRollback_fn).call(this, async ({ releaseLock }) => { -+ const callbackResult = await fn({ releaseLock }); -+ await __privateMethod(this, _updateVault, updateVault_fn).call(this); -+ return callbackResult; -+ }); -+}; -+_withRollback = new WeakSet(); -+withRollback_fn = async function(fn) { -+ return __privateMethod(this, _withControllerLock, withControllerLock_fn).call(this, async ({ releaseLock }) => { -+ const currentSerializedKeyrings = await __privateMethod(this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -+ const currentPassword = __privateGet(this, _password); -+ try { -+ return await fn({ releaseLock }); -+ } catch (e) { -+ await __privateMethod(this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, currentSerializedKeyrings); -+ __privateSet(this, _password, currentPassword); -+ throw e; -+ } -+ }); -+}; -+_assertControllerMutexIsLocked = new WeakSet(); -+assertControllerMutexIsLocked_fn = function() { -+ if (!__privateGet(this, _controllerOperationMutex).isLocked()) { -+ throw new Error("KeyringController - attempt to update vault during a non mutually exclusive operation" /* ControllerLockRequired */); -+ } -+}; -+_withControllerLock = new WeakSet(); -+withControllerLock_fn = async function(fn) { -+ return withLock(__privateGet(this, _controllerOperationMutex), fn); -+}; -+_withVaultLock = new WeakSet(); -+withVaultLock_fn = async function(fn) { -+ __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ return withLock(__privateGet(this, _vaultOperationMutex), fn); -+}; -+async function withLock(mutex, fn) { -+ const releaseLock = await mutex.acquire(); -+ try { -+ return await fn({ releaseLock }); -+ } finally { -+ releaseLock(); -+ } -+} -+var KeyringController_default = KeyringController; -+ -+export { -+ KeyringTypes, -+ isCustodyKeyring, -+ AccountImportStrategy, -+ SignTypedDataVersion, -+ keyringBuilderFactory, -+ getDefaultKeyringState, -+ KeyringController, -+ KeyringController_default -+}; -+//# sourceMappingURL=chunk-7A7D7THR.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-7A7D7THR.mjs.map b/dist/chunk-7A7D7THR.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..1e66368615fd8c5550c36f68586a25339b072f2f ---- /dev/null -+++ b/dist/chunk-7A7D7THR.mjs.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/KeyringController.ts"],"sourcesContent":["import type { TxData, TypedTransaction } from '@ethereumjs/tx';\nimport { isValidPrivate, toBuffer, getBinarySize } from '@ethereumjs/util';\nimport type {\n MetaMaskKeyring as QRKeyring,\n IKeyringState as IQRKeyringState,\n} from '@keystonehq/metamask-airgapped-keyring';\nimport type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport * as encryptorUtils from '@metamask/browser-passworder';\nimport HDKeyring from '@metamask/eth-hd-keyring';\nimport { normalize as ethNormalize } from '@metamask/eth-sig-util';\nimport SimpleKeyring from '@metamask/eth-simple-keyring';\nimport type {\n EthBaseTransaction,\n EthBaseUserOperation,\n EthKeyring,\n EthUserOperation,\n EthUserOperationPatch,\n KeyringExecutionContext,\n} from '@metamask/keyring-api';\nimport type {\n PersonalMessageParams,\n TypedMessageParams,\n} from '@metamask/message-manager';\nimport type {\n Eip1024EncryptedData,\n Hex,\n Json,\n KeyringClass,\n} from '@metamask/utils';\nimport {\n add0x,\n assertIsStrictHexString,\n bytesToHex,\n hasProperty,\n isObject,\n isStrictHexString,\n isValidHexAddress,\n isValidJson,\n remove0x,\n} from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\nimport type { MutexInterface } from 'async-mutex';\nimport Wallet, { thirdparty as importers } from 'ethereumjs-wallet';\nimport type { Patch } from 'immer';\n\nimport { KeyringControllerError } from './constants';\n\nconst name = 'KeyringController';\n\n/**\n * Available keyring types\n */\nexport enum KeyringTypes {\n simple = 'Simple Key Pair',\n hd = 'HD Key Tree',\n qr = 'QR Hardware Wallet Device',\n trezor = 'Trezor Hardware',\n ledger = 'Ledger Hardware',\n lattice = 'Lattice Hardware',\n snap = 'Snap Keyring',\n}\n\n/**\n * Custody keyring types are a special case, as they are not a single type\n * but they all start with the prefix \"Custody\".\n * @param keyringType - The type of the keyring.\n * @returns Whether the keyring type is a custody keyring.\n */\nexport const isCustodyKeyring = (keyringType: string): boolean => {\n return keyringType.startsWith('Custody');\n};\n\n/**\n * @type KeyringControllerState\n *\n * Keyring controller state\n * @property vault - Encrypted string representing keyring data\n * @property isUnlocked - Whether vault is unlocked\n * @property keyringTypes - Account types\n * @property keyrings - Group of accounts\n * @property encryptionKey - Keyring encryption key\n * @property encryptionSalt - Keyring encryption salt\n */\nexport type KeyringControllerState = {\n vault?: string;\n isUnlocked: boolean;\n keyrings: KeyringObject[];\n encryptionKey?: string;\n encryptionSalt?: string;\n};\n\nexport type KeyringControllerMemState = Omit<\n KeyringControllerState,\n 'vault' | 'encryptionKey' | 'encryptionSalt'\n>;\n\nexport type KeyringControllerGetStateAction = {\n type: `${typeof name}:getState`;\n handler: () => KeyringControllerState;\n};\n\nexport type KeyringControllerSignMessageAction = {\n type: `${typeof name}:signMessage`;\n handler: KeyringController['signMessage'];\n};\n\nexport type KeyringControllerSignPersonalMessageAction = {\n type: `${typeof name}:signPersonalMessage`;\n handler: KeyringController['signPersonalMessage'];\n};\n\nexport type KeyringControllerSignTypedMessageAction = {\n type: `${typeof name}:signTypedMessage`;\n handler: KeyringController['signTypedMessage'];\n};\n\nexport type KeyringControllerDecryptMessageAction = {\n type: `${typeof name}:decryptMessage`;\n handler: KeyringController['decryptMessage'];\n};\n\nexport type KeyringControllerGetEncryptionPublicKeyAction = {\n type: `${typeof name}:getEncryptionPublicKey`;\n handler: KeyringController['getEncryptionPublicKey'];\n};\n\nexport type KeyringControllerGetKeyringsByTypeAction = {\n type: `${typeof name}:getKeyringsByType`;\n handler: KeyringController['getKeyringsByType'];\n};\n\nexport type KeyringControllerGetKeyringForAccountAction = {\n type: `${typeof name}:getKeyringForAccount`;\n handler: KeyringController['getKeyringForAccount'];\n};\n\nexport type KeyringControllerGetAccountsAction = {\n type: `${typeof name}:getAccounts`;\n handler: KeyringController['getAccounts'];\n};\n\nexport type KeyringControllerPersistAllKeyringsAction = {\n type: `${typeof name}:persistAllKeyrings`;\n handler: KeyringController['persistAllKeyrings'];\n};\n\nexport type KeyringControllerPrepareUserOperationAction = {\n type: `${typeof name}:prepareUserOperation`;\n handler: KeyringController['prepareUserOperation'];\n};\n\nexport type KeyringControllerPatchUserOperationAction = {\n type: `${typeof name}:patchUserOperation`;\n handler: KeyringController['patchUserOperation'];\n};\n\nexport type KeyringControllerSignUserOperationAction = {\n type: `${typeof name}:signUserOperation`;\n handler: KeyringController['signUserOperation'];\n};\n\nexport type KeyringControllerStateChangeEvent = {\n type: `${typeof name}:stateChange`;\n payload: [KeyringControllerState, Patch[]];\n};\n\nexport type KeyringControllerAccountRemovedEvent = {\n type: `${typeof name}:accountRemoved`;\n payload: [string];\n};\n\nexport type KeyringControllerLockEvent = {\n type: `${typeof name}:lock`;\n payload: [];\n};\n\nexport type KeyringControllerUnlockEvent = {\n type: `${typeof name}:unlock`;\n payload: [];\n};\n\nexport type KeyringControllerQRKeyringStateChangeEvent = {\n type: `${typeof name}:qrKeyringStateChange`;\n payload: [ReturnType];\n};\n\nexport type KeyringControllerActions =\n | KeyringControllerGetStateAction\n | KeyringControllerSignMessageAction\n | KeyringControllerSignPersonalMessageAction\n | KeyringControllerSignTypedMessageAction\n | KeyringControllerDecryptMessageAction\n | KeyringControllerGetEncryptionPublicKeyAction\n | KeyringControllerGetAccountsAction\n | KeyringControllerGetKeyringsByTypeAction\n | KeyringControllerGetKeyringForAccountAction\n | KeyringControllerPersistAllKeyringsAction\n | KeyringControllerPrepareUserOperationAction\n | KeyringControllerPatchUserOperationAction\n | KeyringControllerSignUserOperationAction;\n\nexport type KeyringControllerEvents =\n | KeyringControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent\n | KeyringControllerAccountRemovedEvent\n | KeyringControllerQRKeyringStateChangeEvent;\n\nexport type KeyringControllerMessenger = RestrictedControllerMessenger<\n typeof name,\n KeyringControllerActions,\n KeyringControllerEvents,\n never,\n never\n>;\n\nexport type KeyringControllerOptions = {\n keyringBuilders?: { (): EthKeyring; type: string }[];\n messenger: KeyringControllerMessenger;\n state?: { vault?: string };\n} & (\n | {\n cacheEncryptionKey: true;\n encryptor?: ExportableKeyEncryptor;\n }\n | {\n cacheEncryptionKey?: false;\n encryptor?: GenericEncryptor | ExportableKeyEncryptor;\n }\n);\n\n/**\n * @type KeyringObject\n *\n * Keyring object to return in fullUpdate\n * @property type - Keyring type\n * @property accounts - Associated accounts\n */\nexport type KeyringObject = {\n accounts: string[];\n type: string;\n};\n\n/**\n * A strategy for importing an account\n */\nexport enum AccountImportStrategy {\n privateKey = 'privateKey',\n json = 'json',\n}\n\n/**\n * The `signTypedMessage` version\n *\n * @see https://docs.metamask.io/guide/signing-data.html\n */\nexport enum SignTypedDataVersion {\n V1 = 'V1',\n V3 = 'V3',\n V4 = 'V4',\n}\n\n/**\n * A serialized keyring object.\n */\nexport type SerializedKeyring = {\n type: string;\n data: Json;\n};\n\n/**\n * A generic encryptor interface that supports encrypting and decrypting\n * serializable data with a password.\n */\nexport type GenericEncryptor = {\n /**\n * Encrypts the given object with the given password.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encrypted string.\n */\n encrypt: (password: string, object: Json) => Promise;\n /**\n * Decrypts the given encrypted string with the given password.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decrypt: (password: string, encryptedString: string) => Promise;\n /**\n * Optional vault migration helper. Checks if the provided vault is up to date\n * with the desired encryption algorithm.\n *\n * @param vault - The encrypted string to check.\n * @param targetDerivationParams - The desired target derivation params.\n * @returns The updated encrypted string.\n */\n isVaultUpdated?: (\n vault: string,\n targetDerivationParams?: encryptorUtils.KeyDerivationOptions,\n ) => boolean;\n};\n\n/**\n * An encryptor interface that supports encrypting and decrypting\n * serializable data with a password, and exporting and importing keys.\n */\nexport type ExportableKeyEncryptor = GenericEncryptor & {\n /**\n * Encrypts the given object with the given encryption key.\n *\n * @param key - The encryption key to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encryption result.\n */\n encryptWithKey: (\n key: unknown,\n object: Json,\n ) => Promise;\n /**\n * Encrypts the given object with the given password, and returns the\n * encryption result and the exported key string.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @param salt - The optional salt to use for encryption.\n * @returns The encrypted string and the exported key string.\n */\n encryptWithDetail: (\n password: string,\n object: Json,\n salt?: string,\n ) => Promise;\n /**\n * Decrypts the given encrypted string with the given encryption key.\n *\n * @param key - The encryption key to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decryptWithKey: (key: unknown, encryptedString: string) => Promise;\n /**\n * Decrypts the given encrypted string with the given password, and returns\n * the decrypted object and the salt and exported key string used for\n * encryption.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object and the salt and exported key string used for\n * encryption.\n */\n decryptWithDetail: (\n password: string,\n encryptedString: string,\n ) => Promise;\n /**\n * Generates an encryption key from exported key string.\n *\n * @param key - The exported key string.\n * @returns The encryption key.\n */\n importKey: (key: string) => Promise;\n};\n\nexport type KeyringSelector =\n | {\n type: string;\n index?: number;\n }\n | {\n address: Hex;\n };\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise;\n\n/**\n * Get builder function for `Keyring`\n *\n * Returns a builder function for `Keyring` with a `type` property.\n *\n * @param KeyringConstructor - The Keyring class for the builder.\n * @returns A builder function for the given Keyring.\n */\nexport function keyringBuilderFactory(KeyringConstructor: KeyringClass) {\n const builder = () => new KeyringConstructor();\n\n builder.type = KeyringConstructor.type;\n\n return builder;\n}\n\nconst defaultKeyringBuilders = [\n keyringBuilderFactory(SimpleKeyring),\n keyringBuilderFactory(HDKeyring),\n];\n\nexport const getDefaultKeyringState = (): KeyringControllerState => {\n return {\n isUnlocked: false,\n keyrings: [],\n };\n};\n\n/**\n * Assert that the given keyring has an exportable\n * mnemonic.\n *\n * @param keyring - The keyring to check\n * @throws When the keyring does not have a mnemonic\n */\nfunction assertHasUint8ArrayMnemonic(\n keyring: EthKeyring,\n): asserts keyring is EthKeyring & { mnemonic: Uint8Array } {\n if (\n !(\n hasProperty(keyring, 'mnemonic') && keyring.mnemonic instanceof Uint8Array\n )\n ) {\n throw new Error(\"Can't get mnemonic bytes from keyring\");\n }\n}\n\n/**\n * Assert that the provided encryptor supports\n * encryption and encryption key export.\n *\n * @param encryptor - The encryptor to check.\n * @throws If the encryptor does not support key encryption.\n */\nfunction assertIsExportableKeyEncryptor(\n encryptor: GenericEncryptor | ExportableKeyEncryptor,\n): asserts encryptor is ExportableKeyEncryptor {\n if (\n !(\n 'importKey' in encryptor &&\n typeof encryptor.importKey === 'function' &&\n 'decryptWithKey' in encryptor &&\n typeof encryptor.decryptWithKey === 'function' &&\n 'encryptWithKey' in encryptor &&\n typeof encryptor.encryptWithKey === 'function'\n )\n ) {\n throw new Error(KeyringControllerError.UnsupportedEncryptionKeyExport);\n }\n}\n\n/**\n * Assert that the provided password is a valid non-empty string.\n *\n * @param password - The password to check.\n * @throws If the password is not a valid string.\n */\nfunction assertIsValidPassword(password: unknown): asserts password is string {\n if (typeof password !== 'string') {\n throw new Error(KeyringControllerError.WrongPasswordType);\n }\n\n if (!password || !password.length) {\n throw new Error(KeyringControllerError.InvalidEmptyPassword);\n }\n}\n\n/**\n * Checks if the provided value is a serialized keyrings array.\n *\n * @param array - The value to check.\n * @returns True if the value is a serialized keyrings array.\n */\nfunction isSerializedKeyringsArray(\n array: unknown,\n): array is SerializedKeyring[] {\n return (\n typeof array === 'object' &&\n Array.isArray(array) &&\n array.every((value) => value.type && isValidJson(value.data))\n );\n}\n\n/**\n * Display For Keyring\n *\n * Is used for adding the current keyrings to the state object.\n *\n * @param keyring - The keyring to display.\n * @returns A keyring display object, with type and accounts properties.\n */\nasync function displayForKeyring(\n keyring: EthKeyring,\n): Promise<{ type: string; accounts: string[] }> {\n const accounts = await keyring.getAccounts();\n\n return {\n type: keyring.type,\n // Cast to `string[]` here is safe here because `accounts` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n accounts: accounts.map(normalize) as string[],\n };\n}\n\n/**\n * Check if address is an ethereum address\n *\n * @param address - An address.\n * @returns Returns true if the address is an ethereum one, false otherwise.\n */\nfunction isEthAddress(address: string): boolean {\n // We first check if it's a matching `Hex` string, so that is narrows down\n // `address` as an `Hex` type, allowing us to use `isValidHexAddress`\n return (\n // NOTE: This function only checks for lowercased strings\n isStrictHexString(address.toLowerCase()) &&\n // This checks for lowercased addresses and checksum addresses too\n isValidHexAddress(address as Hex)\n );\n}\n\n/**\n * Normalize ethereum or non-EVM address.\n *\n * @param address - Ethereum or non-EVM address.\n * @returns The normalized address.\n */\nfunction normalize(address: string): string | undefined {\n // Since the `KeyringController` is only dealing with address, we have\n // no other way to get the associated account type with this address. So we\n // are down to check the actual address format for now\n // TODO: Find a better way to not have those runtime checks based on the\n // address value!\n return isEthAddress(address) ? ethNormalize(address) : address;\n}\n\n/**\n * Controller responsible for establishing and managing user identity.\n *\n * This class is a wrapper around the `eth-keyring-controller` package. The\n * `eth-keyring-controller` manages the \"vault\", which is an encrypted store of private keys, and\n * it manages the wallet \"lock\" state. This wrapper class has convenience methods for interacting\n * with the internal keyring controller and handling certain complex operations that involve the\n * keyrings.\n */\nexport class KeyringController extends BaseController<\n typeof name,\n KeyringControllerState,\n KeyringControllerMessenger\n> {\n readonly #controllerOperationMutex = new Mutex();\n\n readonly #vaultOperationMutex = new Mutex();\n\n #keyringBuilders: { (): EthKeyring; type: string }[];\n\n #keyrings: EthKeyring[];\n\n #unsupportedKeyrings: SerializedKeyring[];\n\n #password?: string;\n\n #encryptor: GenericEncryptor | ExportableKeyEncryptor;\n\n #cacheEncryptionKey: boolean;\n\n #qrKeyringStateListener?: (\n state: ReturnType,\n ) => void;\n\n /**\n * Creates a KeyringController instance.\n *\n * @param options - Initial options used to configure this controller\n * @param options.encryptor - An optional object for defining encryption schemes.\n * @param options.keyringBuilders - Set a new name for account.\n * @param options.cacheEncryptionKey - Whether to cache or not encryption key.\n * @param options.messenger - A restricted controller messenger.\n * @param options.state - Initial state to set on this controller.\n */\n constructor(options: KeyringControllerOptions) {\n const {\n encryptor = encryptorUtils,\n keyringBuilders,\n messenger,\n state,\n } = options;\n\n super({\n name,\n metadata: {\n vault: { persist: true, anonymous: false },\n isUnlocked: { persist: false, anonymous: true },\n keyrings: { persist: false, anonymous: false },\n encryptionKey: { persist: false, anonymous: false },\n encryptionSalt: { persist: false, anonymous: false },\n },\n messenger,\n state: {\n ...getDefaultKeyringState(),\n ...state,\n },\n });\n\n this.#keyringBuilders = keyringBuilders\n ? defaultKeyringBuilders.concat(keyringBuilders)\n : defaultKeyringBuilders;\n\n this.#encryptor = encryptor;\n this.#keyrings = [];\n this.#unsupportedKeyrings = [];\n\n // This option allows the controller to cache an exported key\n // for use in decrypting and encrypting data without password\n this.#cacheEncryptionKey = Boolean(options.cacheEncryptionKey);\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(encryptor);\n }\n\n this.#registerMessageHandlers();\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring.\n *\n * @param accountCount - Number of accounts before adding a new one, used to\n * make the method idempotent.\n * @returns Promise resolving to the added account address.\n */\n async addNewAccount(accountCount?: number): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const oldAccounts = await primaryKeyring.getAccounts();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n // we return the account already existing at index `accountCount`\n const existingAccount = oldAccounts[accountCount];\n\n if (!existingAccount) {\n throw new Error(`Can't find account at index ${accountCount}`);\n }\n\n return existingAccount;\n }\n\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the specified keyring.\n *\n * @param keyring - Keyring to add the account to.\n * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent.\n * @returns Promise resolving to the added account address\n */\n async addNewAccountForKeyring(\n keyring: EthKeyring,\n accountCount?: number,\n ): Promise {\n // READ THIS CAREFULLY:\n // We still uses `Hex` here, since we are not using this method when creating\n // and account using a \"Snap Keyring\". This function assume the `keyring` is\n // ethereum compatible, but \"Snap Keyring\" might not be.\n return this.#persistOrRollback(async () => {\n const oldAccounts = await this.#getAccountsFromKeyrings();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n\n const existingAccount = oldAccounts[accountCount];\n assertIsStrictHexString(existingAccount);\n\n return existingAccount;\n }\n\n await keyring.addAccounts(1);\n\n const addedAccountAddress = (await this.#getAccountsFromKeyrings()).find(\n (selectedAddress) => !oldAccounts.includes(selectedAddress),\n );\n assertIsStrictHexString(addedAccountAddress);\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences.\n *\n * @returns Promise resolving to the added account address.\n */\n async addNewAccountWithoutUpdate(): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n return addedAccountAddress;\n });\n }\n\n /**\n * Effectively the same as creating a new keychain then populating it\n * using the given seed phrase.\n *\n * @param password - Password to unlock keychain.\n * @param seed - A BIP39-compliant seed phrase as Uint8Array,\n * either as a string or an array of UTF-8 bytes that represent the string.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndRestore(\n password: string,\n seed: Uint8Array,\n ): Promise {\n return this.#persistOrRollback(async () => {\n assertIsValidPassword(password);\n\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n opts: {\n mnemonic: seed,\n numberOfAccounts: 1,\n },\n });\n });\n }\n\n /**\n * Create a new primary keychain and wipe any previous keychains.\n *\n * @param password - Password to unlock the new vault.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndKeychain(password: string) {\n return this.#persistOrRollback(async () => {\n const accounts = await this.#getAccountsFromKeyrings();\n if (!accounts.length) {\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n });\n }\n });\n }\n\n /**\n * Adds a new keyring of the given `type`.\n *\n * @param type - Keyring type name.\n * @param opts - Keyring options.\n * @throws If a builder for the given `type` does not exist.\n * @returns Promise resolving to the added keyring.\n */\n async addNewKeyring(\n type: KeyringTypes | string,\n opts?: unknown,\n ): Promise {\n if (type === KeyringTypes.qr) {\n return this.getOrAddQRKeyring();\n }\n\n return this.#persistOrRollback(async () => this.#newKeyring(type, opts));\n }\n\n /**\n * Method to verify a given password validity. Throws an\n * error if the password is invalid.\n *\n * @param password - Password of the keyring.\n */\n async verifyPassword(password: string) {\n if (!this.state.vault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n await this.#encryptor.decrypt(password, this.state.vault);\n }\n\n /**\n * Returns the status of the vault.\n *\n * @returns Boolean returning true if the vault is unlocked.\n */\n isUnlocked(): boolean {\n return this.state.isUnlocked;\n }\n\n /**\n * Gets the seed phrase of the HD keyring.\n *\n * @param password - Password of the keyring.\n * @returns Promise resolving to the seed phrase.\n */\n async exportSeedPhrase(password: string): Promise {\n await this.verifyPassword(password);\n assertHasUint8ArrayMnemonic(this.#keyrings[0]);\n return this.#keyrings[0].mnemonic;\n }\n\n /**\n * Gets the private key from the keyring controlling an address.\n *\n * @param password - Password of the keyring.\n * @param address - Address to export.\n * @returns Promise resolving to the private key for an address.\n */\n async exportAccount(password: string, address: string): Promise {\n await this.verifyPassword(password);\n\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.exportAccount) {\n throw new Error(KeyringControllerError.UnsupportedExportAccount);\n }\n\n return await keyring.exportAccount(normalize(address) as Hex);\n }\n\n /**\n * Returns the public addresses of all accounts from every keyring.\n *\n * @returns A promise resolving to an array of addresses.\n */\n async getAccounts(): Promise {\n return this.state.keyrings.reduce(\n (accounts, keyring) => accounts.concat(keyring.accounts),\n [],\n );\n }\n\n /**\n * Get encryption public key.\n *\n * @param account - An account address.\n * @param opts - Additional encryption options.\n * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method\n * @returns Promise resolving to encyption public key of the `account` if one exists.\n */\n async getEncryptionPublicKey(\n account: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(account) as Hex;\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n if (!keyring.getEncryptionPublicKey) {\n throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey);\n }\n\n return await keyring.getEncryptionPublicKey(address, opts);\n }\n\n /**\n * Attempts to decrypt the provided message parameters.\n *\n * @param messageParams - The decryption message parameters.\n * @param messageParams.from - The address of the account you want to use to decrypt the message.\n * @param messageParams.data - The encrypted data that you want to decrypt.\n * @returns The raw decryption result.\n */\n async decryptMessage(messageParams: {\n from: string;\n data: Eip1024EncryptedData;\n }): Promise {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.decryptMessage) {\n throw new Error(KeyringControllerError.UnsupportedDecryptMessage);\n }\n\n return keyring.decryptMessage(address, messageParams.data);\n }\n\n /**\n * Returns the currently initialized keyring that manages\n * the specified `address` if one exists.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param account - An account address.\n * @returns Promise resolving to keyring of the `account` if one exists.\n */\n async getKeyringForAccount(account: string): Promise {\n const address = normalize(account);\n\n const candidates = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n return Promise.all([keyring, keyring.getAccounts()]);\n }),\n );\n\n const winners = candidates.filter((candidate) => {\n const accounts = candidate[1].map(normalize);\n return accounts.includes(address);\n });\n\n if (winners.length && winners[0]?.length) {\n return winners[0][0];\n }\n\n // Adding more info to the error\n let errorInfo = '';\n if (!candidates.length) {\n errorInfo = 'There are no keyrings';\n } else if (!winners.length) {\n errorInfo = 'There are keyrings, but none match the address';\n }\n throw new Error(\n `${KeyringControllerError.NoKeyring}. Error info: ${errorInfo}`,\n );\n }\n\n /**\n * Returns all keyrings of the given type.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param type - Keyring type name.\n * @returns An array of keyrings of the given type.\n */\n getKeyringsByType(type: KeyringTypes | string): unknown[] {\n return this.#keyrings.filter((keyring) => keyring.type === type);\n }\n\n /**\n * Persist all serialized keyrings in the vault.\n *\n * @deprecated This method is being phased out in favor of `withKeyring`.\n * @returns Promise resolving with `true` value when the\n * operation completes.\n */\n async persistAllKeyrings(): Promise {\n return this.#persistOrRollback(async () => true);\n }\n\n /**\n * Imports an account with the specified import strategy.\n *\n * @param strategy - Import strategy name.\n * @param args - Array of arguments to pass to the underlying stategy.\n * @throws Will throw when passed an unrecognized strategy.\n * @returns Promise resolving to the imported account address.\n */\n async importAccountWithStrategy(\n strategy: AccountImportStrategy,\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[],\n ): Promise {\n return this.#persistOrRollback(async () => {\n let privateKey;\n switch (strategy) {\n case 'privateKey':\n const [importedKey] = args;\n if (!importedKey) {\n throw new Error('Cannot import an empty key.');\n }\n const prefixed = add0x(importedKey);\n\n let bufferedPrivateKey;\n try {\n bufferedPrivateKey = toBuffer(prefixed);\n } catch {\n throw new Error('Cannot import invalid private key.');\n }\n\n if (\n !isValidPrivate(bufferedPrivateKey) ||\n // ensures that the key is 64 bytes long\n getBinarySize(prefixed) !== 64 + '0x'.length\n ) {\n throw new Error('Cannot import invalid private key.');\n }\n\n privateKey = remove0x(prefixed);\n break;\n case 'json':\n let wallet;\n const [input, password] = args;\n try {\n wallet = importers.fromEtherWallet(input, password);\n } catch (e) {\n wallet = wallet || (await Wallet.fromV3(input, password, true));\n }\n privateKey = bytesToHex(wallet.getPrivateKey());\n break;\n default:\n throw new Error(`Unexpected import strategy: '${strategy}'`);\n }\n const newKeyring = (await this.#newKeyring(KeyringTypes.simple, [\n privateKey,\n ])) as EthKeyring;\n const accounts = await newKeyring.getAccounts();\n return accounts[0];\n });\n }\n\n /**\n * Removes an account from keyring state.\n *\n * @param address - Address of the account to remove.\n * @fires KeyringController:accountRemoved\n * @returns Promise resolving when the account is removed.\n */\n async removeAccount(address: string): Promise {\n await this.#persistOrRollback(async () => {\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n // Not all the keyrings support this, so we have to check\n if (!keyring.removeAccount) {\n throw new Error(KeyringControllerError.UnsupportedRemoveAccount);\n }\n\n // The `removeAccount` method of snaps keyring is async. We have to update\n // the interface of the other keyrings to be async as well.\n // eslint-disable-next-line @typescript-eslint/await-thenable\n // FIXME: We do cast to `Hex` to makes the type checker happy here, and\n // because `Keyring.removeAccount` requires address to be `Hex`. Those\n // type would need to be updated for a full non-EVM support.\n await keyring.removeAccount(address as Hex);\n\n const accounts = await keyring.getAccounts();\n // Check if this was the last/only account\n if (accounts.length === 0) {\n await this.#removeEmptyKeyrings();\n }\n });\n\n this.messagingSystem.publish(`${name}:accountRemoved`, address);\n }\n\n /**\n * Deallocates all secrets and locks the wallet.\n *\n * @returns Promise resolving when the operation completes.\n */\n async setLocked(): Promise {\n return this.#withRollback(async () => {\n this.#unsubscribeFromQRKeyringsEvents();\n\n this.#password = undefined;\n await this.#clearKeyrings();\n\n this.update((state) => {\n state.isUnlocked = false;\n state.keyrings = [];\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n\n this.messagingSystem.publish(`${name}:lock`);\n });\n }\n\n /**\n * Signs message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signMessage(messageParams: PersonalMessageParams): Promise {\n if (!messageParams.data) {\n throw new Error(\"Can't sign an empty message\");\n }\n\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignMessage);\n }\n\n return await keyring.signMessage(address, messageParams.data);\n }\n\n /**\n * Signs personal message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signPersonalMessage(messageParams: PersonalMessageParams) {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signPersonalMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignPersonalMessage);\n }\n\n const normalizedData = normalize(messageParams.data) as Hex;\n\n return await keyring.signPersonalMessage(address, normalizedData);\n }\n\n /**\n * Signs typed message by calling down into a specific keyring.\n *\n * @param messageParams - TypedMessageParams object to sign.\n * @param version - Compatibility version EIP712.\n * @throws Will throw when passed an unrecognized version.\n * @returns Promise resolving to a signed message string or an error if any.\n */\n async signTypedMessage(\n messageParams: TypedMessageParams,\n version: SignTypedDataVersion,\n ): Promise {\n try {\n if (\n ![\n SignTypedDataVersion.V1,\n SignTypedDataVersion.V3,\n SignTypedDataVersion.V4,\n ].includes(version)\n ) {\n throw new Error(`Unexpected signTypedMessage version: '${version}'`);\n }\n\n // Cast to `Hex` here is safe here because `messageParams.from` is not nullish.\n // `normalize` returns `Hex` unless given a nullish value.\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTypedData) {\n throw new Error(KeyringControllerError.UnsupportedSignTypedMessage);\n }\n\n return await keyring.signTypedData(\n address,\n version !== SignTypedDataVersion.V1 &&\n typeof messageParams.data === 'string'\n ? JSON.parse(messageParams.data)\n : messageParams.data,\n { version },\n );\n } catch (error) {\n throw new Error(`Keyring Controller signTypedMessage: ${error}`);\n }\n }\n\n /**\n * Signs a transaction by calling down into a specific keyring.\n *\n * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance.\n * @param from - Address to sign from, should be in keychain.\n * @param opts - An optional options object.\n * @returns Promise resolving to a signed transaction string.\n */\n async signTransaction(\n transaction: TypedTransaction,\n from: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTransaction) {\n throw new Error(KeyringControllerError.UnsupportedSignTransaction);\n }\n\n return await keyring.signTransaction(address, transaction, opts);\n }\n\n /**\n * Convert a base transaction to a base UserOperation.\n *\n * @param from - Address of the sender.\n * @param transactions - Base transactions to include in the UserOperation.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A pseudo-UserOperation that can be used to construct a real.\n */\n async prepareUserOperation(\n from: string,\n transactions: EthBaseTransaction[],\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.prepareUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPrepareUserOperation);\n }\n\n return await keyring.prepareUserOperation(\n address,\n transactions,\n executionContext,\n );\n }\n\n /**\n * Patches properties of a UserOperation. Currently, only the\n * `paymasterAndData` can be patched.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to patch.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A patch to apply to the UserOperation.\n */\n async patchUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.patchUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPatchUserOperation);\n }\n\n return await keyring.patchUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Signs an UserOperation.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to sign.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns The signature of the UserOperation.\n */\n async signUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.signUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedSignUserOperation);\n }\n\n return await keyring.signUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Changes the password used to encrypt the vault.\n *\n * @param password - The new password.\n * @returns Promise resolving when the operation completes.\n */\n changePassword(password: string): Promise {\n return this.#persistOrRollback(async () => {\n if (!this.state.isUnlocked) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n assertIsValidPassword(password);\n\n this.#password = password;\n // We need to clear encryption key and salt from state\n // to force the controller to re-encrypt the vault using\n // the new password.\n if (this.#cacheEncryptionKey) {\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n }\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given encryption key and salt.\n *\n * @param encryptionKey - Key to unlock the keychain.\n * @param encryptionSalt - Salt to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitEncryptionKey(\n encryptionKey: string,\n encryptionSalt: string,\n ): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(\n undefined,\n encryptionKey,\n encryptionSalt,\n );\n this.#setUnlocked();\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given password.\n *\n * @param password - Password to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitPassword(password: string): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(password);\n this.#setUnlocked();\n });\n }\n\n /**\n * Verifies the that the seed phrase restores the current keychain's accounts.\n *\n * @returns Promise resolving to the seed phrase as Uint8Array.\n */\n async verifySeedPhrase(): Promise {\n const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found.');\n }\n\n assertHasUint8ArrayMnemonic(primaryKeyring);\n\n const seedWords = primaryKeyring.mnemonic;\n const accounts = await primaryKeyring.getAccounts();\n /* istanbul ignore if */\n if (accounts.length === 0) {\n throw new Error('Cannot verify an empty keyring.');\n }\n\n // The HD Keyring Builder is a default keyring builder\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!;\n\n const hdKeyring = hdKeyringBuilder();\n // @ts-expect-error @metamask/eth-hd-keyring correctly handles\n // Uint8Array seed phrases in the `deserialize` method.\n await hdKeyring.deserialize({\n mnemonic: seedWords,\n numberOfAccounts: accounts.length,\n });\n const testAccounts = await hdKeyring.getAccounts();\n /* istanbul ignore if */\n if (testAccounts.length !== accounts.length) {\n throw new Error('Seed phrase imported incorrect number of accounts.');\n }\n\n testAccounts.forEach((account: string, i: number) => {\n /* istanbul ignore if */\n if (account.toLowerCase() !== accounts[i].toLowerCase()) {\n throw new Error('Seed phrase imported different accounts.');\n }\n });\n\n return seedWords;\n }\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @param options - Additional options.\n * @param options.createIfMissing - Whether to create a new keyring if the selected one is missing.\n * @param options.createWithData - Optional data to use when creating a new keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n * @deprecated This method overload is deprecated. Use `withKeyring` without options instead.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n // eslint-disable-next-line @typescript-eslint/unified-signatures\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown },\n ): Promise;\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n ): Promise;\n\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown } = {\n createIfMissing: false,\n },\n ): Promise {\n return this.#persistOrRollback(async () => {\n let keyring: SelectedKeyring | undefined;\n\n if ('address' in selector) {\n keyring = (await this.getKeyringForAccount(selector.address)) as\n | SelectedKeyring\n | undefined;\n } else {\n keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as\n | SelectedKeyring\n | undefined;\n\n if (!keyring && options.createIfMissing) {\n keyring = (await this.#newKeyring(\n selector.type,\n options.createWithData,\n )) as SelectedKeyring;\n }\n }\n\n if (!keyring) {\n throw new Error(KeyringControllerError.KeyringNotFound);\n }\n\n const result = await operation(keyring);\n\n if (Object.is(result, keyring)) {\n // Access to a keyring instance outside of controller safeguards\n // should be discouraged, as it can lead to unexpected behavior.\n // This error is thrown to prevent consumers using `withKeyring`\n // as a way to get a reference to a keyring instance.\n throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess);\n }\n\n return result;\n });\n }\n\n // QR Hardware related methods\n\n /**\n * Get QR Hardware keyring.\n *\n * @returns The QR Keyring if defined, otherwise undefined\n */\n getQRKeyring(): QRKeyring | undefined {\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return this.getKeyringsByType(KeyringTypes.qr)[0] as unknown as QRKeyring;\n }\n\n /**\n * Get QR hardware keyring. If it doesn't exist, add it.\n *\n * @returns The added keyring\n */\n async getOrAddQRKeyring(): Promise {\n return (\n this.getQRKeyring() ||\n (await this.#persistOrRollback(async () => this.#addQRKeyring()))\n );\n }\n\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async restoreQRKeyring(serialized: any): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n keyring.deserialize(serialized);\n });\n }\n\n async resetQRKeyringState(): Promise {\n (await this.getOrAddQRKeyring()).resetStore();\n }\n\n async getQRKeyringState(): Promise {\n return (await this.getOrAddQRKeyring()).getMemStore();\n }\n\n async submitQRCryptoHDKey(cryptoHDKey: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey);\n }\n\n async submitQRCryptoAccount(cryptoAccount: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount);\n }\n\n async submitQRSignature(\n requestId: string,\n ethSignature: string,\n ): Promise {\n (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature);\n }\n\n async cancelQRSignRequest(): Promise {\n (await this.getOrAddQRKeyring()).cancelSignRequest();\n }\n\n /**\n * Cancels qr keyring sync.\n */\n async cancelQRSynchronization(): Promise {\n // eslint-disable-next-line n/no-sync\n (await this.getOrAddQRKeyring()).cancelSync();\n }\n\n async connectQRHardware(\n page: number,\n ): Promise<{ balance: string; address: string; index: number }[]> {\n return this.#persistOrRollback(async () => {\n try {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n let accounts;\n switch (page) {\n case -1:\n accounts = await keyring.getPreviousPage();\n break;\n case 1:\n accounts = await keyring.getNextPage();\n break;\n default:\n accounts = await keyring.getFirstPage();\n }\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return accounts.map((account: any) => {\n return {\n ...account,\n balance: '0x0',\n };\n });\n } catch (e) {\n // TODO: Add test case for when keyring throws\n /* istanbul ignore next */\n throw new Error(`Unspecified error when connect QR Hardware, ${e}`);\n }\n });\n }\n\n async unlockQRHardwareWalletAccount(index: number): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n\n keyring.setAccountToUnlock(index);\n await keyring.addAccounts(1);\n });\n }\n\n async getAccountKeyringType(account: string): Promise {\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n return keyring.type;\n }\n\n async forgetQRDevice(): Promise<{\n removedAccounts: string[];\n remainingAccounts: string[];\n }> {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring();\n\n if (!keyring) {\n return { removedAccounts: [], remainingAccounts: [] };\n }\n\n const allAccounts = (await this.#getAccountsFromKeyrings()) as string[];\n keyring.forgetDevice();\n const remainingAccounts =\n (await this.#getAccountsFromKeyrings()) as string[];\n const removedAccounts = allAccounts.filter(\n (address: string) => !remainingAccounts.includes(address),\n );\n return { removedAccounts, remainingAccounts };\n });\n }\n\n /**\n * Constructor helper for registering this controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messagingSystem.registerActionHandler(\n `${name}:signMessage`,\n this.signMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signPersonalMessage`,\n this.signPersonalMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signTypedMessage`,\n this.signTypedMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:decryptMessage`,\n this.decryptMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getEncryptionPublicKey`,\n this.getEncryptionPublicKey.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getAccounts`,\n this.getAccounts.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringsByType`,\n this.getKeyringsByType.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringForAccount`,\n this.getKeyringForAccount.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:persistAllKeyrings`,\n this.persistAllKeyrings.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:prepareUserOperation`,\n this.prepareUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:patchUserOperation`,\n this.patchUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signUserOperation`,\n this.signUserOperation.bind(this),\n );\n }\n\n /**\n * Get the keyring builder for the given `type`.\n *\n * @param type - The type of keyring to get the builder for.\n * @returns The keyring builder, or undefined if none exists.\n */\n #getKeyringBuilderForType(\n type: string,\n ): { (): EthKeyring; type: string } | undefined {\n return this.#keyringBuilders.find(\n (keyringBuilder) => keyringBuilder.type === type,\n );\n }\n\n /**\n * Add qr hardware keyring.\n *\n * @returns The added keyring\n * @throws If a QRKeyring builder is not provided\n * when initializing the controller\n */\n async #addQRKeyring(): Promise {\n this.#assertControllerMutexIsLocked();\n\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return (await this.#newKeyring(KeyringTypes.qr)) as unknown as QRKeyring;\n }\n\n /**\n * Subscribe to a QRKeyring state change events and\n * forward them through the messaging system.\n *\n * @param qrKeyring - The QRKeyring instance to subscribe to\n */\n #subscribeToQRKeyringEvents(qrKeyring: QRKeyring) {\n this.#qrKeyringStateListener = (state) => {\n this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state);\n };\n\n qrKeyring.getMemStore().subscribe(this.#qrKeyringStateListener);\n }\n\n #unsubscribeFromQRKeyringsEvents() {\n const qrKeyrings = this.getKeyringsByType(\n KeyringTypes.qr,\n ) as unknown as QRKeyring[];\n\n qrKeyrings.forEach((qrKeyring) => {\n if (this.#qrKeyringStateListener) {\n qrKeyring.getMemStore().unsubscribe(this.#qrKeyringStateListener);\n }\n });\n }\n\n /**\n * Create new vault with an initial keyring\n *\n * Destroys any old encrypted storage,\n * creates a new encrypted store with the given password,\n * creates a new wallet with 1 account.\n *\n * @fires KeyringController:unlock\n * @param password - The password to encrypt the vault with.\n * @param keyring - A object containing the params to instantiate a new keyring.\n * @param keyring.type - The keyring type.\n * @param keyring.opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves to the state.\n */\n async #createNewVaultWithKeyring(\n password: string,\n keyring: {\n type: string;\n opts?: unknown;\n },\n ): Promise {\n this.#assertControllerMutexIsLocked();\n\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n\n this.#password = password;\n\n await this.#clearKeyrings();\n await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);\n this.#setUnlocked();\n }\n\n /**\n * Get the updated array of each keyring's type and\n * accounts list.\n *\n * @returns A promise resolving to the updated keyrings array.\n */\n async #getUpdatedKeyrings(): Promise {\n return Promise.all(this.#keyrings.map(displayForKeyring));\n }\n\n /**\n * Serialize the current array of keyring instances,\n * including unsupported keyrings by default.\n *\n * @param options - Method options.\n * @param options.includeUnsupported - Whether to include unsupported keyrings.\n * @returns The serialized keyrings.\n */\n async #getSerializedKeyrings(\n { includeUnsupported }: { includeUnsupported: boolean } = {\n includeUnsupported: true,\n },\n ): Promise {\n const serializedKeyrings = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n const [type, data] = await Promise.all([\n keyring.type,\n keyring.serialize(),\n ]);\n return { type, data };\n }),\n );\n\n if (includeUnsupported) {\n serializedKeyrings.push(...this.#unsupportedKeyrings);\n }\n\n return serializedKeyrings;\n }\n\n /**\n * Restore a serialized keyrings array.\n *\n * @param serializedKeyrings - The serialized keyrings array.\n */\n async #restoreSerializedKeyrings(\n serializedKeyrings: SerializedKeyring[],\n ): Promise {\n await this.#clearKeyrings();\n\n for (const serializedKeyring of serializedKeyrings) {\n await this.#restoreKeyring(serializedKeyring);\n }\n }\n\n /**\n * Unlock Keyrings, decrypting the vault and deserializing all\n * keyrings contained in it, using a password or an encryption key with salt.\n *\n * @param password - The keyring controller password.\n * @param encryptionKey - An exported key string to unlock keyrings with.\n * @param encryptionSalt - The salt used to encrypt the vault.\n * @returns A promise resolving to the deserialized keyrings array.\n */\n async #unlockKeyrings(\n password: string | undefined,\n encryptionKey?: string,\n encryptionSalt?: string,\n ): Promise[]> {\n return this.#withVaultLock(async ({ releaseLock }) => {\n const encryptedVault = this.state.vault;\n if (!encryptedVault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n\n let vault;\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (password) {\n const result = await this.#encryptor.decryptWithDetail(\n password,\n encryptedVault,\n );\n vault = result.vault;\n this.#password = password;\n\n updatedState.encryptionKey = result.exportedKeyString;\n updatedState.encryptionSalt = result.salt;\n } else {\n const parsedEncryptedVault = JSON.parse(encryptedVault);\n\n if (encryptionSalt !== parsedEncryptedVault.salt) {\n throw new Error(KeyringControllerError.ExpiredCredentials);\n }\n\n if (typeof encryptionKey !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n const key = await this.#encryptor.importKey(encryptionKey);\n vault = await this.#encryptor.decryptWithKey(\n key,\n parsedEncryptedVault,\n );\n\n // This call is required on the first call because encryptionKey\n // is not yet inside the memStore\n updatedState.encryptionKey = encryptionKey;\n // we can safely assume that encryptionSalt is defined here\n // because we compare it with the salt from the vault\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n updatedState.encryptionSalt = encryptionSalt!;\n }\n } else {\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n vault = await this.#encryptor.decrypt(password, encryptedVault);\n this.#password = password;\n }\n\n if (!isSerializedKeyringsArray(vault)) {\n throw new Error(KeyringControllerError.VaultDataError);\n }\n\n await this.#restoreSerializedKeyrings(vault);\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n\n this.update((state) => {\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey || updatedState.encryptionSalt) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = updatedState.encryptionSalt;\n }\n });\n\n if (\n this.#password &&\n (!this.#cacheEncryptionKey || !encryptionKey) &&\n this.#encryptor.isVaultUpdated &&\n !this.#encryptor.isVaultUpdated(encryptedVault)\n ) {\n // The lock needs to be released before persisting the keyrings\n // to avoid deadlock\n releaseLock();\n // Re-encrypt the vault with safer method if one is available\n await this.#updateVault();\n }\n\n return this.#keyrings;\n });\n }\n\n /**\n * Update the vault with the current keyrings.\n *\n * @returns A promise resolving to `true` if the operation is successful.\n */\n #updateVault(): Promise {\n return this.#withVaultLock(async () => {\n const { encryptionKey, encryptionSalt } = this.state;\n\n if (!this.#password && !encryptionKey) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n const serializedKeyrings = await this.#getSerializedKeyrings();\n\n if (\n !serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)\n ) {\n throw new Error(KeyringControllerError.NoHdKeyring);\n }\n\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (encryptionKey) {\n const key = await this.#encryptor.importKey(encryptionKey);\n const vaultJSON = await this.#encryptor.encryptWithKey(\n key,\n serializedKeyrings,\n );\n vaultJSON.salt = encryptionSalt;\n updatedState.vault = JSON.stringify(vaultJSON);\n } else if (this.#password) {\n const { vault: newVault, exportedKeyString } =\n await this.#encryptor.encryptWithDetail(\n this.#password,\n serializedKeyrings,\n );\n\n updatedState.vault = newVault;\n updatedState.encryptionKey = exportedKeyString;\n }\n } else {\n assertIsValidPassword(this.#password);\n updatedState.vault = await this.#encryptor.encrypt(\n this.#password,\n serializedKeyrings,\n );\n }\n\n if (!updatedState.vault) {\n throw new Error(KeyringControllerError.MissingVaultData);\n }\n\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n this.update((state) => {\n state.vault = updatedState.vault;\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = JSON.parse(updatedState.vault as string).salt;\n }\n });\n\n return true;\n });\n }\n\n /**\n * Retrieves all the accounts from keyrings instances\n * that are currently in memory.\n *\n * @returns A promise resolving to an array of accounts.\n */\n async #getAccountsFromKeyrings(): Promise {\n const keyrings = this.#keyrings;\n\n const keyringArrays = await Promise.all(\n keyrings.map(async (keyring) => keyring.getAccounts()),\n );\n const addresses = keyringArrays.reduce((res, arr) => {\n return res.concat(arr);\n }, []);\n\n // Cast to `string[]` here is safe here because `addresses` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n return addresses.map(normalize) as string[];\n }\n\n /**\n * Create a new keyring, ensuring that the first account is\n * also created.\n *\n * @param type - Keyring type to instantiate.\n * @param opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves if the operation is successful.\n */\n async #createKeyringWithFirstAccount(type: string, opts?: unknown) {\n this.#assertControllerMutexIsLocked();\n\n const keyring = (await this.#newKeyring(type, opts)) as EthKeyring;\n\n const [firstAccount] = await keyring.getAccounts();\n if (!firstAccount) {\n throw new Error(KeyringControllerError.NoFirstAccount);\n }\n }\n\n /**\n * Instantiate, initialize and return a new keyring of the given `type`,\n * using the given `opts`. The keyring is built using the keyring builder\n * registered for the given `type`.\n *\n *\n * @param type - The type of keyring to add.\n * @param data - The data to restore a previously serialized keyring.\n * @returns The new keyring.\n * @throws If the keyring includes duplicated accounts.\n */\n async #newKeyring(type: string, data?: unknown): Promise> {\n this.#assertControllerMutexIsLocked();\n\n const keyringBuilder = this.#getKeyringBuilderForType(type);\n\n if (!keyringBuilder) {\n throw new Error(\n `${KeyringControllerError.NoKeyringBuilder}. Keyring type: ${type}`,\n );\n }\n\n const keyring = keyringBuilder();\n\n // @ts-expect-error Enforce data type after updating clients\n await keyring.deserialize(data);\n\n if (keyring.init) {\n await keyring.init();\n }\n\n if (type === KeyringTypes.hd && (!isObject(data) || !data.mnemonic)) {\n if (!keyring.generateRandomMnemonic) {\n throw new Error(\n KeyringControllerError.UnsupportedGenerateRandomMnemonic,\n );\n }\n\n keyring.generateRandomMnemonic();\n await keyring.addAccounts(1);\n }\n\n await this.#checkForDuplicate(type, await keyring.getAccounts());\n\n if (type === KeyringTypes.qr) {\n // In case of a QR keyring type, we need to subscribe\n // to its events after creating it\n this.#subscribeToQRKeyringEvents(keyring as unknown as QRKeyring);\n }\n\n this.#keyrings.push(keyring);\n\n return keyring;\n }\n\n /**\n * Remove all managed keyrings, destroying all their\n * instances in memory.\n */\n async #clearKeyrings() {\n this.#assertControllerMutexIsLocked();\n for (const keyring of this.#keyrings) {\n await this.#destroyKeyring(keyring);\n }\n this.#keyrings = [];\n }\n\n /**\n * Restore a Keyring from a provided serialized payload.\n * On success, returns the resulting keyring instance.\n *\n * @param serialized - The serialized keyring.\n * @returns The deserialized keyring or undefined if the keyring type is unsupported.\n */\n async #restoreKeyring(\n serialized: SerializedKeyring,\n ): Promise | undefined> {\n this.#assertControllerMutexIsLocked();\n\n try {\n const { type, data } = serialized;\n return await this.#newKeyring(type, data);\n } catch (_) {\n this.#unsupportedKeyrings.push(serialized);\n return undefined;\n }\n }\n\n /**\n * Destroy Keyring\n *\n * Some keyrings support a method called `destroy`, that destroys the\n * keyring along with removing all its event listeners and, in some cases,\n * clears the keyring bridge iframe from the DOM.\n *\n * @param keyring - The keyring to destroy.\n */\n async #destroyKeyring(keyring: EthKeyring) {\n await keyring.destroy?.();\n }\n\n /**\n * Remove empty keyrings.\n *\n * Loops through the keyrings and removes the ones with empty accounts\n * (usually after removing the last / only account) from a keyring.\n */\n async #removeEmptyKeyrings(): Promise {\n this.#assertControllerMutexIsLocked();\n const validKeyrings: EthKeyring[] = [];\n\n // Since getAccounts returns a Promise\n // We need to wait to hear back form each keyring\n // in order to decide which ones are now valid (accounts.length > 0)\n\n await Promise.all(\n this.#keyrings.map(async (keyring: EthKeyring) => {\n const accounts = await keyring.getAccounts();\n if (accounts.length > 0) {\n validKeyrings.push(keyring);\n } else {\n await this.#destroyKeyring(keyring);\n }\n }),\n );\n this.#keyrings = validKeyrings;\n }\n\n /**\n * Checks for duplicate keypairs, using the the first account in the given\n * array. Rejects if a duplicate is found.\n *\n * Only supports 'Simple Key Pair'.\n *\n * @param type - The key pair type to check for.\n * @param newAccountArray - Array of new accounts.\n * @returns The account, if no duplicate is found.\n */\n async #checkForDuplicate(\n type: string,\n newAccountArray: string[],\n ): Promise {\n const accounts = await this.#getAccountsFromKeyrings();\n\n switch (type) {\n case KeyringTypes.simple: {\n const isIncluded = Boolean(\n accounts.find(\n (key) =>\n newAccountArray[0] &&\n (key === newAccountArray[0] ||\n key === remove0x(newAccountArray[0])),\n ),\n );\n\n if (isIncluded) {\n throw new Error(KeyringControllerError.DuplicatedAccount);\n }\n return newAccountArray;\n }\n\n default: {\n return newAccountArray;\n }\n }\n }\n\n /**\n * Set the `isUnlocked` to true and notify listeners\n * through the messenger.\n *\n * @fires KeyringController:unlock\n */\n #setUnlocked(): void {\n this.#assertControllerMutexIsLocked();\n\n this.update((state) => {\n state.isUnlocked = true;\n });\n this.messagingSystem.publish(`${name}:unlock`);\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and save the keyrings to state after it, or rollback to their\n * previous state in case of error.\n *\n * @param fn - The function to execute.\n * @returns The result of the function.\n */\n async #persistOrRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withRollback(async ({ releaseLock }) => {\n const callbackResult = await fn({ releaseLock });\n // State is committed only if the operation is successful\n await this.#updateVault();\n\n return callbackResult;\n });\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and rollback keyrings and password states in case of error.\n *\n * @param fn - The function to execute atomically.\n * @returns The result of the function.\n */\n async #withRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withControllerLock(async ({ releaseLock }) => {\n const currentSerializedKeyrings = await this.#getSerializedKeyrings();\n const currentPassword = this.#password;\n\n try {\n return await fn({ releaseLock });\n } catch (e) {\n // Keyrings and password are restored to their previous state\n await this.#restoreSerializedKeyrings(currentSerializedKeyrings);\n this.#password = currentPassword;\n\n throw e;\n }\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(KeyringControllerError.ControllerLockRequired);\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param fn - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock(fn: MutuallyExclusiveCallback): Promise {\n return withLock(this.#controllerOperationMutex, fn);\n }\n\n /**\n * Lock the vault mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This ensures that each operation that interacts with the vault\n * is executed in a mutually exclusive way.\n *\n * @param fn - The function to execute while the vault mutex is locked.\n * @returns The result of the function.\n */\n async #withVaultLock(fn: MutuallyExclusiveCallback): Promise {\n this.#assertControllerMutexIsLocked();\n\n return withLock(this.#vaultOperationMutex, fn);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param fn - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock(\n mutex: Mutex,\n fn: MutuallyExclusiveCallback,\n): Promise {\n const releaseLock = await mutex.acquire();\n\n try {\n return await fn({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n\nexport default KeyringController;\n"],"mappings":"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","names":["KeyringTypes","AccountImportStrategy","SignTypedDataVersion"]} -\ No newline at end of file -diff --git a/dist/chunk-BRS27QHF.js b/dist/chunk-BRS27QHF.js -deleted file mode 100644 -index 93bf7c6e9b238aed269aabc193ede499d7efb76a..0000000000000000000000000000000000000000 ---- a/dist/chunk-BRS27QHF.js -+++ /dev/null -@@ -1,1500 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -- -- -- -- --var _chunkNOCGQCUMjs = require('./chunk-NOCGQCUM.js'); -- --// src/KeyringController.ts --var _util = require('@ethereumjs/util'); --var _basecontroller = require('@metamask/base-controller'); --var _browserpassworder = require('@metamask/browser-passworder'); var encryptorUtils = _interopRequireWildcard(_browserpassworder); --var _ethhdkeyring = require('@metamask/eth-hd-keyring'); var _ethhdkeyring2 = _interopRequireDefault(_ethhdkeyring); --var _ethsigutil = require('@metamask/eth-sig-util'); --var _ethsimplekeyring = require('@metamask/eth-simple-keyring'); var _ethsimplekeyring2 = _interopRequireDefault(_ethsimplekeyring); -- -- -- -- -- -- -- -- -- -- --var _utils = require('@metamask/utils'); --var _asyncmutex = require('async-mutex'); --var _ethereumjswallet = require('ethereumjs-wallet'); var _ethereumjswallet2 = _interopRequireDefault(_ethereumjswallet); --var name = "KeyringController"; --var KeyringTypes = /* @__PURE__ */ ((KeyringTypes2) => { -- KeyringTypes2["simple"] = "Simple Key Pair"; -- KeyringTypes2["hd"] = "HD Key Tree"; -- KeyringTypes2["qr"] = "QR Hardware Wallet Device"; -- KeyringTypes2["trezor"] = "Trezor Hardware"; -- KeyringTypes2["ledger"] = "Ledger Hardware"; -- KeyringTypes2["lattice"] = "Lattice Hardware"; -- KeyringTypes2["snap"] = "Snap Keyring"; -- return KeyringTypes2; --})(KeyringTypes || {}); --var isCustodyKeyring = (keyringType) => { -- return keyringType.startsWith("Custody"); --}; --var AccountImportStrategy = /* @__PURE__ */ ((AccountImportStrategy2) => { -- AccountImportStrategy2["privateKey"] = "privateKey"; -- AccountImportStrategy2["json"] = "json"; -- return AccountImportStrategy2; --})(AccountImportStrategy || {}); --var SignTypedDataVersion = /* @__PURE__ */ ((SignTypedDataVersion2) => { -- SignTypedDataVersion2["V1"] = "V1"; -- SignTypedDataVersion2["V3"] = "V3"; -- SignTypedDataVersion2["V4"] = "V4"; -- return SignTypedDataVersion2; --})(SignTypedDataVersion || {}); --function keyringBuilderFactory(KeyringConstructor) { -- const builder = () => new KeyringConstructor(); -- builder.type = KeyringConstructor.type; -- return builder; --} --var defaultKeyringBuilders = [ -- keyringBuilderFactory(_ethsimplekeyring2.default), -- keyringBuilderFactory(_ethhdkeyring2.default) --]; --var getDefaultKeyringState = () => { -- return { -- isUnlocked: false, -- keyrings: [] -- }; --}; --function assertHasUint8ArrayMnemonic(keyring) { -- if (!(_utils.hasProperty.call(void 0, keyring, "mnemonic") && keyring.mnemonic instanceof Uint8Array)) { -- throw new Error("Can't get mnemonic bytes from keyring"); -- } --} --function assertIsExportableKeyEncryptor(encryptor) { -- if (!("importKey" in encryptor && typeof encryptor.importKey === "function" && "decryptWithKey" in encryptor && typeof encryptor.decryptWithKey === "function" && "encryptWithKey" in encryptor && typeof encryptor.encryptWithKey === "function")) { -- throw new Error("KeyringController - The encryptor does not support encryption key export." /* UnsupportedEncryptionKeyExport */); -- } --} --function assertIsValidPassword(password) { -- if (typeof password !== "string") { -- throw new Error("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- if (!password || !password.length) { -- throw new Error("KeyringController - Password cannot be empty." /* InvalidEmptyPassword */); -- } --} --function isSerializedKeyringsArray(array) { -- return typeof array === "object" && Array.isArray(array) && array.every((value) => value.type && _utils.isValidJson.call(void 0, value.data)); --} --async function displayForKeyring(keyring) { -- const accounts = await keyring.getAccounts(); -- return { -- type: keyring.type, -- // Cast to `string[]` here is safe here because `accounts` has no nullish -- // values, and `normalize` returns `string` unless given a nullish value -- accounts: accounts.map(normalize) -- }; --} --function isEthAddress(address) { -- return ( -- // NOTE: This function only checks for lowercased strings -- _utils.isStrictHexString.call(void 0, address.toLowerCase()) && // This checks for lowercased addresses and checksum addresses too -- _utils.isValidHexAddress.call(void 0, address) -- ); --} --function normalize(address) { -- return isEthAddress(address) ? _ethsigutil.normalize.call(void 0, address) : address; --} --var _controllerOperationMutex, _vaultOperationMutex, _keyringBuilders, _keyrings, _unsupportedKeyrings, _password, _encryptor, _cacheEncryptionKey, _qrKeyringStateListener, _registerMessageHandlers, registerMessageHandlers_fn, _getKeyringBuilderForType, getKeyringBuilderForType_fn, _addQRKeyring, addQRKeyring_fn, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn, _getUpdatedKeyrings, getUpdatedKeyrings_fn, _getSerializedKeyrings, getSerializedKeyrings_fn, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn, _unlockKeyrings, unlockKeyrings_fn, _updateVault, updateVault_fn, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn, _newKeyring, newKeyring_fn, _clearKeyrings, clearKeyrings_fn, _restoreKeyring, restoreKeyring_fn, _destroyKeyring, destroyKeyring_fn, _removeEmptyKeyrings, removeEmptyKeyrings_fn, _checkForDuplicate, checkForDuplicate_fn, _setUnlocked, setUnlocked_fn, _persistOrRollback, persistOrRollback_fn, _withRollback, withRollback_fn, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn, _withControllerLock, withControllerLock_fn, _withVaultLock, withVaultLock_fn; --var KeyringController = class extends _basecontroller.BaseController { -- /** -- * Creates a KeyringController instance. -- * -- * @param options - Initial options used to configure this controller -- * @param options.encryptor - An optional object for defining encryption schemes. -- * @param options.keyringBuilders - Set a new name for account. -- * @param options.cacheEncryptionKey - Whether to cache or not encryption key. -- * @param options.messenger - A restricted controller messenger. -- * @param options.state - Initial state to set on this controller. -- */ -- constructor(options) { -- const { -- encryptor = encryptorUtils, -- keyringBuilders, -- messenger, -- state -- } = options; -- super({ -- name, -- metadata: { -- vault: { persist: true, anonymous: false }, -- isUnlocked: { persist: false, anonymous: true }, -- keyrings: { persist: false, anonymous: false }, -- encryptionKey: { persist: false, anonymous: false }, -- encryptionSalt: { persist: false, anonymous: false } -- }, -- messenger, -- state: { -- ...getDefaultKeyringState(), -- ...state -- } -- }); -- /** -- * Constructor helper for registering this controller's messaging system -- * actions. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _registerMessageHandlers); -- /** -- * Get the keyring builder for the given `type`. -- * -- * @param type - The type of keyring to get the builder for. -- * @returns The keyring builder, or undefined if none exists. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getKeyringBuilderForType); -- /** -- * Add qr hardware keyring. -- * -- * @returns The added keyring -- * @throws If a QRKeyring builder is not provided -- * when initializing the controller -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _addQRKeyring); -- /** -- * Subscribe to a QRKeyring state change events and -- * forward them through the messaging system. -- * -- * @param qrKeyring - The QRKeyring instance to subscribe to -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _subscribeToQRKeyringEvents); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unsubscribeFromQRKeyringsEvents); -- /** -- * Create new vault with an initial keyring -- * -- * Destroys any old encrypted storage, -- * creates a new encrypted store with the given password, -- * creates a new wallet with 1 account. -- * -- * @fires KeyringController:unlock -- * @param password - The password to encrypt the vault with. -- * @param keyring - A object containing the params to instantiate a new keyring. -- * @param keyring.type - The keyring type. -- * @param keyring.opts - Optional parameters required to instantiate the keyring. -- * @returns A promise that resolves to the state. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _createNewVaultWithKeyring); -- /** -- * Get the updated array of each keyring's type and -- * accounts list. -- * -- * @returns A promise resolving to the updated keyrings array. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getUpdatedKeyrings); -- /** -- * Serialize the current array of keyring instances, -- * including unsupported keyrings by default. -- * -- * @param options - Method options. -- * @param options.includeUnsupported - Whether to include unsupported keyrings. -- * @returns The serialized keyrings. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getSerializedKeyrings); -- /** -- * Restore a serialized keyrings array. -- * -- * @param serializedKeyrings - The serialized keyrings array. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _restoreSerializedKeyrings); -- /** -- * Unlock Keyrings, decrypting the vault and deserializing all -- * keyrings contained in it, using a password or an encryption key with salt. -- * -- * @param password - The keyring controller password. -- * @param encryptionKey - An exported key string to unlock keyrings with. -- * @param encryptionSalt - The salt used to encrypt the vault. -- * @returns A promise resolving to the deserialized keyrings array. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unlockKeyrings); -- /** -- * Update the vault with the current keyrings. -- * -- * @returns A promise resolving to `true` if the operation is successful. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _updateVault); -- /** -- * Retrieves all the accounts from keyrings instances -- * that are currently in memory. -- * -- * @returns A promise resolving to an array of accounts. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getAccountsFromKeyrings); -- /** -- * Create a new keyring, ensuring that the first account is -- * also created. -- * -- * @param type - Keyring type to instantiate. -- * @param opts - Optional parameters required to instantiate the keyring. -- * @returns A promise that resolves if the operation is successful. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _createKeyringWithFirstAccount); -- /** -- * Instantiate, initialize and return a new keyring of the given `type`, -- * using the given `opts`. The keyring is built using the keyring builder -- * registered for the given `type`. -- * -- * -- * @param type - The type of keyring to add. -- * @param data - The data to restore a previously serialized keyring. -- * @returns The new keyring. -- * @throws If the keyring includes duplicated accounts. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _newKeyring); -- /** -- * Remove all managed keyrings, destroying all their -- * instances in memory. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _clearKeyrings); -- /** -- * Restore a Keyring from a provided serialized payload. -- * On success, returns the resulting keyring instance. -- * -- * @param serialized - The serialized keyring. -- * @returns The deserialized keyring or undefined if the keyring type is unsupported. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _restoreKeyring); -- /** -- * Destroy Keyring -- * -- * Some keyrings support a method called `destroy`, that destroys the -- * keyring along with removing all its event listeners and, in some cases, -- * clears the keyring bridge iframe from the DOM. -- * -- * @param keyring - The keyring to destroy. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _destroyKeyring); -- /** -- * Remove empty keyrings. -- * -- * Loops through the keyrings and removes the ones with empty accounts -- * (usually after removing the last / only account) from a keyring. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _removeEmptyKeyrings); -- /** -- * Checks for duplicate keypairs, using the the first account in the given -- * array. Rejects if a duplicate is found. -- * -- * Only supports 'Simple Key Pair'. -- * -- * @param type - The key pair type to check for. -- * @param newAccountArray - Array of new accounts. -- * @returns The account, if no duplicate is found. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _checkForDuplicate); -- /** -- * Set the `isUnlocked` to true and notify listeners -- * through the messenger. -- * -- * @fires KeyringController:unlock -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _setUnlocked); -- /** -- * Execute the given function after acquiring the controller lock -- * and save the keyrings to state after it, or rollback to their -- * previous state in case of error. -- * -- * @param fn - The function to execute. -- * @returns The result of the function. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _persistOrRollback); -- /** -- * Execute the given function after acquiring the controller lock -- * and rollback keyrings and password states in case of error. -- * -- * @param fn - The function to execute atomically. -- * @returns The result of the function. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withRollback); -- /** -- * Assert that the controller mutex is locked. -- * -- * @throws If the controller mutex is not locked. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _assertControllerMutexIsLocked); -- /** -- * Lock the controller mutex before executing the given function, -- * and release it after the function is resolved or after an -- * error is thrown. -- * -- * This wrapper ensures that each mutable operation that interacts with the -- * controller and that changes its state is executed in a mutually exclusive way, -- * preventing unsafe concurrent access that could lead to unpredictable behavior. -- * -- * @param fn - The function to execute while the controller mutex is locked. -- * @returns The result of the function. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withControllerLock); -- /** -- * Lock the vault mutex before executing the given function, -- * and release it after the function is resolved or after an -- * error is thrown. -- * -- * This ensures that each operation that interacts with the vault -- * is executed in a mutually exclusive way. -- * -- * @param fn - The function to execute while the vault mutex is locked. -- * @returns The result of the function. -- */ -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withVaultLock); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _controllerOperationMutex, new (0, _asyncmutex.Mutex)()); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _vaultOperationMutex, new (0, _asyncmutex.Mutex)()); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _keyringBuilders, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _keyrings, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unsupportedKeyrings, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _password, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _encryptor, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _cacheEncryptionKey, void 0); -- _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _qrKeyringStateListener, void 0); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyringBuilders, keyringBuilders ? defaultKeyringBuilders.concat(keyringBuilders) : defaultKeyringBuilders); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _encryptor, encryptor); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, []); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _unsupportedKeyrings, []); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _cacheEncryptionKey, Boolean(options.cacheEncryptionKey)); -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(encryptor); -- } -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -- } -- /** -- * Adds a new account to the default (first) HD seed phrase keyring. -- * -- * @param accountCount - Number of accounts before adding a new one, used to -- * make the method idempotent. -- * @returns Promise resolving to the added account address. -- */ -- async addNewAccount(accountCount) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found"); -- } -- const oldAccounts = await primaryKeyring.getAccounts(); -- if (accountCount && oldAccounts.length !== accountCount) { -- if (accountCount > oldAccounts.length) { -- throw new Error("Account out of sequence"); -- } -- const existingAccount = oldAccounts[accountCount]; -- if (!existingAccount) { -- throw new Error(`Can't find account at index ${accountCount}`); -- } -- return existingAccount; -- } -- const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -- await this.verifySeedPhrase(); -- return addedAccountAddress; -- }); -- } -- /** -- * Adds a new account to the specified keyring. -- * -- * @param keyring - Keyring to add the account to. -- * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent. -- * @returns Promise resolving to the added account address -- */ -- async addNewAccountForKeyring(keyring, accountCount) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const oldAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- if (accountCount && oldAccounts.length !== accountCount) { -- if (accountCount > oldAccounts.length) { -- throw new Error("Account out of sequence"); -- } -- const existingAccount = oldAccounts[accountCount]; -- _utils.assertIsStrictHexString.call(void 0, existingAccount); -- return existingAccount; -- } -- await keyring.addAccounts(1); -- const addedAccountAddress = (await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this)).find( -- (selectedAddress) => !oldAccounts.includes(selectedAddress) -- ); -- _utils.assertIsStrictHexString.call(void 0, addedAccountAddress); -- return addedAccountAddress; -- }); -- } -- /** -- * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences. -- * -- * @returns Promise resolving to the added account address. -- */ -- async addNewAccountWithoutUpdate() { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found"); -- } -- const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -- await this.verifySeedPhrase(); -- return addedAccountAddress; -- }); -- } -- /** -- * Effectively the same as creating a new keychain then populating it -- * using the given seed phrase. -- * -- * @param password - Password to unlock keychain. -- * @param seed - A BIP39-compliant seed phrase as Uint8Array, -- * either as a string or an array of UTF-8 bytes that represent the string. -- * @returns Promise resolving when the operation ends successfully. -- */ -- async createNewVaultAndRestore(password, seed) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- assertIsValidPassword(password); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -- type: "HD Key Tree" /* hd */, -- opts: { -- mnemonic: seed, -- numberOfAccounts: 1 -- } -- }); -- }); -- } -- /** -- * Create a new primary keychain and wipe any previous keychains. -- * -- * @param password - Password to unlock the new vault. -- * @returns Promise resolving when the operation ends successfully. -- */ -- async createNewVaultAndKeychain(password) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const accounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- if (!accounts.length) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -- type: "HD Key Tree" /* hd */ -- }); -- } -- }); -- } -- /** -- * Adds a new keyring of the given `type`. -- * -- * @param type - Keyring type name. -- * @param opts - Keyring options. -- * @throws If a builder for the given `type` does not exist. -- * @returns Promise resolving to the added keyring. -- */ -- async addNewKeyring(type, opts) { -- if (type === "QR Hardware Wallet Device" /* qr */) { -- return this.getOrAddQRKeyring(); -- } -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, opts)); -- } -- /** -- * Method to verify a given password validity. Throws an -- * error if the password is invalid. -- * -- * @param password - Password of the keyring. -- */ -- async verifyPassword(password) { -- if (!this.state.vault) { -- throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -- } -- await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decrypt(password, this.state.vault); -- } -- /** -- * Returns the status of the vault. -- * -- * @returns Boolean returning true if the vault is unlocked. -- */ -- isUnlocked() { -- return this.state.isUnlocked; -- } -- /** -- * Gets the seed phrase of the HD keyring. -- * -- * @param password - Password of the keyring. -- * @returns Promise resolving to the seed phrase. -- */ -- async exportSeedPhrase(password) { -- await this.verifyPassword(password); -- assertHasUint8ArrayMnemonic(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)[0]); -- return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)[0].mnemonic; -- } -- /** -- * Gets the private key from the keyring controlling an address. -- * -- * @param password - Password of the keyring. -- * @param address - Address to export. -- * @returns Promise resolving to the private key for an address. -- */ -- async exportAccount(password, address) { -- await this.verifyPassword(password); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.exportAccount) { -- throw new Error("`KeyringController - The keyring for the current address does not support the method exportAccount" /* UnsupportedExportAccount */); -- } -- return await keyring.exportAccount(normalize(address)); -- } -- /** -- * Returns the public addresses of all accounts from every keyring. -- * -- * @returns A promise resolving to an array of addresses. -- */ -- async getAccounts() { -- return this.state.keyrings.reduce( -- (accounts, keyring) => accounts.concat(keyring.accounts), -- [] -- ); -- } -- /** -- * Get encryption public key. -- * -- * @param account - An account address. -- * @param opts - Additional encryption options. -- * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method -- * @returns Promise resolving to encyption public key of the `account` if one exists. -- */ -- async getEncryptionPublicKey(account, opts) { -- const address = _ethsigutil.normalize.call(void 0, account); -- const keyring = await this.getKeyringForAccount( -- account -- ); -- if (!keyring.getEncryptionPublicKey) { -- throw new Error("KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey." /* UnsupportedGetEncryptionPublicKey */); -- } -- return await keyring.getEncryptionPublicKey(address, opts); -- } -- /** -- * Attempts to decrypt the provided message parameters. -- * -- * @param messageParams - The decryption message parameters. -- * @param messageParams.from - The address of the account you want to use to decrypt the message. -- * @param messageParams.data - The encrypted data that you want to decrypt. -- * @returns The raw decryption result. -- */ -- async decryptMessage(messageParams) { -- const address = _ethsigutil.normalize.call(void 0, messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.decryptMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method decryptMessage." /* UnsupportedDecryptMessage */); -- } -- return keyring.decryptMessage(address, messageParams.data); -- } -- /** -- * Returns the currently initialized keyring that manages -- * the specified `address` if one exists. -- * -- * @deprecated Use of this method is discouraged as actions executed directly on -- * keyrings are not being reflected in the KeyringController state and not -- * persisted in the vault. Use `withKeyring` instead. -- * @param account - An account address. -- * @returns Promise resolving to keyring of the `account` if one exists. -- */ -- async getKeyringForAccount(account) { -- const address = normalize(account); -- const candidates = await Promise.all( -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -- return Promise.all([keyring, keyring.getAccounts()]); -- }) -- ); -- const winners = candidates.filter((candidate) => { -- const accounts = candidate[1].map(normalize); -- return accounts.includes(address); -- }); -- if (winners.length && winners[0]?.length) { -- return winners[0][0]; -- } -- let errorInfo = ""; -- if (!candidates.length) { -- errorInfo = "There are no keyrings"; -- } else if (!winners.length) { -- errorInfo = "There are keyrings, but none match the address"; -- } -- throw new Error( -- `${"KeyringController - No keyring found" /* NoKeyring */}. Error info: ${errorInfo}` -- ); -- } -- /** -- * Returns all keyrings of the given type. -- * -- * @deprecated Use of this method is discouraged as actions executed directly on -- * keyrings are not being reflected in the KeyringController state and not -- * persisted in the vault. Use `withKeyring` instead. -- * @param type - Keyring type name. -- * @returns An array of keyrings of the given type. -- */ -- getKeyringsByType(type) { -- return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).filter((keyring) => keyring.type === type); -- } -- /** -- * Persist all serialized keyrings in the vault. -- * -- * @deprecated This method is being phased out in favor of `withKeyring`. -- * @returns Promise resolving with `true` value when the -- * operation completes. -- */ -- async persistAllKeyrings() { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => true); -- } -- /** -- * Imports an account with the specified import strategy. -- * -- * @param strategy - Import strategy name. -- * @param args - Array of arguments to pass to the underlying stategy. -- * @throws Will throw when passed an unrecognized strategy. -- * @returns Promise resolving to the imported account address. -- */ -- async importAccountWithStrategy(strategy, args) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- let privateKey; -- switch (strategy) { -- case "privateKey": -- const [importedKey] = args; -- if (!importedKey) { -- throw new Error("Cannot import an empty key."); -- } -- const prefixed = _utils.add0x.call(void 0, importedKey); -- let bufferedPrivateKey; -- try { -- bufferedPrivateKey = _util.toBuffer.call(void 0, prefixed); -- } catch { -- throw new Error("Cannot import invalid private key."); -- } -- if (!_util.isValidPrivate.call(void 0, bufferedPrivateKey) || // ensures that the key is 64 bytes long -- _util.getBinarySize.call(void 0, prefixed) !== 64 + "0x".length) { -- throw new Error("Cannot import invalid private key."); -- } -- privateKey = _utils.remove0x.call(void 0, prefixed); -- break; -- case "json": -- let wallet; -- const [input, password] = args; -- try { -- wallet = _ethereumjswallet.thirdparty.fromEtherWallet(input, password); -- } catch (e) { -- wallet = wallet || await _ethereumjswallet2.default.fromV3(input, password, true); -- } -- privateKey = _utils.bytesToHex.call(void 0, wallet.getPrivateKey()); -- break; -- default: -- throw new Error(`Unexpected import strategy: '${strategy}'`); -- } -- const newKeyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "Simple Key Pair" /* simple */, [ -- privateKey -- ]); -- const accounts = await newKeyring.getAccounts(); -- return accounts[0]; -- }); -- } -- /** -- * Removes an account from keyring state. -- * -- * @param address - Address of the account to remove. -- * @fires KeyringController:accountRemoved -- * @returns Promise resolving when the account is removed. -- */ -- async removeAccount(address) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.removeAccount) { -- throw new Error("`KeyringController - The keyring for the current address does not support the method removeAccount" /* UnsupportedRemoveAccount */); -- } -- await keyring.removeAccount(address); -- const accounts = await keyring.getAccounts(); -- if (accounts.length === 0) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _removeEmptyKeyrings, removeEmptyKeyrings_fn).call(this); -- } -- }); -- this.messagingSystem.publish(`${name}:accountRemoved`, address); -- } -- /** -- * Deallocates all secrets and locks the wallet. -- * -- * @returns Promise resolving when the operation completes. -- */ -- async setLocked() { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn).call(this); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, void 0); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -- this.update((state) => { -- state.isUnlocked = false; -- state.keyrings = []; -- }); -- this.messagingSystem.publish(`${name}:lock`); -- }); -- } -- /** -- * Signs message by calling down into a specific keyring. -- * -- * @param messageParams - PersonalMessageParams object to sign. -- * @returns Promise resolving to a signed message string. -- */ -- async signMessage(messageParams) { -- if (!messageParams.data) { -- throw new Error("Can't sign an empty message"); -- } -- const address = _ethsigutil.normalize.call(void 0, messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signMessage." /* UnsupportedSignMessage */); -- } -- return await keyring.signMessage(address, messageParams.data); -- } -- /** -- * Signs personal message by calling down into a specific keyring. -- * -- * @param messageParams - PersonalMessageParams object to sign. -- * @returns Promise resolving to a signed message string. -- */ -- async signPersonalMessage(messageParams) { -- const address = _ethsigutil.normalize.call(void 0, messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signPersonalMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signPersonalMessage." /* UnsupportedSignPersonalMessage */); -- } -- const normalizedData = normalize(messageParams.data); -- return await keyring.signPersonalMessage(address, normalizedData); -- } -- /** -- * Signs typed message by calling down into a specific keyring. -- * -- * @param messageParams - TypedMessageParams object to sign. -- * @param version - Compatibility version EIP712. -- * @throws Will throw when passed an unrecognized version. -- * @returns Promise resolving to a signed message string or an error if any. -- */ -- async signTypedMessage(messageParams, version) { -- try { -- if (![ -- "V1" /* V1 */, -- "V3" /* V3 */, -- "V4" /* V4 */ -- ].includes(version)) { -- throw new Error(`Unexpected signTypedMessage version: '${version}'`); -- } -- const address = _ethsigutil.normalize.call(void 0, messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signTypedData) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signTypedMessage." /* UnsupportedSignTypedMessage */); -- } -- return await keyring.signTypedData( -- address, -- version !== "V1" /* V1 */ && typeof messageParams.data === "string" ? JSON.parse(messageParams.data) : messageParams.data, -- { version } -- ); -- } catch (error) { -- throw new Error(`Keyring Controller signTypedMessage: ${error}`); -- } -- } -- /** -- * Signs a transaction by calling down into a specific keyring. -- * -- * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance. -- * @param from - Address to sign from, should be in keychain. -- * @param opts - An optional options object. -- * @returns Promise resolving to a signed transaction string. -- */ -- async signTransaction(transaction, from, opts) { -- const address = _ethsigutil.normalize.call(void 0, from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signTransaction) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signTransaction." /* UnsupportedSignTransaction */); -- } -- return await keyring.signTransaction(address, transaction, opts); -- } -- /** -- * Convert a base transaction to a base UserOperation. -- * -- * @param from - Address of the sender. -- * @param transactions - Base transactions to include in the UserOperation. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns A pseudo-UserOperation that can be used to construct a real. -- */ -- async prepareUserOperation(from, transactions, executionContext) { -- const address = _ethsigutil.normalize.call(void 0, from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.prepareUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method prepareUserOperation." /* UnsupportedPrepareUserOperation */); -- } -- return await keyring.prepareUserOperation( -- address, -- transactions, -- executionContext -- ); -- } -- /** -- * Patches properties of a UserOperation. Currently, only the -- * `paymasterAndData` can be patched. -- * -- * @param from - Address of the sender. -- * @param userOp - UserOperation to patch. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns A patch to apply to the UserOperation. -- */ -- async patchUserOperation(from, userOp, executionContext) { -- const address = _ethsigutil.normalize.call(void 0, from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.patchUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method patchUserOperation." /* UnsupportedPatchUserOperation */); -- } -- return await keyring.patchUserOperation(address, userOp, executionContext); -- } -- /** -- * Signs an UserOperation. -- * -- * @param from - Address of the sender. -- * @param userOp - UserOperation to sign. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns The signature of the UserOperation. -- */ -- async signUserOperation(from, userOp, executionContext) { -- const address = _ethsigutil.normalize.call(void 0, from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signUserOperation." /* UnsupportedSignUserOperation */); -- } -- return await keyring.signUserOperation(address, userOp, executionContext); -- } -- /** -- * Changes the password used to encrypt the vault. -- * -- * @param password - The new password. -- * @returns Promise resolving when the operation completes. -- */ -- changePassword(password) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- if (!this.state.isUnlocked) { -- throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -- } -- assertIsValidPassword(password); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -- this.update((state) => { -- delete state.encryptionKey; -- delete state.encryptionSalt; -- }); -- } -- }); -- } -- /** -- * Attempts to decrypt the current vault and load its keyrings, -- * using the given encryption key and salt. -- * -- * @param encryptionKey - Key to unlock the keychain. -- * @param encryptionSalt - Salt to unlock the keychain. -- * @returns Promise resolving when the operation completes. -- */ -- async submitEncryptionKey(encryptionKey, encryptionSalt) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unlockKeyrings, unlockKeyrings_fn).call(this, void 0, encryptionKey, encryptionSalt)); -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); -- }); -- } -- /** -- * Attempts to decrypt the current vault and load its keyrings, -- * using the given password. -- * -- * @param password - Password to unlock the keychain. -- * @returns Promise resolving when the operation completes. -- */ -- async submitPassword(password) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unlockKeyrings, unlockKeyrings_fn).call(this, password)); -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); -- }); -- } -- /** -- * Verifies the that the seed phrase restores the current keychain's accounts. -- * -- * @returns Promise resolving to the seed phrase as Uint8Array. -- */ -- async verifySeedPhrase() { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree" /* hd */)[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found."); -- } -- assertHasUint8ArrayMnemonic(primaryKeyring); -- const seedWords = primaryKeyring.mnemonic; -- const accounts = await primaryKeyring.getAccounts(); -- if (accounts.length === 0) { -- throw new Error("Cannot verify an empty keyring."); -- } -- const hdKeyringBuilder = _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, "HD Key Tree" /* hd */); -- const hdKeyring = hdKeyringBuilder(); -- await hdKeyring.deserialize({ -- mnemonic: seedWords, -- numberOfAccounts: accounts.length -- }); -- const testAccounts = await hdKeyring.getAccounts(); -- if (testAccounts.length !== accounts.length) { -- throw new Error("Seed phrase imported incorrect number of accounts."); -- } -- testAccounts.forEach((account, i) => { -- if (account.toLowerCase() !== accounts[i].toLowerCase()) { -- throw new Error("Seed phrase imported different accounts."); -- } -- }); -- return seedWords; -- } -- async withKeyring(selector, operation, options = { -- createIfMissing: false -- }) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- let keyring; -- if ("address" in selector) { -- keyring = await this.getKeyringForAccount(selector.address); -- } else { -- keyring = this.getKeyringsByType(selector.type)[selector.index || 0]; -- if (!keyring && options.createIfMissing) { -- keyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, selector.type, options.createWithData); -- } -- } -- if (!keyring) { -- throw new Error("KeyringController - Keyring not found." /* KeyringNotFound */); -- } -- const result = await operation(keyring); -- if (Object.is(result, keyring)) { -- throw new Error("KeyringController - Returning keyring instances is unsafe" /* UnsafeDirectKeyringAccess */); -- } -- return result; -- }); -- } -- // QR Hardware related methods -- /** -- * Get QR Hardware keyring. -- * -- * @returns The QR Keyring if defined, otherwise undefined -- */ -- getQRKeyring() { -- return this.getKeyringsByType("QR Hardware Wallet Device" /* qr */)[0]; -- } -- /** -- * Get QR hardware keyring. If it doesn't exist, add it. -- * -- * @returns The added keyring -- */ -- async getOrAddQRKeyring() { -- return this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this)); -- } -- // TODO: Replace `any` with type -- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- async restoreQRKeyring(serialized) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -- keyring.deserialize(serialized); -- }); -- } -- async resetQRKeyringState() { -- (await this.getOrAddQRKeyring()).resetStore(); -- } -- async getQRKeyringState() { -- return (await this.getOrAddQRKeyring()).getMemStore(); -- } -- async submitQRCryptoHDKey(cryptoHDKey) { -- (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey); -- } -- async submitQRCryptoAccount(cryptoAccount) { -- (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount); -- } -- async submitQRSignature(requestId, ethSignature) { -- (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature); -- } -- async cancelQRSignRequest() { -- (await this.getOrAddQRKeyring()).cancelSignRequest(); -- } -- /** -- * Cancels qr keyring sync. -- */ -- async cancelQRSynchronization() { -- (await this.getOrAddQRKeyring()).cancelSync(); -- } -- async connectQRHardware(page) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- try { -- const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -- let accounts; -- switch (page) { -- case -1: -- accounts = await keyring.getPreviousPage(); -- break; -- case 1: -- accounts = await keyring.getNextPage(); -- break; -- default: -- accounts = await keyring.getFirstPage(); -- } -- return accounts.map((account) => { -- return { -- ...account, -- balance: "0x0" -- }; -- }); -- } catch (e) { -- throw new Error(`Unspecified error when connect QR Hardware, ${e}`); -- } -- }); -- } -- async unlockQRHardwareWalletAccount(index) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -- keyring.setAccountToUnlock(index); -- await keyring.addAccounts(1); -- }); -- } -- async getAccountKeyringType(account) { -- const keyring = await this.getKeyringForAccount( -- account -- ); -- return keyring.type; -- } -- async forgetQRDevice() { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring(); -- if (!keyring) { -- return { removedAccounts: [], remainingAccounts: [] }; -- } -- const allAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- keyring.forgetDevice(); -- const remainingAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- const removedAccounts = allAccounts.filter( -- (address) => !remainingAccounts.includes(address) -- ); -- return { removedAccounts, remainingAccounts }; -- }); -- } --}; --_controllerOperationMutex = new WeakMap(); --_vaultOperationMutex = new WeakMap(); --_keyringBuilders = new WeakMap(); --_keyrings = new WeakMap(); --_unsupportedKeyrings = new WeakMap(); --_password = new WeakMap(); --_encryptor = new WeakMap(); --_cacheEncryptionKey = new WeakMap(); --_qrKeyringStateListener = new WeakMap(); --_registerMessageHandlers = new WeakSet(); --registerMessageHandlers_fn = function() { -- this.messagingSystem.registerActionHandler( -- `${name}:signMessage`, -- this.signMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signPersonalMessage`, -- this.signPersonalMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signTypedMessage`, -- this.signTypedMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:decryptMessage`, -- this.decryptMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getEncryptionPublicKey`, -- this.getEncryptionPublicKey.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getAccounts`, -- this.getAccounts.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getKeyringsByType`, -- this.getKeyringsByType.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getKeyringForAccount`, -- this.getKeyringForAccount.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:persistAllKeyrings`, -- this.persistAllKeyrings.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:prepareUserOperation`, -- this.prepareUserOperation.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:patchUserOperation`, -- this.patchUserOperation.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signUserOperation`, -- this.signUserOperation.bind(this) -- ); --}; --_getKeyringBuilderForType = new WeakSet(); --getKeyringBuilderForType_fn = function(type) { -- return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyringBuilders).find( -- (keyringBuilder) => keyringBuilder.type === type -- ); --}; --_addQRKeyring = new WeakSet(); --addQRKeyring_fn = async function() { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- return await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */); --}; --_subscribeToQRKeyringEvents = new WeakSet(); --subscribeToQRKeyringEvents_fn = function(qrKeyring) { -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _qrKeyringStateListener, (state) => { -- this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state); -- }); -- qrKeyring.getMemStore().subscribe(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)); --}; --_unsubscribeFromQRKeyringsEvents = new WeakSet(); --unsubscribeFromQRKeyringsEvents_fn = function() { -- const qrKeyrings = this.getKeyringsByType( -- "QR Hardware Wallet Device" /* qr */ -- ); -- qrKeyrings.forEach((qrKeyring) => { -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)) { -- qrKeyring.getMemStore().unsubscribe(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)); -- } -- }); --}; --_createNewVaultWithKeyring = new WeakSet(); --createNewVaultWithKeyring_fn = async function(password, keyring) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- if (typeof password !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn).call(this, keyring.type, keyring.opts); -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); --}; --_getUpdatedKeyrings = new WeakSet(); --getUpdatedKeyrings_fn = async function() { -- return Promise.all(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(displayForKeyring)); --}; --_getSerializedKeyrings = new WeakSet(); --getSerializedKeyrings_fn = async function({ includeUnsupported } = { -- includeUnsupported: true --}) { -- const serializedKeyrings = await Promise.all( -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -- const [type, data] = await Promise.all([ -- keyring.type, -- keyring.serialize() -- ]); -- return { type, data }; -- }) -- ); -- if (includeUnsupported) { -- serializedKeyrings.push(..._chunkNOCGQCUMjs.__privateGet.call(void 0, this, _unsupportedKeyrings)); -- } -- return serializedKeyrings; --}; --_restoreSerializedKeyrings = new WeakSet(); --restoreSerializedKeyrings_fn = async function(serializedKeyrings) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -- for (const serializedKeyring of serializedKeyrings) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreKeyring, restoreKeyring_fn).call(this, serializedKeyring); -- } --}; --_unlockKeyrings = new WeakSet(); --unlockKeyrings_fn = async function(password, encryptionKey, encryptionSalt) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withVaultLock, withVaultLock_fn).call(this, async ({ releaseLock }) => { -- const encryptedVault = this.state.vault; -- if (!encryptedVault) { -- throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -- } -- let vault; -- const updatedState = {}; -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor)); -- if (password) { -- const result = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decryptWithDetail( -- password, -- encryptedVault -- ); -- vault = result.vault; -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -- updatedState.encryptionKey = result.exportedKeyString; -- updatedState.encryptionSalt = result.salt; -- } else { -- const parsedEncryptedVault = JSON.parse(encryptedVault); -- if (encryptionSalt !== parsedEncryptedVault.salt) { -- throw new Error("KeyringController - Encryption key and salt provided are expired" /* ExpiredCredentials */); -- } -- if (typeof encryptionKey !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- const key = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).importKey(encryptionKey); -- vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decryptWithKey( -- key, -- parsedEncryptedVault -- ); -- updatedState.encryptionKey = encryptionKey; -- updatedState.encryptionSalt = encryptionSalt; -- } -- } else { -- if (typeof password !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decrypt(password, encryptedVault); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -- } -- if (!isSerializedKeyringsArray(vault)) { -- throw new Error("KeyringController - The decrypted vault has an unexpected shape." /* VaultDataError */); -- } -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, vault); -- const updatedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -- this.update((state) => { -- state.keyrings = updatedKeyrings; -- if (updatedState.encryptionKey || updatedState.encryptionSalt) { -- state.encryptionKey = updatedState.encryptionKey; -- state.encryptionSalt = updatedState.encryptionSalt; -- } -- }); -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password) && (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey) || !encryptionKey) && _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).isVaultUpdated && !_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).isVaultUpdated(encryptedVault)) { -- releaseLock(); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _updateVault, updateVault_fn).call(this); -- } -- return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings); -- }); --}; --_updateVault = new WeakSet(); --updateVault_fn = function() { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withVaultLock, withVaultLock_fn).call(this, async () => { -- const { encryptionKey, encryptionSalt } = this.state; -- if (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password) && !encryptionKey) { -- throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -- } -- const serializedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -- if (!serializedKeyrings.some((keyring) => keyring.type === "HD Key Tree" /* hd */)) { -- throw new Error("KeyringController - No HD Keyring found" /* NoHdKeyring */); -- } -- const updatedState = {}; -- if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor)); -- if (encryptionKey) { -- const key = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).importKey(encryptionKey); -- const vaultJSON = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encryptWithKey( -- key, -- serializedKeyrings -- ); -- vaultJSON.salt = encryptionSalt; -- updatedState.vault = JSON.stringify(vaultJSON); -- } else if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password)) { -- const { vault: newVault, exportedKeyString } = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encryptWithDetail( -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password), -- serializedKeyrings -- ); -- updatedState.vault = newVault; -- updatedState.encryptionKey = exportedKeyString; -- } -- } else { -- assertIsValidPassword(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password)); -- updatedState.vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encrypt( -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password), -- serializedKeyrings -- ); -- } -- if (!updatedState.vault) { -- throw new Error("KeyringController - Cannot persist vault without vault information" /* MissingVaultData */); -- } -- const updatedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -- this.update((state) => { -- state.vault = updatedState.vault; -- state.keyrings = updatedKeyrings; -- if (updatedState.encryptionKey) { -- state.encryptionKey = updatedState.encryptionKey; -- state.encryptionSalt = JSON.parse(updatedState.vault).salt; -- } -- }); -- return true; -- }); --}; --_getAccountsFromKeyrings = new WeakSet(); --getAccountsFromKeyrings_fn = async function() { -- const keyrings = _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings); -- const keyringArrays = await Promise.all( -- keyrings.map(async (keyring) => keyring.getAccounts()) -- ); -- const addresses = keyringArrays.reduce((res, arr) => { -- return res.concat(arr); -- }, []); -- return addresses.map(normalize); --}; --_createKeyringWithFirstAccount = new WeakSet(); --createKeyringWithFirstAccount_fn = async function(type, opts) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const keyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, opts); -- const [firstAccount] = await keyring.getAccounts(); -- if (!firstAccount) { -- throw new Error("KeyringController - First Account not found." /* NoFirstAccount */); -- } --}; --_newKeyring = new WeakSet(); --newKeyring_fn = async function(type, data) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const keyringBuilder = _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, type); -- if (!keyringBuilder) { -- throw new Error( -- `${"KeyringController - No keyringBuilder found for keyring" /* NoKeyringBuilder */}. Keyring type: ${type}` -- ); -- } -- const keyring = keyringBuilder(); -- await keyring.deserialize(data); -- if (keyring.init) { -- await keyring.init(); -- } -- if (type === "HD Key Tree" /* hd */ && (!_utils.isObject.call(void 0, data) || !data.mnemonic)) { -- if (!keyring.generateRandomMnemonic) { -- throw new Error( -- "KeyringController - The current keyring does not support the method generateRandomMnemonic." /* UnsupportedGenerateRandomMnemonic */ -- ); -- } -- keyring.generateRandomMnemonic(); -- await keyring.addAccounts(1); -- } -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _checkForDuplicate, checkForDuplicate_fn).call(this, type, await keyring.getAccounts()); -- if (type === "QR Hardware Wallet Device" /* qr */) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn).call(this, keyring); -- } -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).push(keyring); -- return keyring; --}; --_clearKeyrings = new WeakSet(); --clearKeyrings_fn = async function() { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- for (const keyring of _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -- } -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, []); --}; --_restoreKeyring = new WeakSet(); --restoreKeyring_fn = async function(serialized) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- try { -- const { type, data } = serialized; -- return await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, data); -- } catch (_) { -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _unsupportedKeyrings).push(serialized); -- return void 0; -- } --}; --_destroyKeyring = new WeakSet(); --destroyKeyring_fn = async function(keyring) { -- await keyring.destroy?.(); --}; --_removeEmptyKeyrings = new WeakSet(); --removeEmptyKeyrings_fn = async function() { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const validKeyrings = []; -- await Promise.all( -- _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -- const accounts = await keyring.getAccounts(); -- if (accounts.length > 0) { -- validKeyrings.push(keyring); -- } else { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -- } -- }) -- ); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, validKeyrings); --}; --_checkForDuplicate = new WeakSet(); --checkForDuplicate_fn = async function(type, newAccountArray) { -- const accounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- switch (type) { -- case "Simple Key Pair" /* simple */: { -- const isIncluded = Boolean( -- accounts.find( -- (key) => newAccountArray[0] && (key === newAccountArray[0] || key === _utils.remove0x.call(void 0, newAccountArray[0])) -- ) -- ); -- if (isIncluded) { -- throw new Error("KeyringController - The account you are trying to import is a duplicate" /* DuplicatedAccount */); -- } -- return newAccountArray; -- } -- default: { -- return newAccountArray; -- } -- } --}; --_setUnlocked = new WeakSet(); --setUnlocked_fn = function() { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- this.update((state) => { -- state.isUnlocked = true; -- }); -- this.messagingSystem.publish(`${name}:unlock`); --}; --_persistOrRollback = new WeakSet(); --persistOrRollback_fn = async function(fn) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async ({ releaseLock }) => { -- const callbackResult = await fn({ releaseLock }); -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _updateVault, updateVault_fn).call(this); -- return callbackResult; -- }); --}; --_withRollback = new WeakSet(); --withRollback_fn = async function(fn) { -- return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withControllerLock, withControllerLock_fn).call(this, async ({ releaseLock }) => { -- const currentSerializedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -- const currentPassword = _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password); -- try { -- return await fn({ releaseLock }); -- } catch (e) { -- await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, currentSerializedKeyrings); -- _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, currentPassword); -- throw e; -- } -- }); --}; --_assertControllerMutexIsLocked = new WeakSet(); --assertControllerMutexIsLocked_fn = function() { -- if (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _controllerOperationMutex).isLocked()) { -- throw new Error("KeyringController - attempt to update vault during a non mutually exclusive operation" /* ControllerLockRequired */); -- } --}; --_withControllerLock = new WeakSet(); --withControllerLock_fn = async function(fn) { -- return withLock(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _controllerOperationMutex), fn); --}; --_withVaultLock = new WeakSet(); --withVaultLock_fn = async function(fn) { -- _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- return withLock(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _vaultOperationMutex), fn); --}; --async function withLock(mutex, fn) { -- const releaseLock = await mutex.acquire(); -- try { -- return await fn({ releaseLock }); -- } finally { -- releaseLock(); -- } --} --var KeyringController_default = KeyringController; -- -- -- -- -- -- -- -- -- -- --exports.KeyringTypes = KeyringTypes; exports.isCustodyKeyring = isCustodyKeyring; exports.AccountImportStrategy = AccountImportStrategy; exports.SignTypedDataVersion = SignTypedDataVersion; exports.keyringBuilderFactory = keyringBuilderFactory; exports.getDefaultKeyringState = getDefaultKeyringState; exports.KeyringController = KeyringController; exports.KeyringController_default = KeyringController_default; --//# sourceMappingURL=chunk-BRS27QHF.js.map -\ No newline at end of file -diff --git a/dist/chunk-BRS27QHF.js.map b/dist/chunk-BRS27QHF.js.map -deleted file mode 100644 -index e425cd5a63571b3647494e83eb458829f9c6c941..0000000000000000000000000000000000000000 ---- a/dist/chunk-BRS27QHF.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/KeyringController.ts"],"names":["KeyringTypes","AccountImportStrategy","SignTypedDataVersion"],"mappings":"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","sourcesContent":["import type { TxData, TypedTransaction } from '@ethereumjs/tx';\nimport { isValidPrivate, toBuffer, getBinarySize } from '@ethereumjs/util';\nimport type {\n MetaMaskKeyring as QRKeyring,\n IKeyringState as IQRKeyringState,\n} from '@keystonehq/metamask-airgapped-keyring';\nimport type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport * as encryptorUtils from '@metamask/browser-passworder';\nimport HDKeyring from '@metamask/eth-hd-keyring';\nimport { normalize as ethNormalize } from '@metamask/eth-sig-util';\nimport SimpleKeyring from '@metamask/eth-simple-keyring';\nimport type {\n EthBaseTransaction,\n EthBaseUserOperation,\n EthKeyring,\n EthUserOperation,\n EthUserOperationPatch,\n KeyringExecutionContext,\n} from '@metamask/keyring-api';\nimport type {\n PersonalMessageParams,\n TypedMessageParams,\n} from '@metamask/message-manager';\nimport type {\n Eip1024EncryptedData,\n Hex,\n Json,\n KeyringClass,\n} from '@metamask/utils';\nimport {\n add0x,\n assertIsStrictHexString,\n bytesToHex,\n hasProperty,\n isObject,\n isStrictHexString,\n isValidHexAddress,\n isValidJson,\n remove0x,\n} from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\nimport type { MutexInterface } from 'async-mutex';\nimport Wallet, { thirdparty as importers } from 'ethereumjs-wallet';\nimport type { Patch } from 'immer';\n\nimport { KeyringControllerError } from './constants';\n\nconst name = 'KeyringController';\n\n/**\n * Available keyring types\n */\nexport enum KeyringTypes {\n simple = 'Simple Key Pair',\n hd = 'HD Key Tree',\n qr = 'QR Hardware Wallet Device',\n trezor = 'Trezor Hardware',\n ledger = 'Ledger Hardware',\n lattice = 'Lattice Hardware',\n snap = 'Snap Keyring',\n}\n\n/**\n * Custody keyring types are a special case, as they are not a single type\n * but they all start with the prefix \"Custody\".\n * @param keyringType - The type of the keyring.\n * @returns Whether the keyring type is a custody keyring.\n */\nexport const isCustodyKeyring = (keyringType: string): boolean => {\n return keyringType.startsWith('Custody');\n};\n\n/**\n * @type KeyringControllerState\n *\n * Keyring controller state\n * @property vault - Encrypted string representing keyring data\n * @property isUnlocked - Whether vault is unlocked\n * @property keyringTypes - Account types\n * @property keyrings - Group of accounts\n * @property encryptionKey - Keyring encryption key\n * @property encryptionSalt - Keyring encryption salt\n */\nexport type KeyringControllerState = {\n vault?: string;\n isUnlocked: boolean;\n keyrings: KeyringObject[];\n encryptionKey?: string;\n encryptionSalt?: string;\n};\n\nexport type KeyringControllerMemState = Omit<\n KeyringControllerState,\n 'vault' | 'encryptionKey' | 'encryptionSalt'\n>;\n\nexport type KeyringControllerGetStateAction = {\n type: `${typeof name}:getState`;\n handler: () => KeyringControllerState;\n};\n\nexport type KeyringControllerSignMessageAction = {\n type: `${typeof name}:signMessage`;\n handler: KeyringController['signMessage'];\n};\n\nexport type KeyringControllerSignPersonalMessageAction = {\n type: `${typeof name}:signPersonalMessage`;\n handler: KeyringController['signPersonalMessage'];\n};\n\nexport type KeyringControllerSignTypedMessageAction = {\n type: `${typeof name}:signTypedMessage`;\n handler: KeyringController['signTypedMessage'];\n};\n\nexport type KeyringControllerDecryptMessageAction = {\n type: `${typeof name}:decryptMessage`;\n handler: KeyringController['decryptMessage'];\n};\n\nexport type KeyringControllerGetEncryptionPublicKeyAction = {\n type: `${typeof name}:getEncryptionPublicKey`;\n handler: KeyringController['getEncryptionPublicKey'];\n};\n\nexport type KeyringControllerGetKeyringsByTypeAction = {\n type: `${typeof name}:getKeyringsByType`;\n handler: KeyringController['getKeyringsByType'];\n};\n\nexport type KeyringControllerGetKeyringForAccountAction = {\n type: `${typeof name}:getKeyringForAccount`;\n handler: KeyringController['getKeyringForAccount'];\n};\n\nexport type KeyringControllerGetAccountsAction = {\n type: `${typeof name}:getAccounts`;\n handler: KeyringController['getAccounts'];\n};\n\nexport type KeyringControllerPersistAllKeyringsAction = {\n type: `${typeof name}:persistAllKeyrings`;\n handler: KeyringController['persistAllKeyrings'];\n};\n\nexport type KeyringControllerPrepareUserOperationAction = {\n type: `${typeof name}:prepareUserOperation`;\n handler: KeyringController['prepareUserOperation'];\n};\n\nexport type KeyringControllerPatchUserOperationAction = {\n type: `${typeof name}:patchUserOperation`;\n handler: KeyringController['patchUserOperation'];\n};\n\nexport type KeyringControllerSignUserOperationAction = {\n type: `${typeof name}:signUserOperation`;\n handler: KeyringController['signUserOperation'];\n};\n\nexport type KeyringControllerStateChangeEvent = {\n type: `${typeof name}:stateChange`;\n payload: [KeyringControllerState, Patch[]];\n};\n\nexport type KeyringControllerAccountRemovedEvent = {\n type: `${typeof name}:accountRemoved`;\n payload: [string];\n};\n\nexport type KeyringControllerLockEvent = {\n type: `${typeof name}:lock`;\n payload: [];\n};\n\nexport type KeyringControllerUnlockEvent = {\n type: `${typeof name}:unlock`;\n payload: [];\n};\n\nexport type KeyringControllerQRKeyringStateChangeEvent = {\n type: `${typeof name}:qrKeyringStateChange`;\n payload: [ReturnType];\n};\n\nexport type KeyringControllerActions =\n | KeyringControllerGetStateAction\n | KeyringControllerSignMessageAction\n | KeyringControllerSignPersonalMessageAction\n | KeyringControllerSignTypedMessageAction\n | KeyringControllerDecryptMessageAction\n | KeyringControllerGetEncryptionPublicKeyAction\n | KeyringControllerGetAccountsAction\n | KeyringControllerGetKeyringsByTypeAction\n | KeyringControllerGetKeyringForAccountAction\n | KeyringControllerPersistAllKeyringsAction\n | KeyringControllerPrepareUserOperationAction\n | KeyringControllerPatchUserOperationAction\n | KeyringControllerSignUserOperationAction;\n\nexport type KeyringControllerEvents =\n | KeyringControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent\n | KeyringControllerAccountRemovedEvent\n | KeyringControllerQRKeyringStateChangeEvent;\n\nexport type KeyringControllerMessenger = RestrictedControllerMessenger<\n typeof name,\n KeyringControllerActions,\n KeyringControllerEvents,\n never,\n never\n>;\n\nexport type KeyringControllerOptions = {\n keyringBuilders?: { (): EthKeyring; type: string }[];\n messenger: KeyringControllerMessenger;\n state?: { vault?: string };\n} & (\n | {\n cacheEncryptionKey: true;\n encryptor?: ExportableKeyEncryptor;\n }\n | {\n cacheEncryptionKey?: false;\n encryptor?: GenericEncryptor | ExportableKeyEncryptor;\n }\n);\n\n/**\n * @type KeyringObject\n *\n * Keyring object to return in fullUpdate\n * @property type - Keyring type\n * @property accounts - Associated accounts\n */\nexport type KeyringObject = {\n accounts: string[];\n type: string;\n};\n\n/**\n * A strategy for importing an account\n */\nexport enum AccountImportStrategy {\n privateKey = 'privateKey',\n json = 'json',\n}\n\n/**\n * The `signTypedMessage` version\n *\n * @see https://docs.metamask.io/guide/signing-data.html\n */\nexport enum SignTypedDataVersion {\n V1 = 'V1',\n V3 = 'V3',\n V4 = 'V4',\n}\n\n/**\n * A serialized keyring object.\n */\nexport type SerializedKeyring = {\n type: string;\n data: Json;\n};\n\n/**\n * A generic encryptor interface that supports encrypting and decrypting\n * serializable data with a password.\n */\nexport type GenericEncryptor = {\n /**\n * Encrypts the given object with the given password.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encrypted string.\n */\n encrypt: (password: string, object: Json) => Promise;\n /**\n * Decrypts the given encrypted string with the given password.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decrypt: (password: string, encryptedString: string) => Promise;\n /**\n * Optional vault migration helper. Checks if the provided vault is up to date\n * with the desired encryption algorithm.\n *\n * @param vault - The encrypted string to check.\n * @param targetDerivationParams - The desired target derivation params.\n * @returns The updated encrypted string.\n */\n isVaultUpdated?: (\n vault: string,\n targetDerivationParams?: encryptorUtils.KeyDerivationOptions,\n ) => boolean;\n};\n\n/**\n * An encryptor interface that supports encrypting and decrypting\n * serializable data with a password, and exporting and importing keys.\n */\nexport type ExportableKeyEncryptor = GenericEncryptor & {\n /**\n * Encrypts the given object with the given encryption key.\n *\n * @param key - The encryption key to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encryption result.\n */\n encryptWithKey: (\n key: unknown,\n object: Json,\n ) => Promise;\n /**\n * Encrypts the given object with the given password, and returns the\n * encryption result and the exported key string.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @param salt - The optional salt to use for encryption.\n * @returns The encrypted string and the exported key string.\n */\n encryptWithDetail: (\n password: string,\n object: Json,\n salt?: string,\n ) => Promise;\n /**\n * Decrypts the given encrypted string with the given encryption key.\n *\n * @param key - The encryption key to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decryptWithKey: (key: unknown, encryptedString: string) => Promise;\n /**\n * Decrypts the given encrypted string with the given password, and returns\n * the decrypted object and the salt and exported key string used for\n * encryption.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object and the salt and exported key string used for\n * encryption.\n */\n decryptWithDetail: (\n password: string,\n encryptedString: string,\n ) => Promise;\n /**\n * Generates an encryption key from exported key string.\n *\n * @param key - The exported key string.\n * @returns The encryption key.\n */\n importKey: (key: string) => Promise;\n};\n\nexport type KeyringSelector =\n | {\n type: string;\n index?: number;\n }\n | {\n address: Hex;\n };\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise;\n\n/**\n * Get builder function for `Keyring`\n *\n * Returns a builder function for `Keyring` with a `type` property.\n *\n * @param KeyringConstructor - The Keyring class for the builder.\n * @returns A builder function for the given Keyring.\n */\nexport function keyringBuilderFactory(KeyringConstructor: KeyringClass) {\n const builder = () => new KeyringConstructor();\n\n builder.type = KeyringConstructor.type;\n\n return builder;\n}\n\nconst defaultKeyringBuilders = [\n keyringBuilderFactory(SimpleKeyring),\n keyringBuilderFactory(HDKeyring),\n];\n\nexport const getDefaultKeyringState = (): KeyringControllerState => {\n return {\n isUnlocked: false,\n keyrings: [],\n };\n};\n\n/**\n * Assert that the given keyring has an exportable\n * mnemonic.\n *\n * @param keyring - The keyring to check\n * @throws When the keyring does not have a mnemonic\n */\nfunction assertHasUint8ArrayMnemonic(\n keyring: EthKeyring,\n): asserts keyring is EthKeyring & { mnemonic: Uint8Array } {\n if (\n !(\n hasProperty(keyring, 'mnemonic') && keyring.mnemonic instanceof Uint8Array\n )\n ) {\n throw new Error(\"Can't get mnemonic bytes from keyring\");\n }\n}\n\n/**\n * Assert that the provided encryptor supports\n * encryption and encryption key export.\n *\n * @param encryptor - The encryptor to check.\n * @throws If the encryptor does not support key encryption.\n */\nfunction assertIsExportableKeyEncryptor(\n encryptor: GenericEncryptor | ExportableKeyEncryptor,\n): asserts encryptor is ExportableKeyEncryptor {\n if (\n !(\n 'importKey' in encryptor &&\n typeof encryptor.importKey === 'function' &&\n 'decryptWithKey' in encryptor &&\n typeof encryptor.decryptWithKey === 'function' &&\n 'encryptWithKey' in encryptor &&\n typeof encryptor.encryptWithKey === 'function'\n )\n ) {\n throw new Error(KeyringControllerError.UnsupportedEncryptionKeyExport);\n }\n}\n\n/**\n * Assert that the provided password is a valid non-empty string.\n *\n * @param password - The password to check.\n * @throws If the password is not a valid string.\n */\nfunction assertIsValidPassword(password: unknown): asserts password is string {\n if (typeof password !== 'string') {\n throw new Error(KeyringControllerError.WrongPasswordType);\n }\n\n if (!password || !password.length) {\n throw new Error(KeyringControllerError.InvalidEmptyPassword);\n }\n}\n\n/**\n * Checks if the provided value is a serialized keyrings array.\n *\n * @param array - The value to check.\n * @returns True if the value is a serialized keyrings array.\n */\nfunction isSerializedKeyringsArray(\n array: unknown,\n): array is SerializedKeyring[] {\n return (\n typeof array === 'object' &&\n Array.isArray(array) &&\n array.every((value) => value.type && isValidJson(value.data))\n );\n}\n\n/**\n * Display For Keyring\n *\n * Is used for adding the current keyrings to the state object.\n *\n * @param keyring - The keyring to display.\n * @returns A keyring display object, with type and accounts properties.\n */\nasync function displayForKeyring(\n keyring: EthKeyring,\n): Promise<{ type: string; accounts: string[] }> {\n const accounts = await keyring.getAccounts();\n\n return {\n type: keyring.type,\n // Cast to `string[]` here is safe here because `accounts` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n accounts: accounts.map(normalize) as string[],\n };\n}\n\n/**\n * Check if address is an ethereum address\n *\n * @param address - An address.\n * @returns Returns true if the address is an ethereum one, false otherwise.\n */\nfunction isEthAddress(address: string): boolean {\n // We first check if it's a matching `Hex` string, so that is narrows down\n // `address` as an `Hex` type, allowing us to use `isValidHexAddress`\n return (\n // NOTE: This function only checks for lowercased strings\n isStrictHexString(address.toLowerCase()) &&\n // This checks for lowercased addresses and checksum addresses too\n isValidHexAddress(address as Hex)\n );\n}\n\n/**\n * Normalize ethereum or non-EVM address.\n *\n * @param address - Ethereum or non-EVM address.\n * @returns The normalized address.\n */\nfunction normalize(address: string): string | undefined {\n // Since the `KeyringController` is only dealing with address, we have\n // no other way to get the associated account type with this address. So we\n // are down to check the actual address format for now\n // TODO: Find a better way to not have those runtime checks based on the\n // address value!\n return isEthAddress(address) ? ethNormalize(address) : address;\n}\n\n/**\n * Controller responsible for establishing and managing user identity.\n *\n * This class is a wrapper around the `eth-keyring-controller` package. The\n * `eth-keyring-controller` manages the \"vault\", which is an encrypted store of private keys, and\n * it manages the wallet \"lock\" state. This wrapper class has convenience methods for interacting\n * with the internal keyring controller and handling certain complex operations that involve the\n * keyrings.\n */\nexport class KeyringController extends BaseController<\n typeof name,\n KeyringControllerState,\n KeyringControllerMessenger\n> {\n readonly #controllerOperationMutex = new Mutex();\n\n readonly #vaultOperationMutex = new Mutex();\n\n #keyringBuilders: { (): EthKeyring; type: string }[];\n\n #keyrings: EthKeyring[];\n\n #unsupportedKeyrings: SerializedKeyring[];\n\n #password?: string;\n\n #encryptor: GenericEncryptor | ExportableKeyEncryptor;\n\n #cacheEncryptionKey: boolean;\n\n #qrKeyringStateListener?: (\n state: ReturnType,\n ) => void;\n\n /**\n * Creates a KeyringController instance.\n *\n * @param options - Initial options used to configure this controller\n * @param options.encryptor - An optional object for defining encryption schemes.\n * @param options.keyringBuilders - Set a new name for account.\n * @param options.cacheEncryptionKey - Whether to cache or not encryption key.\n * @param options.messenger - A restricted controller messenger.\n * @param options.state - Initial state to set on this controller.\n */\n constructor(options: KeyringControllerOptions) {\n const {\n encryptor = encryptorUtils,\n keyringBuilders,\n messenger,\n state,\n } = options;\n\n super({\n name,\n metadata: {\n vault: { persist: true, anonymous: false },\n isUnlocked: { persist: false, anonymous: true },\n keyrings: { persist: false, anonymous: false },\n encryptionKey: { persist: false, anonymous: false },\n encryptionSalt: { persist: false, anonymous: false },\n },\n messenger,\n state: {\n ...getDefaultKeyringState(),\n ...state,\n },\n });\n\n this.#keyringBuilders = keyringBuilders\n ? defaultKeyringBuilders.concat(keyringBuilders)\n : defaultKeyringBuilders;\n\n this.#encryptor = encryptor;\n this.#keyrings = [];\n this.#unsupportedKeyrings = [];\n\n // This option allows the controller to cache an exported key\n // for use in decrypting and encrypting data without password\n this.#cacheEncryptionKey = Boolean(options.cacheEncryptionKey);\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(encryptor);\n }\n\n this.#registerMessageHandlers();\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring.\n *\n * @param accountCount - Number of accounts before adding a new one, used to\n * make the method idempotent.\n * @returns Promise resolving to the added account address.\n */\n async addNewAccount(accountCount?: number): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const oldAccounts = await primaryKeyring.getAccounts();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n // we return the account already existing at index `accountCount`\n const existingAccount = oldAccounts[accountCount];\n\n if (!existingAccount) {\n throw new Error(`Can't find account at index ${accountCount}`);\n }\n\n return existingAccount;\n }\n\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the specified keyring.\n *\n * @param keyring - Keyring to add the account to.\n * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent.\n * @returns Promise resolving to the added account address\n */\n async addNewAccountForKeyring(\n keyring: EthKeyring,\n accountCount?: number,\n ): Promise {\n // READ THIS CAREFULLY:\n // We still uses `Hex` here, since we are not using this method when creating\n // and account using a \"Snap Keyring\". This function assume the `keyring` is\n // ethereum compatible, but \"Snap Keyring\" might not be.\n return this.#persistOrRollback(async () => {\n const oldAccounts = await this.#getAccountsFromKeyrings();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n\n const existingAccount = oldAccounts[accountCount];\n assertIsStrictHexString(existingAccount);\n\n return existingAccount;\n }\n\n await keyring.addAccounts(1);\n\n const addedAccountAddress = (await this.#getAccountsFromKeyrings()).find(\n (selectedAddress) => !oldAccounts.includes(selectedAddress),\n );\n assertIsStrictHexString(addedAccountAddress);\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences.\n *\n * @returns Promise resolving to the added account address.\n */\n async addNewAccountWithoutUpdate(): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n return addedAccountAddress;\n });\n }\n\n /**\n * Effectively the same as creating a new keychain then populating it\n * using the given seed phrase.\n *\n * @param password - Password to unlock keychain.\n * @param seed - A BIP39-compliant seed phrase as Uint8Array,\n * either as a string or an array of UTF-8 bytes that represent the string.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndRestore(\n password: string,\n seed: Uint8Array,\n ): Promise {\n return this.#persistOrRollback(async () => {\n assertIsValidPassword(password);\n\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n opts: {\n mnemonic: seed,\n numberOfAccounts: 1,\n },\n });\n });\n }\n\n /**\n * Create a new primary keychain and wipe any previous keychains.\n *\n * @param password - Password to unlock the new vault.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndKeychain(password: string) {\n return this.#persistOrRollback(async () => {\n const accounts = await this.#getAccountsFromKeyrings();\n if (!accounts.length) {\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n });\n }\n });\n }\n\n /**\n * Adds a new keyring of the given `type`.\n *\n * @param type - Keyring type name.\n * @param opts - Keyring options.\n * @throws If a builder for the given `type` does not exist.\n * @returns Promise resolving to the added keyring.\n */\n async addNewKeyring(\n type: KeyringTypes | string,\n opts?: unknown,\n ): Promise {\n if (type === KeyringTypes.qr) {\n return this.getOrAddQRKeyring();\n }\n\n return this.#persistOrRollback(async () => this.#newKeyring(type, opts));\n }\n\n /**\n * Method to verify a given password validity. Throws an\n * error if the password is invalid.\n *\n * @param password - Password of the keyring.\n */\n async verifyPassword(password: string) {\n if (!this.state.vault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n await this.#encryptor.decrypt(password, this.state.vault);\n }\n\n /**\n * Returns the status of the vault.\n *\n * @returns Boolean returning true if the vault is unlocked.\n */\n isUnlocked(): boolean {\n return this.state.isUnlocked;\n }\n\n /**\n * Gets the seed phrase of the HD keyring.\n *\n * @param password - Password of the keyring.\n * @returns Promise resolving to the seed phrase.\n */\n async exportSeedPhrase(password: string): Promise {\n await this.verifyPassword(password);\n assertHasUint8ArrayMnemonic(this.#keyrings[0]);\n return this.#keyrings[0].mnemonic;\n }\n\n /**\n * Gets the private key from the keyring controlling an address.\n *\n * @param password - Password of the keyring.\n * @param address - Address to export.\n * @returns Promise resolving to the private key for an address.\n */\n async exportAccount(password: string, address: string): Promise {\n await this.verifyPassword(password);\n\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.exportAccount) {\n throw new Error(KeyringControllerError.UnsupportedExportAccount);\n }\n\n return await keyring.exportAccount(normalize(address) as Hex);\n }\n\n /**\n * Returns the public addresses of all accounts from every keyring.\n *\n * @returns A promise resolving to an array of addresses.\n */\n async getAccounts(): Promise {\n return this.state.keyrings.reduce(\n (accounts, keyring) => accounts.concat(keyring.accounts),\n [],\n );\n }\n\n /**\n * Get encryption public key.\n *\n * @param account - An account address.\n * @param opts - Additional encryption options.\n * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method\n * @returns Promise resolving to encyption public key of the `account` if one exists.\n */\n async getEncryptionPublicKey(\n account: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(account) as Hex;\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n if (!keyring.getEncryptionPublicKey) {\n throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey);\n }\n\n return await keyring.getEncryptionPublicKey(address, opts);\n }\n\n /**\n * Attempts to decrypt the provided message parameters.\n *\n * @param messageParams - The decryption message parameters.\n * @param messageParams.from - The address of the account you want to use to decrypt the message.\n * @param messageParams.data - The encrypted data that you want to decrypt.\n * @returns The raw decryption result.\n */\n async decryptMessage(messageParams: {\n from: string;\n data: Eip1024EncryptedData;\n }): Promise {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.decryptMessage) {\n throw new Error(KeyringControllerError.UnsupportedDecryptMessage);\n }\n\n return keyring.decryptMessage(address, messageParams.data);\n }\n\n /**\n * Returns the currently initialized keyring that manages\n * the specified `address` if one exists.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param account - An account address.\n * @returns Promise resolving to keyring of the `account` if one exists.\n */\n async getKeyringForAccount(account: string): Promise {\n const address = normalize(account);\n\n const candidates = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n return Promise.all([keyring, keyring.getAccounts()]);\n }),\n );\n\n const winners = candidates.filter((candidate) => {\n const accounts = candidate[1].map(normalize);\n return accounts.includes(address);\n });\n\n if (winners.length && winners[0]?.length) {\n return winners[0][0];\n }\n\n // Adding more info to the error\n let errorInfo = '';\n if (!candidates.length) {\n errorInfo = 'There are no keyrings';\n } else if (!winners.length) {\n errorInfo = 'There are keyrings, but none match the address';\n }\n throw new Error(\n `${KeyringControllerError.NoKeyring}. Error info: ${errorInfo}`,\n );\n }\n\n /**\n * Returns all keyrings of the given type.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param type - Keyring type name.\n * @returns An array of keyrings of the given type.\n */\n getKeyringsByType(type: KeyringTypes | string): unknown[] {\n return this.#keyrings.filter((keyring) => keyring.type === type);\n }\n\n /**\n * Persist all serialized keyrings in the vault.\n *\n * @deprecated This method is being phased out in favor of `withKeyring`.\n * @returns Promise resolving with `true` value when the\n * operation completes.\n */\n async persistAllKeyrings(): Promise {\n return this.#persistOrRollback(async () => true);\n }\n\n /**\n * Imports an account with the specified import strategy.\n *\n * @param strategy - Import strategy name.\n * @param args - Array of arguments to pass to the underlying stategy.\n * @throws Will throw when passed an unrecognized strategy.\n * @returns Promise resolving to the imported account address.\n */\n async importAccountWithStrategy(\n strategy: AccountImportStrategy,\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[],\n ): Promise {\n return this.#persistOrRollback(async () => {\n let privateKey;\n switch (strategy) {\n case 'privateKey':\n const [importedKey] = args;\n if (!importedKey) {\n throw new Error('Cannot import an empty key.');\n }\n const prefixed = add0x(importedKey);\n\n let bufferedPrivateKey;\n try {\n bufferedPrivateKey = toBuffer(prefixed);\n } catch {\n throw new Error('Cannot import invalid private key.');\n }\n\n if (\n !isValidPrivate(bufferedPrivateKey) ||\n // ensures that the key is 64 bytes long\n getBinarySize(prefixed) !== 64 + '0x'.length\n ) {\n throw new Error('Cannot import invalid private key.');\n }\n\n privateKey = remove0x(prefixed);\n break;\n case 'json':\n let wallet;\n const [input, password] = args;\n try {\n wallet = importers.fromEtherWallet(input, password);\n } catch (e) {\n wallet = wallet || (await Wallet.fromV3(input, password, true));\n }\n privateKey = bytesToHex(wallet.getPrivateKey());\n break;\n default:\n throw new Error(`Unexpected import strategy: '${strategy}'`);\n }\n const newKeyring = (await this.#newKeyring(KeyringTypes.simple, [\n privateKey,\n ])) as EthKeyring;\n const accounts = await newKeyring.getAccounts();\n return accounts[0];\n });\n }\n\n /**\n * Removes an account from keyring state.\n *\n * @param address - Address of the account to remove.\n * @fires KeyringController:accountRemoved\n * @returns Promise resolving when the account is removed.\n */\n async removeAccount(address: string): Promise {\n await this.#persistOrRollback(async () => {\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n // Not all the keyrings support this, so we have to check\n if (!keyring.removeAccount) {\n throw new Error(KeyringControllerError.UnsupportedRemoveAccount);\n }\n\n // The `removeAccount` method of snaps keyring is async. We have to update\n // the interface of the other keyrings to be async as well.\n // eslint-disable-next-line @typescript-eslint/await-thenable\n // FIXME: We do cast to `Hex` to makes the type checker happy here, and\n // because `Keyring.removeAccount` requires address to be `Hex`. Those\n // type would need to be updated for a full non-EVM support.\n await keyring.removeAccount(address as Hex);\n\n const accounts = await keyring.getAccounts();\n // Check if this was the last/only account\n if (accounts.length === 0) {\n await this.#removeEmptyKeyrings();\n }\n });\n\n this.messagingSystem.publish(`${name}:accountRemoved`, address);\n }\n\n /**\n * Deallocates all secrets and locks the wallet.\n *\n * @returns Promise resolving when the operation completes.\n */\n async setLocked(): Promise {\n return this.#withRollback(async () => {\n this.#unsubscribeFromQRKeyringsEvents();\n\n this.#password = undefined;\n await this.#clearKeyrings();\n\n this.update((state) => {\n state.isUnlocked = false;\n state.keyrings = [];\n });\n\n this.messagingSystem.publish(`${name}:lock`);\n });\n }\n\n /**\n * Signs message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signMessage(messageParams: PersonalMessageParams): Promise {\n if (!messageParams.data) {\n throw new Error(\"Can't sign an empty message\");\n }\n\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignMessage);\n }\n\n return await keyring.signMessage(address, messageParams.data);\n }\n\n /**\n * Signs personal message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signPersonalMessage(messageParams: PersonalMessageParams) {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signPersonalMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignPersonalMessage);\n }\n\n const normalizedData = normalize(messageParams.data) as Hex;\n\n return await keyring.signPersonalMessage(address, normalizedData);\n }\n\n /**\n * Signs typed message by calling down into a specific keyring.\n *\n * @param messageParams - TypedMessageParams object to sign.\n * @param version - Compatibility version EIP712.\n * @throws Will throw when passed an unrecognized version.\n * @returns Promise resolving to a signed message string or an error if any.\n */\n async signTypedMessage(\n messageParams: TypedMessageParams,\n version: SignTypedDataVersion,\n ): Promise {\n try {\n if (\n ![\n SignTypedDataVersion.V1,\n SignTypedDataVersion.V3,\n SignTypedDataVersion.V4,\n ].includes(version)\n ) {\n throw new Error(`Unexpected signTypedMessage version: '${version}'`);\n }\n\n // Cast to `Hex` here is safe here because `messageParams.from` is not nullish.\n // `normalize` returns `Hex` unless given a nullish value.\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTypedData) {\n throw new Error(KeyringControllerError.UnsupportedSignTypedMessage);\n }\n\n return await keyring.signTypedData(\n address,\n version !== SignTypedDataVersion.V1 &&\n typeof messageParams.data === 'string'\n ? JSON.parse(messageParams.data)\n : messageParams.data,\n { version },\n );\n } catch (error) {\n throw new Error(`Keyring Controller signTypedMessage: ${error}`);\n }\n }\n\n /**\n * Signs a transaction by calling down into a specific keyring.\n *\n * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance.\n * @param from - Address to sign from, should be in keychain.\n * @param opts - An optional options object.\n * @returns Promise resolving to a signed transaction string.\n */\n async signTransaction(\n transaction: TypedTransaction,\n from: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTransaction) {\n throw new Error(KeyringControllerError.UnsupportedSignTransaction);\n }\n\n return await keyring.signTransaction(address, transaction, opts);\n }\n\n /**\n * Convert a base transaction to a base UserOperation.\n *\n * @param from - Address of the sender.\n * @param transactions - Base transactions to include in the UserOperation.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A pseudo-UserOperation that can be used to construct a real.\n */\n async prepareUserOperation(\n from: string,\n transactions: EthBaseTransaction[],\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.prepareUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPrepareUserOperation);\n }\n\n return await keyring.prepareUserOperation(\n address,\n transactions,\n executionContext,\n );\n }\n\n /**\n * Patches properties of a UserOperation. Currently, only the\n * `paymasterAndData` can be patched.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to patch.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A patch to apply to the UserOperation.\n */\n async patchUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.patchUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPatchUserOperation);\n }\n\n return await keyring.patchUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Signs an UserOperation.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to sign.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns The signature of the UserOperation.\n */\n async signUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.signUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedSignUserOperation);\n }\n\n return await keyring.signUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Changes the password used to encrypt the vault.\n *\n * @param password - The new password.\n * @returns Promise resolving when the operation completes.\n */\n changePassword(password: string): Promise {\n return this.#persistOrRollback(async () => {\n if (!this.state.isUnlocked) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n assertIsValidPassword(password);\n\n this.#password = password;\n // We need to clear encryption key and salt from state\n // to force the controller to re-encrypt the vault using\n // the new password.\n if (this.#cacheEncryptionKey) {\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n }\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given encryption key and salt.\n *\n * @param encryptionKey - Key to unlock the keychain.\n * @param encryptionSalt - Salt to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitEncryptionKey(\n encryptionKey: string,\n encryptionSalt: string,\n ): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(\n undefined,\n encryptionKey,\n encryptionSalt,\n );\n this.#setUnlocked();\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given password.\n *\n * @param password - Password to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitPassword(password: string): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(password);\n this.#setUnlocked();\n });\n }\n\n /**\n * Verifies the that the seed phrase restores the current keychain's accounts.\n *\n * @returns Promise resolving to the seed phrase as Uint8Array.\n */\n async verifySeedPhrase(): Promise {\n const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found.');\n }\n\n assertHasUint8ArrayMnemonic(primaryKeyring);\n\n const seedWords = primaryKeyring.mnemonic;\n const accounts = await primaryKeyring.getAccounts();\n /* istanbul ignore if */\n if (accounts.length === 0) {\n throw new Error('Cannot verify an empty keyring.');\n }\n\n // The HD Keyring Builder is a default keyring builder\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!;\n\n const hdKeyring = hdKeyringBuilder();\n // @ts-expect-error @metamask/eth-hd-keyring correctly handles\n // Uint8Array seed phrases in the `deserialize` method.\n await hdKeyring.deserialize({\n mnemonic: seedWords,\n numberOfAccounts: accounts.length,\n });\n const testAccounts = await hdKeyring.getAccounts();\n /* istanbul ignore if */\n if (testAccounts.length !== accounts.length) {\n throw new Error('Seed phrase imported incorrect number of accounts.');\n }\n\n testAccounts.forEach((account: string, i: number) => {\n /* istanbul ignore if */\n if (account.toLowerCase() !== accounts[i].toLowerCase()) {\n throw new Error('Seed phrase imported different accounts.');\n }\n });\n\n return seedWords;\n }\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @param options - Additional options.\n * @param options.createIfMissing - Whether to create a new keyring if the selected one is missing.\n * @param options.createWithData - Optional data to use when creating a new keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n * @deprecated This method overload is deprecated. Use `withKeyring` without options instead.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n // eslint-disable-next-line @typescript-eslint/unified-signatures\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown },\n ): Promise;\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n ): Promise;\n\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown } = {\n createIfMissing: false,\n },\n ): Promise {\n return this.#persistOrRollback(async () => {\n let keyring: SelectedKeyring | undefined;\n\n if ('address' in selector) {\n keyring = (await this.getKeyringForAccount(selector.address)) as\n | SelectedKeyring\n | undefined;\n } else {\n keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as\n | SelectedKeyring\n | undefined;\n\n if (!keyring && options.createIfMissing) {\n keyring = (await this.#newKeyring(\n selector.type,\n options.createWithData,\n )) as SelectedKeyring;\n }\n }\n\n if (!keyring) {\n throw new Error(KeyringControllerError.KeyringNotFound);\n }\n\n const result = await operation(keyring);\n\n if (Object.is(result, keyring)) {\n // Access to a keyring instance outside of controller safeguards\n // should be discouraged, as it can lead to unexpected behavior.\n // This error is thrown to prevent consumers using `withKeyring`\n // as a way to get a reference to a keyring instance.\n throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess);\n }\n\n return result;\n });\n }\n\n // QR Hardware related methods\n\n /**\n * Get QR Hardware keyring.\n *\n * @returns The QR Keyring if defined, otherwise undefined\n */\n getQRKeyring(): QRKeyring | undefined {\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return this.getKeyringsByType(KeyringTypes.qr)[0] as unknown as QRKeyring;\n }\n\n /**\n * Get QR hardware keyring. If it doesn't exist, add it.\n *\n * @returns The added keyring\n */\n async getOrAddQRKeyring(): Promise {\n return (\n this.getQRKeyring() ||\n (await this.#persistOrRollback(async () => this.#addQRKeyring()))\n );\n }\n\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async restoreQRKeyring(serialized: any): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n keyring.deserialize(serialized);\n });\n }\n\n async resetQRKeyringState(): Promise {\n (await this.getOrAddQRKeyring()).resetStore();\n }\n\n async getQRKeyringState(): Promise {\n return (await this.getOrAddQRKeyring()).getMemStore();\n }\n\n async submitQRCryptoHDKey(cryptoHDKey: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey);\n }\n\n async submitQRCryptoAccount(cryptoAccount: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount);\n }\n\n async submitQRSignature(\n requestId: string,\n ethSignature: string,\n ): Promise {\n (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature);\n }\n\n async cancelQRSignRequest(): Promise {\n (await this.getOrAddQRKeyring()).cancelSignRequest();\n }\n\n /**\n * Cancels qr keyring sync.\n */\n async cancelQRSynchronization(): Promise {\n // eslint-disable-next-line n/no-sync\n (await this.getOrAddQRKeyring()).cancelSync();\n }\n\n async connectQRHardware(\n page: number,\n ): Promise<{ balance: string; address: string; index: number }[]> {\n return this.#persistOrRollback(async () => {\n try {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n let accounts;\n switch (page) {\n case -1:\n accounts = await keyring.getPreviousPage();\n break;\n case 1:\n accounts = await keyring.getNextPage();\n break;\n default:\n accounts = await keyring.getFirstPage();\n }\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return accounts.map((account: any) => {\n return {\n ...account,\n balance: '0x0',\n };\n });\n } catch (e) {\n // TODO: Add test case for when keyring throws\n /* istanbul ignore next */\n throw new Error(`Unspecified error when connect QR Hardware, ${e}`);\n }\n });\n }\n\n async unlockQRHardwareWalletAccount(index: number): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n\n keyring.setAccountToUnlock(index);\n await keyring.addAccounts(1);\n });\n }\n\n async getAccountKeyringType(account: string): Promise {\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n return keyring.type;\n }\n\n async forgetQRDevice(): Promise<{\n removedAccounts: string[];\n remainingAccounts: string[];\n }> {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring();\n\n if (!keyring) {\n return { removedAccounts: [], remainingAccounts: [] };\n }\n\n const allAccounts = (await this.#getAccountsFromKeyrings()) as string[];\n keyring.forgetDevice();\n const remainingAccounts =\n (await this.#getAccountsFromKeyrings()) as string[];\n const removedAccounts = allAccounts.filter(\n (address: string) => !remainingAccounts.includes(address),\n );\n return { removedAccounts, remainingAccounts };\n });\n }\n\n /**\n * Constructor helper for registering this controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messagingSystem.registerActionHandler(\n `${name}:signMessage`,\n this.signMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signPersonalMessage`,\n this.signPersonalMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signTypedMessage`,\n this.signTypedMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:decryptMessage`,\n this.decryptMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getEncryptionPublicKey`,\n this.getEncryptionPublicKey.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getAccounts`,\n this.getAccounts.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringsByType`,\n this.getKeyringsByType.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringForAccount`,\n this.getKeyringForAccount.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:persistAllKeyrings`,\n this.persistAllKeyrings.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:prepareUserOperation`,\n this.prepareUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:patchUserOperation`,\n this.patchUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signUserOperation`,\n this.signUserOperation.bind(this),\n );\n }\n\n /**\n * Get the keyring builder for the given `type`.\n *\n * @param type - The type of keyring to get the builder for.\n * @returns The keyring builder, or undefined if none exists.\n */\n #getKeyringBuilderForType(\n type: string,\n ): { (): EthKeyring; type: string } | undefined {\n return this.#keyringBuilders.find(\n (keyringBuilder) => keyringBuilder.type === type,\n );\n }\n\n /**\n * Add qr hardware keyring.\n *\n * @returns The added keyring\n * @throws If a QRKeyring builder is not provided\n * when initializing the controller\n */\n async #addQRKeyring(): Promise {\n this.#assertControllerMutexIsLocked();\n\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return (await this.#newKeyring(KeyringTypes.qr)) as unknown as QRKeyring;\n }\n\n /**\n * Subscribe to a QRKeyring state change events and\n * forward them through the messaging system.\n *\n * @param qrKeyring - The QRKeyring instance to subscribe to\n */\n #subscribeToQRKeyringEvents(qrKeyring: QRKeyring) {\n this.#qrKeyringStateListener = (state) => {\n this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state);\n };\n\n qrKeyring.getMemStore().subscribe(this.#qrKeyringStateListener);\n }\n\n #unsubscribeFromQRKeyringsEvents() {\n const qrKeyrings = this.getKeyringsByType(\n KeyringTypes.qr,\n ) as unknown as QRKeyring[];\n\n qrKeyrings.forEach((qrKeyring) => {\n if (this.#qrKeyringStateListener) {\n qrKeyring.getMemStore().unsubscribe(this.#qrKeyringStateListener);\n }\n });\n }\n\n /**\n * Create new vault with an initial keyring\n *\n * Destroys any old encrypted storage,\n * creates a new encrypted store with the given password,\n * creates a new wallet with 1 account.\n *\n * @fires KeyringController:unlock\n * @param password - The password to encrypt the vault with.\n * @param keyring - A object containing the params to instantiate a new keyring.\n * @param keyring.type - The keyring type.\n * @param keyring.opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves to the state.\n */\n async #createNewVaultWithKeyring(\n password: string,\n keyring: {\n type: string;\n opts?: unknown;\n },\n ): Promise {\n this.#assertControllerMutexIsLocked();\n\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n this.#password = password;\n\n await this.#clearKeyrings();\n await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);\n this.#setUnlocked();\n }\n\n /**\n * Get the updated array of each keyring's type and\n * accounts list.\n *\n * @returns A promise resolving to the updated keyrings array.\n */\n async #getUpdatedKeyrings(): Promise {\n return Promise.all(this.#keyrings.map(displayForKeyring));\n }\n\n /**\n * Serialize the current array of keyring instances,\n * including unsupported keyrings by default.\n *\n * @param options - Method options.\n * @param options.includeUnsupported - Whether to include unsupported keyrings.\n * @returns The serialized keyrings.\n */\n async #getSerializedKeyrings(\n { includeUnsupported }: { includeUnsupported: boolean } = {\n includeUnsupported: true,\n },\n ): Promise {\n const serializedKeyrings = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n const [type, data] = await Promise.all([\n keyring.type,\n keyring.serialize(),\n ]);\n return { type, data };\n }),\n );\n\n if (includeUnsupported) {\n serializedKeyrings.push(...this.#unsupportedKeyrings);\n }\n\n return serializedKeyrings;\n }\n\n /**\n * Restore a serialized keyrings array.\n *\n * @param serializedKeyrings - The serialized keyrings array.\n */\n async #restoreSerializedKeyrings(\n serializedKeyrings: SerializedKeyring[],\n ): Promise {\n await this.#clearKeyrings();\n\n for (const serializedKeyring of serializedKeyrings) {\n await this.#restoreKeyring(serializedKeyring);\n }\n }\n\n /**\n * Unlock Keyrings, decrypting the vault and deserializing all\n * keyrings contained in it, using a password or an encryption key with salt.\n *\n * @param password - The keyring controller password.\n * @param encryptionKey - An exported key string to unlock keyrings with.\n * @param encryptionSalt - The salt used to encrypt the vault.\n * @returns A promise resolving to the deserialized keyrings array.\n */\n async #unlockKeyrings(\n password: string | undefined,\n encryptionKey?: string,\n encryptionSalt?: string,\n ): Promise[]> {\n return this.#withVaultLock(async ({ releaseLock }) => {\n const encryptedVault = this.state.vault;\n if (!encryptedVault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n\n let vault;\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (password) {\n const result = await this.#encryptor.decryptWithDetail(\n password,\n encryptedVault,\n );\n vault = result.vault;\n this.#password = password;\n\n updatedState.encryptionKey = result.exportedKeyString;\n updatedState.encryptionSalt = result.salt;\n } else {\n const parsedEncryptedVault = JSON.parse(encryptedVault);\n\n if (encryptionSalt !== parsedEncryptedVault.salt) {\n throw new Error(KeyringControllerError.ExpiredCredentials);\n }\n\n if (typeof encryptionKey !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n const key = await this.#encryptor.importKey(encryptionKey);\n vault = await this.#encryptor.decryptWithKey(\n key,\n parsedEncryptedVault,\n );\n\n // This call is required on the first call because encryptionKey\n // is not yet inside the memStore\n updatedState.encryptionKey = encryptionKey;\n // we can safely assume that encryptionSalt is defined here\n // because we compare it with the salt from the vault\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n updatedState.encryptionSalt = encryptionSalt!;\n }\n } else {\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n vault = await this.#encryptor.decrypt(password, encryptedVault);\n this.#password = password;\n }\n\n if (!isSerializedKeyringsArray(vault)) {\n throw new Error(KeyringControllerError.VaultDataError);\n }\n\n await this.#restoreSerializedKeyrings(vault);\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n\n this.update((state) => {\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey || updatedState.encryptionSalt) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = updatedState.encryptionSalt;\n }\n });\n\n if (\n this.#password &&\n (!this.#cacheEncryptionKey || !encryptionKey) &&\n this.#encryptor.isVaultUpdated &&\n !this.#encryptor.isVaultUpdated(encryptedVault)\n ) {\n // The lock needs to be released before persisting the keyrings\n // to avoid deadlock\n releaseLock();\n // Re-encrypt the vault with safer method if one is available\n await this.#updateVault();\n }\n\n return this.#keyrings;\n });\n }\n\n /**\n * Update the vault with the current keyrings.\n *\n * @returns A promise resolving to `true` if the operation is successful.\n */\n #updateVault(): Promise {\n return this.#withVaultLock(async () => {\n const { encryptionKey, encryptionSalt } = this.state;\n\n if (!this.#password && !encryptionKey) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n const serializedKeyrings = await this.#getSerializedKeyrings();\n\n if (\n !serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)\n ) {\n throw new Error(KeyringControllerError.NoHdKeyring);\n }\n\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (encryptionKey) {\n const key = await this.#encryptor.importKey(encryptionKey);\n const vaultJSON = await this.#encryptor.encryptWithKey(\n key,\n serializedKeyrings,\n );\n vaultJSON.salt = encryptionSalt;\n updatedState.vault = JSON.stringify(vaultJSON);\n } else if (this.#password) {\n const { vault: newVault, exportedKeyString } =\n await this.#encryptor.encryptWithDetail(\n this.#password,\n serializedKeyrings,\n );\n\n updatedState.vault = newVault;\n updatedState.encryptionKey = exportedKeyString;\n }\n } else {\n assertIsValidPassword(this.#password);\n updatedState.vault = await this.#encryptor.encrypt(\n this.#password,\n serializedKeyrings,\n );\n }\n\n if (!updatedState.vault) {\n throw new Error(KeyringControllerError.MissingVaultData);\n }\n\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n this.update((state) => {\n state.vault = updatedState.vault;\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = JSON.parse(updatedState.vault as string).salt;\n }\n });\n\n return true;\n });\n }\n\n /**\n * Retrieves all the accounts from keyrings instances\n * that are currently in memory.\n *\n * @returns A promise resolving to an array of accounts.\n */\n async #getAccountsFromKeyrings(): Promise {\n const keyrings = this.#keyrings;\n\n const keyringArrays = await Promise.all(\n keyrings.map(async (keyring) => keyring.getAccounts()),\n );\n const addresses = keyringArrays.reduce((res, arr) => {\n return res.concat(arr);\n }, []);\n\n // Cast to `string[]` here is safe here because `addresses` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n return addresses.map(normalize) as string[];\n }\n\n /**\n * Create a new keyring, ensuring that the first account is\n * also created.\n *\n * @param type - Keyring type to instantiate.\n * @param opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves if the operation is successful.\n */\n async #createKeyringWithFirstAccount(type: string, opts?: unknown) {\n this.#assertControllerMutexIsLocked();\n\n const keyring = (await this.#newKeyring(type, opts)) as EthKeyring;\n\n const [firstAccount] = await keyring.getAccounts();\n if (!firstAccount) {\n throw new Error(KeyringControllerError.NoFirstAccount);\n }\n }\n\n /**\n * Instantiate, initialize and return a new keyring of the given `type`,\n * using the given `opts`. The keyring is built using the keyring builder\n * registered for the given `type`.\n *\n *\n * @param type - The type of keyring to add.\n * @param data - The data to restore a previously serialized keyring.\n * @returns The new keyring.\n * @throws If the keyring includes duplicated accounts.\n */\n async #newKeyring(type: string, data?: unknown): Promise> {\n this.#assertControllerMutexIsLocked();\n\n const keyringBuilder = this.#getKeyringBuilderForType(type);\n\n if (!keyringBuilder) {\n throw new Error(\n `${KeyringControllerError.NoKeyringBuilder}. Keyring type: ${type}`,\n );\n }\n\n const keyring = keyringBuilder();\n\n // @ts-expect-error Enforce data type after updating clients\n await keyring.deserialize(data);\n\n if (keyring.init) {\n await keyring.init();\n }\n\n if (type === KeyringTypes.hd && (!isObject(data) || !data.mnemonic)) {\n if (!keyring.generateRandomMnemonic) {\n throw new Error(\n KeyringControllerError.UnsupportedGenerateRandomMnemonic,\n );\n }\n\n keyring.generateRandomMnemonic();\n await keyring.addAccounts(1);\n }\n\n await this.#checkForDuplicate(type, await keyring.getAccounts());\n\n if (type === KeyringTypes.qr) {\n // In case of a QR keyring type, we need to subscribe\n // to its events after creating it\n this.#subscribeToQRKeyringEvents(keyring as unknown as QRKeyring);\n }\n\n this.#keyrings.push(keyring);\n\n return keyring;\n }\n\n /**\n * Remove all managed keyrings, destroying all their\n * instances in memory.\n */\n async #clearKeyrings() {\n this.#assertControllerMutexIsLocked();\n for (const keyring of this.#keyrings) {\n await this.#destroyKeyring(keyring);\n }\n this.#keyrings = [];\n }\n\n /**\n * Restore a Keyring from a provided serialized payload.\n * On success, returns the resulting keyring instance.\n *\n * @param serialized - The serialized keyring.\n * @returns The deserialized keyring or undefined if the keyring type is unsupported.\n */\n async #restoreKeyring(\n serialized: SerializedKeyring,\n ): Promise | undefined> {\n this.#assertControllerMutexIsLocked();\n\n try {\n const { type, data } = serialized;\n return await this.#newKeyring(type, data);\n } catch (_) {\n this.#unsupportedKeyrings.push(serialized);\n return undefined;\n }\n }\n\n /**\n * Destroy Keyring\n *\n * Some keyrings support a method called `destroy`, that destroys the\n * keyring along with removing all its event listeners and, in some cases,\n * clears the keyring bridge iframe from the DOM.\n *\n * @param keyring - The keyring to destroy.\n */\n async #destroyKeyring(keyring: EthKeyring) {\n await keyring.destroy?.();\n }\n\n /**\n * Remove empty keyrings.\n *\n * Loops through the keyrings and removes the ones with empty accounts\n * (usually after removing the last / only account) from a keyring.\n */\n async #removeEmptyKeyrings(): Promise {\n this.#assertControllerMutexIsLocked();\n const validKeyrings: EthKeyring[] = [];\n\n // Since getAccounts returns a Promise\n // We need to wait to hear back form each keyring\n // in order to decide which ones are now valid (accounts.length > 0)\n\n await Promise.all(\n this.#keyrings.map(async (keyring: EthKeyring) => {\n const accounts = await keyring.getAccounts();\n if (accounts.length > 0) {\n validKeyrings.push(keyring);\n } else {\n await this.#destroyKeyring(keyring);\n }\n }),\n );\n this.#keyrings = validKeyrings;\n }\n\n /**\n * Checks for duplicate keypairs, using the the first account in the given\n * array. Rejects if a duplicate is found.\n *\n * Only supports 'Simple Key Pair'.\n *\n * @param type - The key pair type to check for.\n * @param newAccountArray - Array of new accounts.\n * @returns The account, if no duplicate is found.\n */\n async #checkForDuplicate(\n type: string,\n newAccountArray: string[],\n ): Promise {\n const accounts = await this.#getAccountsFromKeyrings();\n\n switch (type) {\n case KeyringTypes.simple: {\n const isIncluded = Boolean(\n accounts.find(\n (key) =>\n newAccountArray[0] &&\n (key === newAccountArray[0] ||\n key === remove0x(newAccountArray[0])),\n ),\n );\n\n if (isIncluded) {\n throw new Error(KeyringControllerError.DuplicatedAccount);\n }\n return newAccountArray;\n }\n\n default: {\n return newAccountArray;\n }\n }\n }\n\n /**\n * Set the `isUnlocked` to true and notify listeners\n * through the messenger.\n *\n * @fires KeyringController:unlock\n */\n #setUnlocked(): void {\n this.#assertControllerMutexIsLocked();\n\n this.update((state) => {\n state.isUnlocked = true;\n });\n this.messagingSystem.publish(`${name}:unlock`);\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and save the keyrings to state after it, or rollback to their\n * previous state in case of error.\n *\n * @param fn - The function to execute.\n * @returns The result of the function.\n */\n async #persistOrRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withRollback(async ({ releaseLock }) => {\n const callbackResult = await fn({ releaseLock });\n // State is committed only if the operation is successful\n await this.#updateVault();\n\n return callbackResult;\n });\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and rollback keyrings and password states in case of error.\n *\n * @param fn - The function to execute atomically.\n * @returns The result of the function.\n */\n async #withRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withControllerLock(async ({ releaseLock }) => {\n const currentSerializedKeyrings = await this.#getSerializedKeyrings();\n const currentPassword = this.#password;\n\n try {\n return await fn({ releaseLock });\n } catch (e) {\n // Keyrings and password are restored to their previous state\n await this.#restoreSerializedKeyrings(currentSerializedKeyrings);\n this.#password = currentPassword;\n\n throw e;\n }\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(KeyringControllerError.ControllerLockRequired);\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param fn - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock(fn: MutuallyExclusiveCallback): Promise {\n return withLock(this.#controllerOperationMutex, fn);\n }\n\n /**\n * Lock the vault mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This ensures that each operation that interacts with the vault\n * is executed in a mutually exclusive way.\n *\n * @param fn - The function to execute while the vault mutex is locked.\n * @returns The result of the function.\n */\n async #withVaultLock(fn: MutuallyExclusiveCallback): Promise {\n this.#assertControllerMutexIsLocked();\n\n return withLock(this.#vaultOperationMutex, fn);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param fn - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock(\n mutex: Mutex,\n fn: MutuallyExclusiveCallback,\n): Promise {\n const releaseLock = await mutex.acquire();\n\n try {\n return await fn({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n\nexport default KeyringController;\n"]} -\ No newline at end of file -diff --git a/dist/chunk-L4UUWIZA.js b/dist/chunk-L4UUWIZA.js -new file mode 100644 -index 0000000000000000000000000000000000000000..3e85597548d0825ba3e1e7d938def8c630ba161a ---- /dev/null -+++ b/dist/chunk-L4UUWIZA.js -@@ -0,0 +1,1506 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -+ -+ -+ -+ -+var _chunkNOCGQCUMjs = require('./chunk-NOCGQCUM.js'); -+ -+// src/KeyringController.ts -+var _util = require('@ethereumjs/util'); -+var _basecontroller = require('@metamask/base-controller'); -+var _browserpassworder = require('@metamask/browser-passworder'); var encryptorUtils = _interopRequireWildcard(_browserpassworder); -+var _ethhdkeyring = require('@metamask/eth-hd-keyring'); var _ethhdkeyring2 = _interopRequireDefault(_ethhdkeyring); -+var _ethsigutil = require('@metamask/eth-sig-util'); -+var _ethsimplekeyring = require('@metamask/eth-simple-keyring'); var _ethsimplekeyring2 = _interopRequireDefault(_ethsimplekeyring); -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+var _utils = require('@metamask/utils'); -+var _asyncmutex = require('async-mutex'); -+var _ethereumjswallet = require('ethereumjs-wallet'); var _ethereumjswallet2 = _interopRequireDefault(_ethereumjswallet); -+var name = "KeyringController"; -+var KeyringTypes = /* @__PURE__ */ ((KeyringTypes2) => { -+ KeyringTypes2["simple"] = "Simple Key Pair"; -+ KeyringTypes2["hd"] = "HD Key Tree"; -+ KeyringTypes2["qr"] = "QR Hardware Wallet Device"; -+ KeyringTypes2["trezor"] = "Trezor Hardware"; -+ KeyringTypes2["ledger"] = "Ledger Hardware"; -+ KeyringTypes2["lattice"] = "Lattice Hardware"; -+ KeyringTypes2["snap"] = "Snap Keyring"; -+ return KeyringTypes2; -+})(KeyringTypes || {}); -+var isCustodyKeyring = (keyringType) => { -+ return keyringType.startsWith("Custody"); -+}; -+var AccountImportStrategy = /* @__PURE__ */ ((AccountImportStrategy2) => { -+ AccountImportStrategy2["privateKey"] = "privateKey"; -+ AccountImportStrategy2["json"] = "json"; -+ return AccountImportStrategy2; -+})(AccountImportStrategy || {}); -+var SignTypedDataVersion = /* @__PURE__ */ ((SignTypedDataVersion2) => { -+ SignTypedDataVersion2["V1"] = "V1"; -+ SignTypedDataVersion2["V3"] = "V3"; -+ SignTypedDataVersion2["V4"] = "V4"; -+ return SignTypedDataVersion2; -+})(SignTypedDataVersion || {}); -+function keyringBuilderFactory(KeyringConstructor) { -+ const builder = () => new KeyringConstructor(); -+ builder.type = KeyringConstructor.type; -+ return builder; -+} -+var defaultKeyringBuilders = [ -+ keyringBuilderFactory(_ethsimplekeyring2.default), -+ keyringBuilderFactory(_ethhdkeyring2.default) -+]; -+var getDefaultKeyringState = () => { -+ return { -+ isUnlocked: false, -+ keyrings: [] -+ }; -+}; -+function assertHasUint8ArrayMnemonic(keyring) { -+ if (!(_utils.hasProperty.call(void 0, keyring, "mnemonic") && keyring.mnemonic instanceof Uint8Array)) { -+ throw new Error("Can't get mnemonic bytes from keyring"); -+ } -+} -+function assertIsExportableKeyEncryptor(encryptor) { -+ if (!("importKey" in encryptor && typeof encryptor.importKey === "function" && "decryptWithKey" in encryptor && typeof encryptor.decryptWithKey === "function" && "encryptWithKey" in encryptor && typeof encryptor.encryptWithKey === "function")) { -+ throw new Error("KeyringController - The encryptor does not support encryption key export." /* UnsupportedEncryptionKeyExport */); -+ } -+} -+function assertIsValidPassword(password) { -+ if (typeof password !== "string") { -+ throw new Error("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ if (!password || !password.length) { -+ throw new Error("KeyringController - Password cannot be empty." /* InvalidEmptyPassword */); -+ } -+} -+function isSerializedKeyringsArray(array) { -+ return typeof array === "object" && Array.isArray(array) && array.every((value) => value.type && _utils.isValidJson.call(void 0, value.data)); -+} -+async function displayForKeyring(keyring) { -+ const accounts = await keyring.getAccounts(); -+ return { -+ type: keyring.type, -+ // Cast to `string[]` here is safe here because `accounts` has no nullish -+ // values, and `normalize` returns `string` unless given a nullish value -+ accounts: accounts.map(normalize) -+ }; -+} -+function isEthAddress(address) { -+ return ( -+ // NOTE: This function only checks for lowercased strings -+ _utils.isStrictHexString.call(void 0, address.toLowerCase()) && // This checks for lowercased addresses and checksum addresses too -+ _utils.isValidHexAddress.call(void 0, address) -+ ); -+} -+function normalize(address) { -+ return isEthAddress(address) ? _ethsigutil.normalize.call(void 0, address) : address; -+} -+var _controllerOperationMutex, _vaultOperationMutex, _keyringBuilders, _keyrings, _unsupportedKeyrings, _password, _encryptor, _cacheEncryptionKey, _qrKeyringStateListener, _registerMessageHandlers, registerMessageHandlers_fn, _getKeyringBuilderForType, getKeyringBuilderForType_fn, _addQRKeyring, addQRKeyring_fn, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn, _getUpdatedKeyrings, getUpdatedKeyrings_fn, _getSerializedKeyrings, getSerializedKeyrings_fn, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn, _unlockKeyrings, unlockKeyrings_fn, _updateVault, updateVault_fn, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn, _newKeyring, newKeyring_fn, _clearKeyrings, clearKeyrings_fn, _restoreKeyring, restoreKeyring_fn, _destroyKeyring, destroyKeyring_fn, _removeEmptyKeyrings, removeEmptyKeyrings_fn, _checkForDuplicate, checkForDuplicate_fn, _setUnlocked, setUnlocked_fn, _persistOrRollback, persistOrRollback_fn, _withRollback, withRollback_fn, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn, _withControllerLock, withControllerLock_fn, _withVaultLock, withVaultLock_fn; -+var KeyringController = class extends _basecontroller.BaseController { -+ /** -+ * Creates a KeyringController instance. -+ * -+ * @param options - Initial options used to configure this controller -+ * @param options.encryptor - An optional object for defining encryption schemes. -+ * @param options.keyringBuilders - Set a new name for account. -+ * @param options.cacheEncryptionKey - Whether to cache or not encryption key. -+ * @param options.messenger - A restricted controller messenger. -+ * @param options.state - Initial state to set on this controller. -+ */ -+ constructor(options) { -+ const { -+ encryptor = encryptorUtils, -+ keyringBuilders, -+ messenger, -+ state -+ } = options; -+ super({ -+ name, -+ metadata: { -+ vault: { persist: true, anonymous: false }, -+ isUnlocked: { persist: false, anonymous: true }, -+ keyrings: { persist: false, anonymous: false }, -+ encryptionKey: { persist: false, anonymous: false }, -+ encryptionSalt: { persist: false, anonymous: false } -+ }, -+ messenger, -+ state: { -+ ...getDefaultKeyringState(), -+ ...state -+ } -+ }); -+ /** -+ * Constructor helper for registering this controller's messaging system -+ * actions. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _registerMessageHandlers); -+ /** -+ * Get the keyring builder for the given `type`. -+ * -+ * @param type - The type of keyring to get the builder for. -+ * @returns The keyring builder, or undefined if none exists. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getKeyringBuilderForType); -+ /** -+ * Add qr hardware keyring. -+ * -+ * @returns The added keyring -+ * @throws If a QRKeyring builder is not provided -+ * when initializing the controller -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _addQRKeyring); -+ /** -+ * Subscribe to a QRKeyring state change events and -+ * forward them through the messaging system. -+ * -+ * @param qrKeyring - The QRKeyring instance to subscribe to -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _subscribeToQRKeyringEvents); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unsubscribeFromQRKeyringsEvents); -+ /** -+ * Create new vault with an initial keyring -+ * -+ * Destroys any old encrypted storage, -+ * creates a new encrypted store with the given password, -+ * creates a new wallet with 1 account. -+ * -+ * @fires KeyringController:unlock -+ * @param password - The password to encrypt the vault with. -+ * @param keyring - A object containing the params to instantiate a new keyring. -+ * @param keyring.type - The keyring type. -+ * @param keyring.opts - Optional parameters required to instantiate the keyring. -+ * @returns A promise that resolves to the state. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _createNewVaultWithKeyring); -+ /** -+ * Get the updated array of each keyring's type and -+ * accounts list. -+ * -+ * @returns A promise resolving to the updated keyrings array. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getUpdatedKeyrings); -+ /** -+ * Serialize the current array of keyring instances, -+ * including unsupported keyrings by default. -+ * -+ * @param options - Method options. -+ * @param options.includeUnsupported - Whether to include unsupported keyrings. -+ * @returns The serialized keyrings. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getSerializedKeyrings); -+ /** -+ * Restore a serialized keyrings array. -+ * -+ * @param serializedKeyrings - The serialized keyrings array. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _restoreSerializedKeyrings); -+ /** -+ * Unlock Keyrings, decrypting the vault and deserializing all -+ * keyrings contained in it, using a password or an encryption key with salt. -+ * -+ * @param password - The keyring controller password. -+ * @param encryptionKey - An exported key string to unlock keyrings with. -+ * @param encryptionSalt - The salt used to encrypt the vault. -+ * @returns A promise resolving to the deserialized keyrings array. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unlockKeyrings); -+ /** -+ * Update the vault with the current keyrings. -+ * -+ * @returns A promise resolving to `true` if the operation is successful. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _updateVault); -+ /** -+ * Retrieves all the accounts from keyrings instances -+ * that are currently in memory. -+ * -+ * @returns A promise resolving to an array of accounts. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _getAccountsFromKeyrings); -+ /** -+ * Create a new keyring, ensuring that the first account is -+ * also created. -+ * -+ * @param type - Keyring type to instantiate. -+ * @param opts - Optional parameters required to instantiate the keyring. -+ * @returns A promise that resolves if the operation is successful. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _createKeyringWithFirstAccount); -+ /** -+ * Instantiate, initialize and return a new keyring of the given `type`, -+ * using the given `opts`. The keyring is built using the keyring builder -+ * registered for the given `type`. -+ * -+ * -+ * @param type - The type of keyring to add. -+ * @param data - The data to restore a previously serialized keyring. -+ * @returns The new keyring. -+ * @throws If the keyring includes duplicated accounts. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _newKeyring); -+ /** -+ * Remove all managed keyrings, destroying all their -+ * instances in memory. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _clearKeyrings); -+ /** -+ * Restore a Keyring from a provided serialized payload. -+ * On success, returns the resulting keyring instance. -+ * -+ * @param serialized - The serialized keyring. -+ * @returns The deserialized keyring or undefined if the keyring type is unsupported. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _restoreKeyring); -+ /** -+ * Destroy Keyring -+ * -+ * Some keyrings support a method called `destroy`, that destroys the -+ * keyring along with removing all its event listeners and, in some cases, -+ * clears the keyring bridge iframe from the DOM. -+ * -+ * @param keyring - The keyring to destroy. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _destroyKeyring); -+ /** -+ * Remove empty keyrings. -+ * -+ * Loops through the keyrings and removes the ones with empty accounts -+ * (usually after removing the last / only account) from a keyring. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _removeEmptyKeyrings); -+ /** -+ * Checks for duplicate keypairs, using the the first account in the given -+ * array. Rejects if a duplicate is found. -+ * -+ * Only supports 'Simple Key Pair'. -+ * -+ * @param type - The key pair type to check for. -+ * @param newAccountArray - Array of new accounts. -+ * @returns The account, if no duplicate is found. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _checkForDuplicate); -+ /** -+ * Set the `isUnlocked` to true and notify listeners -+ * through the messenger. -+ * -+ * @fires KeyringController:unlock -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _setUnlocked); -+ /** -+ * Execute the given function after acquiring the controller lock -+ * and save the keyrings to state after it, or rollback to their -+ * previous state in case of error. -+ * -+ * @param fn - The function to execute. -+ * @returns The result of the function. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _persistOrRollback); -+ /** -+ * Execute the given function after acquiring the controller lock -+ * and rollback keyrings and password states in case of error. -+ * -+ * @param fn - The function to execute atomically. -+ * @returns The result of the function. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withRollback); -+ /** -+ * Assert that the controller mutex is locked. -+ * -+ * @throws If the controller mutex is not locked. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _assertControllerMutexIsLocked); -+ /** -+ * Lock the controller mutex before executing the given function, -+ * and release it after the function is resolved or after an -+ * error is thrown. -+ * -+ * This wrapper ensures that each mutable operation that interacts with the -+ * controller and that changes its state is executed in a mutually exclusive way, -+ * preventing unsafe concurrent access that could lead to unpredictable behavior. -+ * -+ * @param fn - The function to execute while the controller mutex is locked. -+ * @returns The result of the function. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withControllerLock); -+ /** -+ * Lock the vault mutex before executing the given function, -+ * and release it after the function is resolved or after an -+ * error is thrown. -+ * -+ * This ensures that each operation that interacts with the vault -+ * is executed in a mutually exclusive way. -+ * -+ * @param fn - The function to execute while the vault mutex is locked. -+ * @returns The result of the function. -+ */ -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _withVaultLock); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _controllerOperationMutex, new (0, _asyncmutex.Mutex)()); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _vaultOperationMutex, new (0, _asyncmutex.Mutex)()); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _keyringBuilders, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _keyrings, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _unsupportedKeyrings, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _password, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _encryptor, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _cacheEncryptionKey, void 0); -+ _chunkNOCGQCUMjs.__privateAdd.call(void 0, this, _qrKeyringStateListener, void 0); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyringBuilders, keyringBuilders ? defaultKeyringBuilders.concat(keyringBuilders) : defaultKeyringBuilders); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _encryptor, encryptor); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, []); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _unsupportedKeyrings, []); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _cacheEncryptionKey, Boolean(options.cacheEncryptionKey)); -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(encryptor); -+ } -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -+ } -+ /** -+ * Adds a new account to the default (first) HD seed phrase keyring. -+ * -+ * @param accountCount - Number of accounts before adding a new one, used to -+ * make the method idempotent. -+ * @returns Promise resolving to the added account address. -+ */ -+ async addNewAccount(accountCount) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found"); -+ } -+ const oldAccounts = await primaryKeyring.getAccounts(); -+ if (accountCount && oldAccounts.length !== accountCount) { -+ if (accountCount > oldAccounts.length) { -+ throw new Error("Account out of sequence"); -+ } -+ const existingAccount = oldAccounts[accountCount]; -+ if (!existingAccount) { -+ throw new Error(`Can't find account at index ${accountCount}`); -+ } -+ return existingAccount; -+ } -+ const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -+ await this.verifySeedPhrase(); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Adds a new account to the specified keyring. -+ * -+ * @param keyring - Keyring to add the account to. -+ * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent. -+ * @returns Promise resolving to the added account address -+ */ -+ async addNewAccountForKeyring(keyring, accountCount) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const oldAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ if (accountCount && oldAccounts.length !== accountCount) { -+ if (accountCount > oldAccounts.length) { -+ throw new Error("Account out of sequence"); -+ } -+ const existingAccount = oldAccounts[accountCount]; -+ _utils.assertIsStrictHexString.call(void 0, existingAccount); -+ return existingAccount; -+ } -+ await keyring.addAccounts(1); -+ const addedAccountAddress = (await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this)).find( -+ (selectedAddress) => !oldAccounts.includes(selectedAddress) -+ ); -+ _utils.assertIsStrictHexString.call(void 0, addedAccountAddress); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences. -+ * -+ * @returns Promise resolving to the added account address. -+ */ -+ async addNewAccountWithoutUpdate() { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found"); -+ } -+ const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -+ await this.verifySeedPhrase(); -+ return addedAccountAddress; -+ }); -+ } -+ /** -+ * Effectively the same as creating a new keychain then populating it -+ * using the given seed phrase. -+ * -+ * @param password - Password to unlock keychain. -+ * @param seed - A BIP39-compliant seed phrase as Uint8Array, -+ * either as a string or an array of UTF-8 bytes that represent the string. -+ * @returns Promise resolving when the operation ends successfully. -+ */ -+ async createNewVaultAndRestore(password, seed) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ assertIsValidPassword(password); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -+ type: "HD Key Tree" /* hd */, -+ opts: { -+ mnemonic: seed, -+ numberOfAccounts: 1 -+ } -+ }); -+ }); -+ } -+ /** -+ * Create a new primary keychain and wipe any previous keychains. -+ * -+ * @param password - Password to unlock the new vault. -+ * @returns Promise resolving when the operation ends successfully. -+ */ -+ async createNewVaultAndKeychain(password) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const accounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ if (!accounts.length) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -+ type: "HD Key Tree" /* hd */ -+ }); -+ } -+ }); -+ } -+ /** -+ * Adds a new keyring of the given `type`. -+ * -+ * @param type - Keyring type name. -+ * @param opts - Keyring options. -+ * @throws If a builder for the given `type` does not exist. -+ * @returns Promise resolving to the added keyring. -+ */ -+ async addNewKeyring(type, opts) { -+ if (type === "QR Hardware Wallet Device" /* qr */) { -+ return this.getOrAddQRKeyring(); -+ } -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, opts)); -+ } -+ /** -+ * Method to verify a given password validity. Throws an -+ * error if the password is invalid. -+ * -+ * @param password - Password of the keyring. -+ */ -+ async verifyPassword(password) { -+ if (!this.state.vault) { -+ throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -+ } -+ await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decrypt(password, this.state.vault); -+ } -+ /** -+ * Returns the status of the vault. -+ * -+ * @returns Boolean returning true if the vault is unlocked. -+ */ -+ isUnlocked() { -+ return this.state.isUnlocked; -+ } -+ /** -+ * Gets the seed phrase of the HD keyring. -+ * -+ * @param password - Password of the keyring. -+ * @returns Promise resolving to the seed phrase. -+ */ -+ async exportSeedPhrase(password) { -+ await this.verifyPassword(password); -+ assertHasUint8ArrayMnemonic(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)[0]); -+ return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)[0].mnemonic; -+ } -+ /** -+ * Gets the private key from the keyring controlling an address. -+ * -+ * @param password - Password of the keyring. -+ * @param address - Address to export. -+ * @returns Promise resolving to the private key for an address. -+ */ -+ async exportAccount(password, address) { -+ await this.verifyPassword(password); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.exportAccount) { -+ throw new Error("`KeyringController - The keyring for the current address does not support the method exportAccount" /* UnsupportedExportAccount */); -+ } -+ return await keyring.exportAccount(normalize(address)); -+ } -+ /** -+ * Returns the public addresses of all accounts from every keyring. -+ * -+ * @returns A promise resolving to an array of addresses. -+ */ -+ async getAccounts() { -+ return this.state.keyrings.reduce( -+ (accounts, keyring) => accounts.concat(keyring.accounts), -+ [] -+ ); -+ } -+ /** -+ * Get encryption public key. -+ * -+ * @param account - An account address. -+ * @param opts - Additional encryption options. -+ * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method -+ * @returns Promise resolving to encyption public key of the `account` if one exists. -+ */ -+ async getEncryptionPublicKey(account, opts) { -+ const address = _ethsigutil.normalize.call(void 0, account); -+ const keyring = await this.getKeyringForAccount( -+ account -+ ); -+ if (!keyring.getEncryptionPublicKey) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey." /* UnsupportedGetEncryptionPublicKey */); -+ } -+ return await keyring.getEncryptionPublicKey(address, opts); -+ } -+ /** -+ * Attempts to decrypt the provided message parameters. -+ * -+ * @param messageParams - The decryption message parameters. -+ * @param messageParams.from - The address of the account you want to use to decrypt the message. -+ * @param messageParams.data - The encrypted data that you want to decrypt. -+ * @returns The raw decryption result. -+ */ -+ async decryptMessage(messageParams) { -+ const address = _ethsigutil.normalize.call(void 0, messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.decryptMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method decryptMessage." /* UnsupportedDecryptMessage */); -+ } -+ return keyring.decryptMessage(address, messageParams.data); -+ } -+ /** -+ * Returns the currently initialized keyring that manages -+ * the specified `address` if one exists. -+ * -+ * @deprecated Use of this method is discouraged as actions executed directly on -+ * keyrings are not being reflected in the KeyringController state and not -+ * persisted in the vault. Use `withKeyring` instead. -+ * @param account - An account address. -+ * @returns Promise resolving to keyring of the `account` if one exists. -+ */ -+ async getKeyringForAccount(account) { -+ const address = normalize(account); -+ const candidates = await Promise.all( -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -+ return Promise.all([keyring, keyring.getAccounts()]); -+ }) -+ ); -+ const winners = candidates.filter((candidate) => { -+ const accounts = candidate[1].map(normalize); -+ return accounts.includes(address); -+ }); -+ if (winners.length && winners[0]?.length) { -+ return winners[0][0]; -+ } -+ let errorInfo = ""; -+ if (!candidates.length) { -+ errorInfo = "There are no keyrings"; -+ } else if (!winners.length) { -+ errorInfo = "There are keyrings, but none match the address"; -+ } -+ throw new Error( -+ `${"KeyringController - No keyring found" /* NoKeyring */}. Error info: ${errorInfo}` -+ ); -+ } -+ /** -+ * Returns all keyrings of the given type. -+ * -+ * @deprecated Use of this method is discouraged as actions executed directly on -+ * keyrings are not being reflected in the KeyringController state and not -+ * persisted in the vault. Use `withKeyring` instead. -+ * @param type - Keyring type name. -+ * @returns An array of keyrings of the given type. -+ */ -+ getKeyringsByType(type) { -+ return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).filter((keyring) => keyring.type === type); -+ } -+ /** -+ * Persist all serialized keyrings in the vault. -+ * -+ * @deprecated This method is being phased out in favor of `withKeyring`. -+ * @returns Promise resolving with `true` value when the -+ * operation completes. -+ */ -+ async persistAllKeyrings() { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => true); -+ } -+ /** -+ * Imports an account with the specified import strategy. -+ * -+ * @param strategy - Import strategy name. -+ * @param args - Array of arguments to pass to the underlying stategy. -+ * @throws Will throw when passed an unrecognized strategy. -+ * @returns Promise resolving to the imported account address. -+ */ -+ async importAccountWithStrategy(strategy, args) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ let privateKey; -+ switch (strategy) { -+ case "privateKey": -+ const [importedKey] = args; -+ if (!importedKey) { -+ throw new Error("Cannot import an empty key."); -+ } -+ const prefixed = _utils.add0x.call(void 0, importedKey); -+ let bufferedPrivateKey; -+ try { -+ bufferedPrivateKey = _util.toBuffer.call(void 0, prefixed); -+ } catch { -+ throw new Error("Cannot import invalid private key."); -+ } -+ if (!_util.isValidPrivate.call(void 0, bufferedPrivateKey) || // ensures that the key is 64 bytes long -+ _util.getBinarySize.call(void 0, prefixed) !== 64 + "0x".length) { -+ throw new Error("Cannot import invalid private key."); -+ } -+ privateKey = _utils.remove0x.call(void 0, prefixed); -+ break; -+ case "json": -+ let wallet; -+ const [input, password] = args; -+ try { -+ wallet = _ethereumjswallet.thirdparty.fromEtherWallet(input, password); -+ } catch (e) { -+ wallet = wallet || await _ethereumjswallet2.default.fromV3(input, password, true); -+ } -+ privateKey = _utils.bytesToHex.call(void 0, wallet.getPrivateKey()); -+ break; -+ default: -+ throw new Error(`Unexpected import strategy: '${strategy}'`); -+ } -+ const newKeyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "Simple Key Pair" /* simple */, [ -+ privateKey -+ ]); -+ const accounts = await newKeyring.getAccounts(); -+ return accounts[0]; -+ }); -+ } -+ /** -+ * Removes an account from keyring state. -+ * -+ * @param address - Address of the account to remove. -+ * @fires KeyringController:accountRemoved -+ * @returns Promise resolving when the account is removed. -+ */ -+ async removeAccount(address) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.removeAccount) { -+ throw new Error("`KeyringController - The keyring for the current address does not support the method removeAccount" /* UnsupportedRemoveAccount */); -+ } -+ await keyring.removeAccount(address); -+ const accounts = await keyring.getAccounts(); -+ if (accounts.length === 0) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _removeEmptyKeyrings, removeEmptyKeyrings_fn).call(this); -+ } -+ }); -+ this.messagingSystem.publish(`${name}:accountRemoved`, address); -+ } -+ /** -+ * Deallocates all secrets and locks the wallet. -+ * -+ * @returns Promise resolving when the operation completes. -+ */ -+ async setLocked() { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn).call(this); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, void 0); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -+ this.update((state) => { -+ state.isUnlocked = false; -+ state.keyrings = []; -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ this.messagingSystem.publish(`${name}:lock`); -+ }); -+ } -+ /** -+ * Signs message by calling down into a specific keyring. -+ * -+ * @param messageParams - PersonalMessageParams object to sign. -+ * @returns Promise resolving to a signed message string. -+ */ -+ async signMessage(messageParams) { -+ if (!messageParams.data) { -+ throw new Error("Can't sign an empty message"); -+ } -+ const address = _ethsigutil.normalize.call(void 0, messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signMessage." /* UnsupportedSignMessage */); -+ } -+ return await keyring.signMessage(address, messageParams.data); -+ } -+ /** -+ * Signs personal message by calling down into a specific keyring. -+ * -+ * @param messageParams - PersonalMessageParams object to sign. -+ * @returns Promise resolving to a signed message string. -+ */ -+ async signPersonalMessage(messageParams) { -+ const address = _ethsigutil.normalize.call(void 0, messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signPersonalMessage) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signPersonalMessage." /* UnsupportedSignPersonalMessage */); -+ } -+ const normalizedData = normalize(messageParams.data); -+ return await keyring.signPersonalMessage(address, normalizedData); -+ } -+ /** -+ * Signs typed message by calling down into a specific keyring. -+ * -+ * @param messageParams - TypedMessageParams object to sign. -+ * @param version - Compatibility version EIP712. -+ * @throws Will throw when passed an unrecognized version. -+ * @returns Promise resolving to a signed message string or an error if any. -+ */ -+ async signTypedMessage(messageParams, version) { -+ try { -+ if (![ -+ "V1" /* V1 */, -+ "V3" /* V3 */, -+ "V4" /* V4 */ -+ ].includes(version)) { -+ throw new Error(`Unexpected signTypedMessage version: '${version}'`); -+ } -+ const address = _ethsigutil.normalize.call(void 0, messageParams.from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signTypedData) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signTypedMessage." /* UnsupportedSignTypedMessage */); -+ } -+ return await keyring.signTypedData( -+ address, -+ version !== "V1" /* V1 */ && typeof messageParams.data === "string" ? JSON.parse(messageParams.data) : messageParams.data, -+ { version } -+ ); -+ } catch (error) { -+ throw new Error(`Keyring Controller signTypedMessage: ${error}`); -+ } -+ } -+ /** -+ * Signs a transaction by calling down into a specific keyring. -+ * -+ * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance. -+ * @param from - Address to sign from, should be in keychain. -+ * @param opts - An optional options object. -+ * @returns Promise resolving to a signed transaction string. -+ */ -+ async signTransaction(transaction, from, opts) { -+ const address = _ethsigutil.normalize.call(void 0, from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signTransaction) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signTransaction." /* UnsupportedSignTransaction */); -+ } -+ return await keyring.signTransaction(address, transaction, opts); -+ } -+ /** -+ * Convert a base transaction to a base UserOperation. -+ * -+ * @param from - Address of the sender. -+ * @param transactions - Base transactions to include in the UserOperation. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns A pseudo-UserOperation that can be used to construct a real. -+ */ -+ async prepareUserOperation(from, transactions, executionContext) { -+ const address = _ethsigutil.normalize.call(void 0, from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.prepareUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method prepareUserOperation." /* UnsupportedPrepareUserOperation */); -+ } -+ return await keyring.prepareUserOperation( -+ address, -+ transactions, -+ executionContext -+ ); -+ } -+ /** -+ * Patches properties of a UserOperation. Currently, only the -+ * `paymasterAndData` can be patched. -+ * -+ * @param from - Address of the sender. -+ * @param userOp - UserOperation to patch. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns A patch to apply to the UserOperation. -+ */ -+ async patchUserOperation(from, userOp, executionContext) { -+ const address = _ethsigutil.normalize.call(void 0, from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.patchUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method patchUserOperation." /* UnsupportedPatchUserOperation */); -+ } -+ return await keyring.patchUserOperation(address, userOp, executionContext); -+ } -+ /** -+ * Signs an UserOperation. -+ * -+ * @param from - Address of the sender. -+ * @param userOp - UserOperation to sign. -+ * @param executionContext - The execution context to use for the UserOperation. -+ * @returns The signature of the UserOperation. -+ */ -+ async signUserOperation(from, userOp, executionContext) { -+ const address = _ethsigutil.normalize.call(void 0, from); -+ const keyring = await this.getKeyringForAccount( -+ address -+ ); -+ if (!keyring.signUserOperation) { -+ throw new Error("KeyringController - The keyring for the current address does not support the method signUserOperation." /* UnsupportedSignUserOperation */); -+ } -+ return await keyring.signUserOperation(address, userOp, executionContext); -+ } -+ /** -+ * Changes the password used to encrypt the vault. -+ * -+ * @param password - The new password. -+ * @returns Promise resolving when the operation completes. -+ */ -+ changePassword(password) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ if (!this.state.isUnlocked) { -+ throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -+ } -+ assertIsValidPassword(password); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -+ this.update((state) => { -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ } -+ }); -+ } -+ /** -+ * Attempts to decrypt the current vault and load its keyrings, -+ * using the given encryption key and salt. -+ * -+ * @param encryptionKey - Key to unlock the keychain. -+ * @param encryptionSalt - Salt to unlock the keychain. -+ * @returns Promise resolving when the operation completes. -+ */ -+ async submitEncryptionKey(encryptionKey, encryptionSalt) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unlockKeyrings, unlockKeyrings_fn).call(this, void 0, encryptionKey, encryptionSalt)); -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); -+ }); -+ } -+ /** -+ * Attempts to decrypt the current vault and load its keyrings, -+ * using the given password. -+ * -+ * @param password - Password to unlock the keychain. -+ * @returns Promise resolving when the operation completes. -+ */ -+ async submitPassword(password) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async () => { -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _unlockKeyrings, unlockKeyrings_fn).call(this, password)); -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); -+ }); -+ } -+ /** -+ * Verifies the that the seed phrase restores the current keychain's accounts. -+ * -+ * @returns Promise resolving to the seed phrase as Uint8Array. -+ */ -+ async verifySeedPhrase() { -+ const primaryKeyring = this.getKeyringsByType("HD Key Tree" /* hd */)[0]; -+ if (!primaryKeyring) { -+ throw new Error("No HD keyring found."); -+ } -+ assertHasUint8ArrayMnemonic(primaryKeyring); -+ const seedWords = primaryKeyring.mnemonic; -+ const accounts = await primaryKeyring.getAccounts(); -+ if (accounts.length === 0) { -+ throw new Error("Cannot verify an empty keyring."); -+ } -+ const hdKeyringBuilder = _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, "HD Key Tree" /* hd */); -+ const hdKeyring = hdKeyringBuilder(); -+ await hdKeyring.deserialize({ -+ mnemonic: seedWords, -+ numberOfAccounts: accounts.length -+ }); -+ const testAccounts = await hdKeyring.getAccounts(); -+ if (testAccounts.length !== accounts.length) { -+ throw new Error("Seed phrase imported incorrect number of accounts."); -+ } -+ testAccounts.forEach((account, i) => { -+ if (account.toLowerCase() !== accounts[i].toLowerCase()) { -+ throw new Error("Seed phrase imported different accounts."); -+ } -+ }); -+ return seedWords; -+ } -+ async withKeyring(selector, operation, options = { -+ createIfMissing: false -+ }) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ let keyring; -+ if ("address" in selector) { -+ keyring = await this.getKeyringForAccount(selector.address); -+ } else { -+ keyring = this.getKeyringsByType(selector.type)[selector.index || 0]; -+ if (!keyring && options.createIfMissing) { -+ keyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, selector.type, options.createWithData); -+ } -+ } -+ if (!keyring) { -+ throw new Error("KeyringController - Keyring not found." /* KeyringNotFound */); -+ } -+ const result = await operation(keyring); -+ if (Object.is(result, keyring)) { -+ throw new Error("KeyringController - Returning keyring instances is unsafe" /* UnsafeDirectKeyringAccess */); -+ } -+ return result; -+ }); -+ } -+ // QR Hardware related methods -+ /** -+ * Get QR Hardware keyring. -+ * -+ * @returns The QR Keyring if defined, otherwise undefined -+ */ -+ getQRKeyring() { -+ return this.getKeyringsByType("QR Hardware Wallet Device" /* qr */)[0]; -+ } -+ /** -+ * Get QR hardware keyring. If it doesn't exist, add it. -+ * -+ * @returns The added keyring -+ */ -+ async getOrAddQRKeyring() { -+ return this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this)); -+ } -+ // TODO: Replace `any` with type -+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -+ async restoreQRKeyring(serialized) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -+ keyring.deserialize(serialized); -+ }); -+ } -+ async resetQRKeyringState() { -+ (await this.getOrAddQRKeyring()).resetStore(); -+ } -+ async getQRKeyringState() { -+ return (await this.getOrAddQRKeyring()).getMemStore(); -+ } -+ async submitQRCryptoHDKey(cryptoHDKey) { -+ (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey); -+ } -+ async submitQRCryptoAccount(cryptoAccount) { -+ (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount); -+ } -+ async submitQRSignature(requestId, ethSignature) { -+ (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature); -+ } -+ async cancelQRSignRequest() { -+ (await this.getOrAddQRKeyring()).cancelSignRequest(); -+ } -+ /** -+ * Cancels qr keyring sync. -+ */ -+ async cancelQRSynchronization() { -+ (await this.getOrAddQRKeyring()).cancelSync(); -+ } -+ async connectQRHardware(page) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ try { -+ const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -+ let accounts; -+ switch (page) { -+ case -1: -+ accounts = await keyring.getPreviousPage(); -+ break; -+ case 1: -+ accounts = await keyring.getNextPage(); -+ break; -+ default: -+ accounts = await keyring.getFirstPage(); -+ } -+ return accounts.map((account) => { -+ return { -+ ...account, -+ balance: "0x0" -+ }; -+ }); -+ } catch (e) { -+ throw new Error(`Unspecified error when connect QR Hardware, ${e}`); -+ } -+ }); -+ } -+ async unlockQRHardwareWalletAccount(index) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring() || await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _addQRKeyring, addQRKeyring_fn).call(this); -+ keyring.setAccountToUnlock(index); -+ await keyring.addAccounts(1); -+ }); -+ } -+ async getAccountKeyringType(account) { -+ const keyring = await this.getKeyringForAccount( -+ account -+ ); -+ return keyring.type; -+ } -+ async forgetQRDevice() { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -+ const keyring = this.getQRKeyring(); -+ if (!keyring) { -+ return { removedAccounts: [], remainingAccounts: [] }; -+ } -+ const allAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ keyring.forgetDevice(); -+ const remainingAccounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ const removedAccounts = allAccounts.filter( -+ (address) => !remainingAccounts.includes(address) -+ ); -+ return { removedAccounts, remainingAccounts }; -+ }); -+ } -+}; -+_controllerOperationMutex = new WeakMap(); -+_vaultOperationMutex = new WeakMap(); -+_keyringBuilders = new WeakMap(); -+_keyrings = new WeakMap(); -+_unsupportedKeyrings = new WeakMap(); -+_password = new WeakMap(); -+_encryptor = new WeakMap(); -+_cacheEncryptionKey = new WeakMap(); -+_qrKeyringStateListener = new WeakMap(); -+_registerMessageHandlers = new WeakSet(); -+registerMessageHandlers_fn = function() { -+ this.messagingSystem.registerActionHandler( -+ `${name}:signMessage`, -+ this.signMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signPersonalMessage`, -+ this.signPersonalMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signTypedMessage`, -+ this.signTypedMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:decryptMessage`, -+ this.decryptMessage.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getEncryptionPublicKey`, -+ this.getEncryptionPublicKey.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getAccounts`, -+ this.getAccounts.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getKeyringsByType`, -+ this.getKeyringsByType.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:getKeyringForAccount`, -+ this.getKeyringForAccount.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:persistAllKeyrings`, -+ this.persistAllKeyrings.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:prepareUserOperation`, -+ this.prepareUserOperation.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:patchUserOperation`, -+ this.patchUserOperation.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ `${name}:signUserOperation`, -+ this.signUserOperation.bind(this) -+ ); -+}; -+_getKeyringBuilderForType = new WeakSet(); -+getKeyringBuilderForType_fn = function(type) { -+ return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyringBuilders).find( -+ (keyringBuilder) => keyringBuilder.type === type -+ ); -+}; -+_addQRKeyring = new WeakSet(); -+addQRKeyring_fn = async function() { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ return await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */); -+}; -+_subscribeToQRKeyringEvents = new WeakSet(); -+subscribeToQRKeyringEvents_fn = function(qrKeyring) { -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _qrKeyringStateListener, (state) => { -+ this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state); -+ }); -+ qrKeyring.getMemStore().subscribe(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)); -+}; -+_unsubscribeFromQRKeyringsEvents = new WeakSet(); -+unsubscribeFromQRKeyringsEvents_fn = function() { -+ const qrKeyrings = this.getKeyringsByType( -+ "QR Hardware Wallet Device" /* qr */ -+ ); -+ qrKeyrings.forEach((qrKeyring) => { -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)) { -+ qrKeyring.getMemStore().unsubscribe(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _qrKeyringStateListener)); -+ } -+ }); -+}; -+_createNewVaultWithKeyring = new WeakSet(); -+createNewVaultWithKeyring_fn = async function(password, keyring) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ if (typeof password !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ this.update((state) => { -+ delete state.encryptionKey; -+ delete state.encryptionSalt; -+ }); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn).call(this, keyring.type, keyring.opts); -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _setUnlocked, setUnlocked_fn).call(this); -+}; -+_getUpdatedKeyrings = new WeakSet(); -+getUpdatedKeyrings_fn = async function() { -+ return Promise.all(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(displayForKeyring)); -+}; -+_getSerializedKeyrings = new WeakSet(); -+getSerializedKeyrings_fn = async function({ includeUnsupported } = { -+ includeUnsupported: true -+}) { -+ const serializedKeyrings = await Promise.all( -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -+ const [type, data] = await Promise.all([ -+ keyring.type, -+ keyring.serialize() -+ ]); -+ return { type, data }; -+ }) -+ ); -+ if (includeUnsupported) { -+ serializedKeyrings.push(..._chunkNOCGQCUMjs.__privateGet.call(void 0, this, _unsupportedKeyrings)); -+ } -+ return serializedKeyrings; -+}; -+_restoreSerializedKeyrings = new WeakSet(); -+restoreSerializedKeyrings_fn = async function(serializedKeyrings) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _clearKeyrings, clearKeyrings_fn).call(this); -+ for (const serializedKeyring of serializedKeyrings) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreKeyring, restoreKeyring_fn).call(this, serializedKeyring); -+ } -+}; -+_unlockKeyrings = new WeakSet(); -+unlockKeyrings_fn = async function(password, encryptionKey, encryptionSalt) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withVaultLock, withVaultLock_fn).call(this, async ({ releaseLock }) => { -+ const encryptedVault = this.state.vault; -+ if (!encryptedVault) { -+ throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -+ } -+ let vault; -+ const updatedState = {}; -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor)); -+ if (password) { -+ const result = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decryptWithDetail( -+ password, -+ encryptedVault -+ ); -+ vault = result.vault; -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -+ updatedState.encryptionKey = result.exportedKeyString; -+ updatedState.encryptionSalt = result.salt; -+ } else { -+ const parsedEncryptedVault = JSON.parse(encryptedVault); -+ if (encryptionSalt !== parsedEncryptedVault.salt) { -+ throw new Error("KeyringController - Encryption key and salt provided are expired" /* ExpiredCredentials */); -+ } -+ if (typeof encryptionKey !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ const key = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).importKey(encryptionKey); -+ vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decryptWithKey( -+ key, -+ parsedEncryptedVault -+ ); -+ updatedState.encryptionKey = encryptionKey; -+ updatedState.encryptionSalt = encryptionSalt; -+ } -+ } else { -+ if (typeof password !== "string") { -+ throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -+ } -+ vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).decrypt(password, encryptedVault); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, password); -+ } -+ if (!isSerializedKeyringsArray(vault)) { -+ throw new Error("KeyringController - The decrypted vault has an unexpected shape." /* VaultDataError */); -+ } -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, vault); -+ const updatedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -+ this.update((state) => { -+ state.keyrings = updatedKeyrings; -+ if (updatedState.encryptionKey || updatedState.encryptionSalt) { -+ state.encryptionKey = updatedState.encryptionKey; -+ state.encryptionSalt = updatedState.encryptionSalt; -+ } -+ }); -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password) && (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey) || !encryptionKey) && _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).isVaultUpdated && !_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).isVaultUpdated(encryptedVault)) { -+ releaseLock(); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _updateVault, updateVault_fn).call(this); -+ } -+ return _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings); -+ }); -+}; -+_updateVault = new WeakSet(); -+updateVault_fn = function() { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withVaultLock, withVaultLock_fn).call(this, async () => { -+ const { encryptionKey, encryptionSalt } = this.state; -+ if (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password) && !encryptionKey) { -+ throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -+ } -+ const serializedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -+ if (!serializedKeyrings.some((keyring) => keyring.type === "HD Key Tree" /* hd */)) { -+ throw new Error("KeyringController - No HD Keyring found" /* NoHdKeyring */); -+ } -+ const updatedState = {}; -+ if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { -+ assertIsExportableKeyEncryptor(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor)); -+ if (encryptionKey) { -+ const key = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).importKey(encryptionKey); -+ const vaultJSON = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encryptWithKey( -+ key, -+ serializedKeyrings -+ ); -+ vaultJSON.salt = encryptionSalt; -+ updatedState.vault = JSON.stringify(vaultJSON); -+ } else if (_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password)) { -+ const { vault: newVault, exportedKeyString } = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encryptWithDetail( -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password), -+ serializedKeyrings -+ ); -+ updatedState.vault = newVault; -+ updatedState.encryptionKey = exportedKeyString; -+ } -+ } else { -+ assertIsValidPassword(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password)); -+ updatedState.vault = await _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _encryptor).encrypt( -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password), -+ serializedKeyrings -+ ); -+ } -+ if (!updatedState.vault) { -+ throw new Error("KeyringController - Cannot persist vault without vault information" /* MissingVaultData */); -+ } -+ const updatedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -+ this.update((state) => { -+ state.vault = updatedState.vault; -+ state.keyrings = updatedKeyrings; -+ if (updatedState.encryptionKey) { -+ state.encryptionKey = updatedState.encryptionKey; -+ state.encryptionSalt = JSON.parse(updatedState.vault).salt; -+ } -+ }); -+ return true; -+ }); -+}; -+_getAccountsFromKeyrings = new WeakSet(); -+getAccountsFromKeyrings_fn = async function() { -+ const keyrings = _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings); -+ const keyringArrays = await Promise.all( -+ keyrings.map(async (keyring) => keyring.getAccounts()) -+ ); -+ const addresses = keyringArrays.reduce((res, arr) => { -+ return res.concat(arr); -+ }, []); -+ return addresses.map(normalize); -+}; -+_createKeyringWithFirstAccount = new WeakSet(); -+createKeyringWithFirstAccount_fn = async function(type, opts) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const keyring = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, opts); -+ const [firstAccount] = await keyring.getAccounts(); -+ if (!firstAccount) { -+ throw new Error("KeyringController - First Account not found." /* NoFirstAccount */); -+ } -+}; -+_newKeyring = new WeakSet(); -+newKeyring_fn = async function(type, data) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const keyringBuilder = _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, type); -+ if (!keyringBuilder) { -+ throw new Error( -+ `${"KeyringController - No keyringBuilder found for keyring" /* NoKeyringBuilder */}. Keyring type: ${type}` -+ ); -+ } -+ const keyring = keyringBuilder(); -+ await keyring.deserialize(data); -+ if (keyring.init) { -+ await keyring.init(); -+ } -+ if (type === "HD Key Tree" /* hd */ && (!_utils.isObject.call(void 0, data) || !data.mnemonic)) { -+ if (!keyring.generateRandomMnemonic) { -+ throw new Error( -+ "KeyringController - The current keyring does not support the method generateRandomMnemonic." /* UnsupportedGenerateRandomMnemonic */ -+ ); -+ } -+ keyring.generateRandomMnemonic(); -+ await keyring.addAccounts(1); -+ } -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _checkForDuplicate, checkForDuplicate_fn).call(this, type, await keyring.getAccounts()); -+ if (type === "QR Hardware Wallet Device" /* qr */) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn).call(this, keyring); -+ } -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).push(keyring); -+ return keyring; -+}; -+_clearKeyrings = new WeakSet(); -+clearKeyrings_fn = async function() { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ for (const keyring of _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings)) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -+ } -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, []); -+}; -+_restoreKeyring = new WeakSet(); -+restoreKeyring_fn = async function(serialized) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ try { -+ const { type, data } = serialized; -+ return await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, type, data); -+ } catch (_) { -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _unsupportedKeyrings).push(serialized); -+ return void 0; -+ } -+}; -+_destroyKeyring = new WeakSet(); -+destroyKeyring_fn = async function(keyring) { -+ await keyring.destroy?.(); -+}; -+_removeEmptyKeyrings = new WeakSet(); -+removeEmptyKeyrings_fn = async function() { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ const validKeyrings = []; -+ await Promise.all( -+ _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _keyrings).map(async (keyring) => { -+ const accounts = await keyring.getAccounts(); -+ if (accounts.length > 0) { -+ validKeyrings.push(keyring); -+ } else { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -+ } -+ }) -+ ); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _keyrings, validKeyrings); -+}; -+_checkForDuplicate = new WeakSet(); -+checkForDuplicate_fn = async function(type, newAccountArray) { -+ const accounts = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -+ switch (type) { -+ case "Simple Key Pair" /* simple */: { -+ const isIncluded = Boolean( -+ accounts.find( -+ (key) => newAccountArray[0] && (key === newAccountArray[0] || key === _utils.remove0x.call(void 0, newAccountArray[0])) -+ ) -+ ); -+ if (isIncluded) { -+ throw new Error("KeyringController - The account you are trying to import is a duplicate" /* DuplicatedAccount */); -+ } -+ return newAccountArray; -+ } -+ default: { -+ return newAccountArray; -+ } -+ } -+}; -+_setUnlocked = new WeakSet(); -+setUnlocked_fn = function() { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ this.update((state) => { -+ state.isUnlocked = true; -+ }); -+ this.messagingSystem.publish(`${name}:unlock`); -+}; -+_persistOrRollback = new WeakSet(); -+persistOrRollback_fn = async function(fn) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withRollback, withRollback_fn).call(this, async ({ releaseLock }) => { -+ const callbackResult = await fn({ releaseLock }); -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _updateVault, updateVault_fn).call(this); -+ return callbackResult; -+ }); -+}; -+_withRollback = new WeakSet(); -+withRollback_fn = async function(fn) { -+ return _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _withControllerLock, withControllerLock_fn).call(this, async ({ releaseLock }) => { -+ const currentSerializedKeyrings = await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -+ const currentPassword = _chunkNOCGQCUMjs.__privateGet.call(void 0, this, _password); -+ try { -+ return await fn({ releaseLock }); -+ } catch (e) { -+ await _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, currentSerializedKeyrings); -+ _chunkNOCGQCUMjs.__privateSet.call(void 0, this, _password, currentPassword); -+ throw e; -+ } -+ }); -+}; -+_assertControllerMutexIsLocked = new WeakSet(); -+assertControllerMutexIsLocked_fn = function() { -+ if (!_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _controllerOperationMutex).isLocked()) { -+ throw new Error("KeyringController - attempt to update vault during a non mutually exclusive operation" /* ControllerLockRequired */); -+ } -+}; -+_withControllerLock = new WeakSet(); -+withControllerLock_fn = async function(fn) { -+ return withLock(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _controllerOperationMutex), fn); -+}; -+_withVaultLock = new WeakSet(); -+withVaultLock_fn = async function(fn) { -+ _chunkNOCGQCUMjs.__privateMethod.call(void 0, this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -+ return withLock(_chunkNOCGQCUMjs.__privateGet.call(void 0, this, _vaultOperationMutex), fn); -+}; -+async function withLock(mutex, fn) { -+ const releaseLock = await mutex.acquire(); -+ try { -+ return await fn({ releaseLock }); -+ } finally { -+ releaseLock(); -+ } -+} -+var KeyringController_default = KeyringController; -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+exports.KeyringTypes = KeyringTypes; exports.isCustodyKeyring = isCustodyKeyring; exports.AccountImportStrategy = AccountImportStrategy; exports.SignTypedDataVersion = SignTypedDataVersion; exports.keyringBuilderFactory = keyringBuilderFactory; exports.getDefaultKeyringState = getDefaultKeyringState; exports.KeyringController = KeyringController; exports.KeyringController_default = KeyringController_default; -+//# sourceMappingURL=chunk-L4UUWIZA.js.map -\ No newline at end of file -diff --git a/dist/chunk-L4UUWIZA.js.map b/dist/chunk-L4UUWIZA.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..6eb539d49ddeacd961a2282b062088d22ab81bf1 ---- /dev/null -+++ b/dist/chunk-L4UUWIZA.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/KeyringController.ts"],"names":["KeyringTypes","AccountImportStrategy","SignTypedDataVersion"],"mappings":"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","sourcesContent":["import type { TxData, TypedTransaction } from '@ethereumjs/tx';\nimport { isValidPrivate, toBuffer, getBinarySize } from '@ethereumjs/util';\nimport type {\n MetaMaskKeyring as QRKeyring,\n IKeyringState as IQRKeyringState,\n} from '@keystonehq/metamask-airgapped-keyring';\nimport type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport * as encryptorUtils from '@metamask/browser-passworder';\nimport HDKeyring from '@metamask/eth-hd-keyring';\nimport { normalize as ethNormalize } from '@metamask/eth-sig-util';\nimport SimpleKeyring from '@metamask/eth-simple-keyring';\nimport type {\n EthBaseTransaction,\n EthBaseUserOperation,\n EthKeyring,\n EthUserOperation,\n EthUserOperationPatch,\n KeyringExecutionContext,\n} from '@metamask/keyring-api';\nimport type {\n PersonalMessageParams,\n TypedMessageParams,\n} from '@metamask/message-manager';\nimport type {\n Eip1024EncryptedData,\n Hex,\n Json,\n KeyringClass,\n} from '@metamask/utils';\nimport {\n add0x,\n assertIsStrictHexString,\n bytesToHex,\n hasProperty,\n isObject,\n isStrictHexString,\n isValidHexAddress,\n isValidJson,\n remove0x,\n} from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\nimport type { MutexInterface } from 'async-mutex';\nimport Wallet, { thirdparty as importers } from 'ethereumjs-wallet';\nimport type { Patch } from 'immer';\n\nimport { KeyringControllerError } from './constants';\n\nconst name = 'KeyringController';\n\n/**\n * Available keyring types\n */\nexport enum KeyringTypes {\n simple = 'Simple Key Pair',\n hd = 'HD Key Tree',\n qr = 'QR Hardware Wallet Device',\n trezor = 'Trezor Hardware',\n ledger = 'Ledger Hardware',\n lattice = 'Lattice Hardware',\n snap = 'Snap Keyring',\n}\n\n/**\n * Custody keyring types are a special case, as they are not a single type\n * but they all start with the prefix \"Custody\".\n * @param keyringType - The type of the keyring.\n * @returns Whether the keyring type is a custody keyring.\n */\nexport const isCustodyKeyring = (keyringType: string): boolean => {\n return keyringType.startsWith('Custody');\n};\n\n/**\n * @type KeyringControllerState\n *\n * Keyring controller state\n * @property vault - Encrypted string representing keyring data\n * @property isUnlocked - Whether vault is unlocked\n * @property keyringTypes - Account types\n * @property keyrings - Group of accounts\n * @property encryptionKey - Keyring encryption key\n * @property encryptionSalt - Keyring encryption salt\n */\nexport type KeyringControllerState = {\n vault?: string;\n isUnlocked: boolean;\n keyrings: KeyringObject[];\n encryptionKey?: string;\n encryptionSalt?: string;\n};\n\nexport type KeyringControllerMemState = Omit<\n KeyringControllerState,\n 'vault' | 'encryptionKey' | 'encryptionSalt'\n>;\n\nexport type KeyringControllerGetStateAction = {\n type: `${typeof name}:getState`;\n handler: () => KeyringControllerState;\n};\n\nexport type KeyringControllerSignMessageAction = {\n type: `${typeof name}:signMessage`;\n handler: KeyringController['signMessage'];\n};\n\nexport type KeyringControllerSignPersonalMessageAction = {\n type: `${typeof name}:signPersonalMessage`;\n handler: KeyringController['signPersonalMessage'];\n};\n\nexport type KeyringControllerSignTypedMessageAction = {\n type: `${typeof name}:signTypedMessage`;\n handler: KeyringController['signTypedMessage'];\n};\n\nexport type KeyringControllerDecryptMessageAction = {\n type: `${typeof name}:decryptMessage`;\n handler: KeyringController['decryptMessage'];\n};\n\nexport type KeyringControllerGetEncryptionPublicKeyAction = {\n type: `${typeof name}:getEncryptionPublicKey`;\n handler: KeyringController['getEncryptionPublicKey'];\n};\n\nexport type KeyringControllerGetKeyringsByTypeAction = {\n type: `${typeof name}:getKeyringsByType`;\n handler: KeyringController['getKeyringsByType'];\n};\n\nexport type KeyringControllerGetKeyringForAccountAction = {\n type: `${typeof name}:getKeyringForAccount`;\n handler: KeyringController['getKeyringForAccount'];\n};\n\nexport type KeyringControllerGetAccountsAction = {\n type: `${typeof name}:getAccounts`;\n handler: KeyringController['getAccounts'];\n};\n\nexport type KeyringControllerPersistAllKeyringsAction = {\n type: `${typeof name}:persistAllKeyrings`;\n handler: KeyringController['persistAllKeyrings'];\n};\n\nexport type KeyringControllerPrepareUserOperationAction = {\n type: `${typeof name}:prepareUserOperation`;\n handler: KeyringController['prepareUserOperation'];\n};\n\nexport type KeyringControllerPatchUserOperationAction = {\n type: `${typeof name}:patchUserOperation`;\n handler: KeyringController['patchUserOperation'];\n};\n\nexport type KeyringControllerSignUserOperationAction = {\n type: `${typeof name}:signUserOperation`;\n handler: KeyringController['signUserOperation'];\n};\n\nexport type KeyringControllerStateChangeEvent = {\n type: `${typeof name}:stateChange`;\n payload: [KeyringControllerState, Patch[]];\n};\n\nexport type KeyringControllerAccountRemovedEvent = {\n type: `${typeof name}:accountRemoved`;\n payload: [string];\n};\n\nexport type KeyringControllerLockEvent = {\n type: `${typeof name}:lock`;\n payload: [];\n};\n\nexport type KeyringControllerUnlockEvent = {\n type: `${typeof name}:unlock`;\n payload: [];\n};\n\nexport type KeyringControllerQRKeyringStateChangeEvent = {\n type: `${typeof name}:qrKeyringStateChange`;\n payload: [ReturnType];\n};\n\nexport type KeyringControllerActions =\n | KeyringControllerGetStateAction\n | KeyringControllerSignMessageAction\n | KeyringControllerSignPersonalMessageAction\n | KeyringControllerSignTypedMessageAction\n | KeyringControllerDecryptMessageAction\n | KeyringControllerGetEncryptionPublicKeyAction\n | KeyringControllerGetAccountsAction\n | KeyringControllerGetKeyringsByTypeAction\n | KeyringControllerGetKeyringForAccountAction\n | KeyringControllerPersistAllKeyringsAction\n | KeyringControllerPrepareUserOperationAction\n | KeyringControllerPatchUserOperationAction\n | KeyringControllerSignUserOperationAction;\n\nexport type KeyringControllerEvents =\n | KeyringControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent\n | KeyringControllerAccountRemovedEvent\n | KeyringControllerQRKeyringStateChangeEvent;\n\nexport type KeyringControllerMessenger = RestrictedControllerMessenger<\n typeof name,\n KeyringControllerActions,\n KeyringControllerEvents,\n never,\n never\n>;\n\nexport type KeyringControllerOptions = {\n keyringBuilders?: { (): EthKeyring; type: string }[];\n messenger: KeyringControllerMessenger;\n state?: { vault?: string };\n} & (\n | {\n cacheEncryptionKey: true;\n encryptor?: ExportableKeyEncryptor;\n }\n | {\n cacheEncryptionKey?: false;\n encryptor?: GenericEncryptor | ExportableKeyEncryptor;\n }\n);\n\n/**\n * @type KeyringObject\n *\n * Keyring object to return in fullUpdate\n * @property type - Keyring type\n * @property accounts - Associated accounts\n */\nexport type KeyringObject = {\n accounts: string[];\n type: string;\n};\n\n/**\n * A strategy for importing an account\n */\nexport enum AccountImportStrategy {\n privateKey = 'privateKey',\n json = 'json',\n}\n\n/**\n * The `signTypedMessage` version\n *\n * @see https://docs.metamask.io/guide/signing-data.html\n */\nexport enum SignTypedDataVersion {\n V1 = 'V1',\n V3 = 'V3',\n V4 = 'V4',\n}\n\n/**\n * A serialized keyring object.\n */\nexport type SerializedKeyring = {\n type: string;\n data: Json;\n};\n\n/**\n * A generic encryptor interface that supports encrypting and decrypting\n * serializable data with a password.\n */\nexport type GenericEncryptor = {\n /**\n * Encrypts the given object with the given password.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encrypted string.\n */\n encrypt: (password: string, object: Json) => Promise;\n /**\n * Decrypts the given encrypted string with the given password.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decrypt: (password: string, encryptedString: string) => Promise;\n /**\n * Optional vault migration helper. Checks if the provided vault is up to date\n * with the desired encryption algorithm.\n *\n * @param vault - The encrypted string to check.\n * @param targetDerivationParams - The desired target derivation params.\n * @returns The updated encrypted string.\n */\n isVaultUpdated?: (\n vault: string,\n targetDerivationParams?: encryptorUtils.KeyDerivationOptions,\n ) => boolean;\n};\n\n/**\n * An encryptor interface that supports encrypting and decrypting\n * serializable data with a password, and exporting and importing keys.\n */\nexport type ExportableKeyEncryptor = GenericEncryptor & {\n /**\n * Encrypts the given object with the given encryption key.\n *\n * @param key - The encryption key to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encryption result.\n */\n encryptWithKey: (\n key: unknown,\n object: Json,\n ) => Promise;\n /**\n * Encrypts the given object with the given password, and returns the\n * encryption result and the exported key string.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @param salt - The optional salt to use for encryption.\n * @returns The encrypted string and the exported key string.\n */\n encryptWithDetail: (\n password: string,\n object: Json,\n salt?: string,\n ) => Promise;\n /**\n * Decrypts the given encrypted string with the given encryption key.\n *\n * @param key - The encryption key to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decryptWithKey: (key: unknown, encryptedString: string) => Promise;\n /**\n * Decrypts the given encrypted string with the given password, and returns\n * the decrypted object and the salt and exported key string used for\n * encryption.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object and the salt and exported key string used for\n * encryption.\n */\n decryptWithDetail: (\n password: string,\n encryptedString: string,\n ) => Promise;\n /**\n * Generates an encryption key from exported key string.\n *\n * @param key - The exported key string.\n * @returns The encryption key.\n */\n importKey: (key: string) => Promise;\n};\n\nexport type KeyringSelector =\n | {\n type: string;\n index?: number;\n }\n | {\n address: Hex;\n };\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise;\n\n/**\n * Get builder function for `Keyring`\n *\n * Returns a builder function for `Keyring` with a `type` property.\n *\n * @param KeyringConstructor - The Keyring class for the builder.\n * @returns A builder function for the given Keyring.\n */\nexport function keyringBuilderFactory(KeyringConstructor: KeyringClass) {\n const builder = () => new KeyringConstructor();\n\n builder.type = KeyringConstructor.type;\n\n return builder;\n}\n\nconst defaultKeyringBuilders = [\n keyringBuilderFactory(SimpleKeyring),\n keyringBuilderFactory(HDKeyring),\n];\n\nexport const getDefaultKeyringState = (): KeyringControllerState => {\n return {\n isUnlocked: false,\n keyrings: [],\n };\n};\n\n/**\n * Assert that the given keyring has an exportable\n * mnemonic.\n *\n * @param keyring - The keyring to check\n * @throws When the keyring does not have a mnemonic\n */\nfunction assertHasUint8ArrayMnemonic(\n keyring: EthKeyring,\n): asserts keyring is EthKeyring & { mnemonic: Uint8Array } {\n if (\n !(\n hasProperty(keyring, 'mnemonic') && keyring.mnemonic instanceof Uint8Array\n )\n ) {\n throw new Error(\"Can't get mnemonic bytes from keyring\");\n }\n}\n\n/**\n * Assert that the provided encryptor supports\n * encryption and encryption key export.\n *\n * @param encryptor - The encryptor to check.\n * @throws If the encryptor does not support key encryption.\n */\nfunction assertIsExportableKeyEncryptor(\n encryptor: GenericEncryptor | ExportableKeyEncryptor,\n): asserts encryptor is ExportableKeyEncryptor {\n if (\n !(\n 'importKey' in encryptor &&\n typeof encryptor.importKey === 'function' &&\n 'decryptWithKey' in encryptor &&\n typeof encryptor.decryptWithKey === 'function' &&\n 'encryptWithKey' in encryptor &&\n typeof encryptor.encryptWithKey === 'function'\n )\n ) {\n throw new Error(KeyringControllerError.UnsupportedEncryptionKeyExport);\n }\n}\n\n/**\n * Assert that the provided password is a valid non-empty string.\n *\n * @param password - The password to check.\n * @throws If the password is not a valid string.\n */\nfunction assertIsValidPassword(password: unknown): asserts password is string {\n if (typeof password !== 'string') {\n throw new Error(KeyringControllerError.WrongPasswordType);\n }\n\n if (!password || !password.length) {\n throw new Error(KeyringControllerError.InvalidEmptyPassword);\n }\n}\n\n/**\n * Checks if the provided value is a serialized keyrings array.\n *\n * @param array - The value to check.\n * @returns True if the value is a serialized keyrings array.\n */\nfunction isSerializedKeyringsArray(\n array: unknown,\n): array is SerializedKeyring[] {\n return (\n typeof array === 'object' &&\n Array.isArray(array) &&\n array.every((value) => value.type && isValidJson(value.data))\n );\n}\n\n/**\n * Display For Keyring\n *\n * Is used for adding the current keyrings to the state object.\n *\n * @param keyring - The keyring to display.\n * @returns A keyring display object, with type and accounts properties.\n */\nasync function displayForKeyring(\n keyring: EthKeyring,\n): Promise<{ type: string; accounts: string[] }> {\n const accounts = await keyring.getAccounts();\n\n return {\n type: keyring.type,\n // Cast to `string[]` here is safe here because `accounts` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n accounts: accounts.map(normalize) as string[],\n };\n}\n\n/**\n * Check if address is an ethereum address\n *\n * @param address - An address.\n * @returns Returns true if the address is an ethereum one, false otherwise.\n */\nfunction isEthAddress(address: string): boolean {\n // We first check if it's a matching `Hex` string, so that is narrows down\n // `address` as an `Hex` type, allowing us to use `isValidHexAddress`\n return (\n // NOTE: This function only checks for lowercased strings\n isStrictHexString(address.toLowerCase()) &&\n // This checks for lowercased addresses and checksum addresses too\n isValidHexAddress(address as Hex)\n );\n}\n\n/**\n * Normalize ethereum or non-EVM address.\n *\n * @param address - Ethereum or non-EVM address.\n * @returns The normalized address.\n */\nfunction normalize(address: string): string | undefined {\n // Since the `KeyringController` is only dealing with address, we have\n // no other way to get the associated account type with this address. So we\n // are down to check the actual address format for now\n // TODO: Find a better way to not have those runtime checks based on the\n // address value!\n return isEthAddress(address) ? ethNormalize(address) : address;\n}\n\n/**\n * Controller responsible for establishing and managing user identity.\n *\n * This class is a wrapper around the `eth-keyring-controller` package. The\n * `eth-keyring-controller` manages the \"vault\", which is an encrypted store of private keys, and\n * it manages the wallet \"lock\" state. This wrapper class has convenience methods for interacting\n * with the internal keyring controller and handling certain complex operations that involve the\n * keyrings.\n */\nexport class KeyringController extends BaseController<\n typeof name,\n KeyringControllerState,\n KeyringControllerMessenger\n> {\n readonly #controllerOperationMutex = new Mutex();\n\n readonly #vaultOperationMutex = new Mutex();\n\n #keyringBuilders: { (): EthKeyring; type: string }[];\n\n #keyrings: EthKeyring[];\n\n #unsupportedKeyrings: SerializedKeyring[];\n\n #password?: string;\n\n #encryptor: GenericEncryptor | ExportableKeyEncryptor;\n\n #cacheEncryptionKey: boolean;\n\n #qrKeyringStateListener?: (\n state: ReturnType,\n ) => void;\n\n /**\n * Creates a KeyringController instance.\n *\n * @param options - Initial options used to configure this controller\n * @param options.encryptor - An optional object for defining encryption schemes.\n * @param options.keyringBuilders - Set a new name for account.\n * @param options.cacheEncryptionKey - Whether to cache or not encryption key.\n * @param options.messenger - A restricted controller messenger.\n * @param options.state - Initial state to set on this controller.\n */\n constructor(options: KeyringControllerOptions) {\n const {\n encryptor = encryptorUtils,\n keyringBuilders,\n messenger,\n state,\n } = options;\n\n super({\n name,\n metadata: {\n vault: { persist: true, anonymous: false },\n isUnlocked: { persist: false, anonymous: true },\n keyrings: { persist: false, anonymous: false },\n encryptionKey: { persist: false, anonymous: false },\n encryptionSalt: { persist: false, anonymous: false },\n },\n messenger,\n state: {\n ...getDefaultKeyringState(),\n ...state,\n },\n });\n\n this.#keyringBuilders = keyringBuilders\n ? defaultKeyringBuilders.concat(keyringBuilders)\n : defaultKeyringBuilders;\n\n this.#encryptor = encryptor;\n this.#keyrings = [];\n this.#unsupportedKeyrings = [];\n\n // This option allows the controller to cache an exported key\n // for use in decrypting and encrypting data without password\n this.#cacheEncryptionKey = Boolean(options.cacheEncryptionKey);\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(encryptor);\n }\n\n this.#registerMessageHandlers();\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring.\n *\n * @param accountCount - Number of accounts before adding a new one, used to\n * make the method idempotent.\n * @returns Promise resolving to the added account address.\n */\n async addNewAccount(accountCount?: number): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const oldAccounts = await primaryKeyring.getAccounts();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n // we return the account already existing at index `accountCount`\n const existingAccount = oldAccounts[accountCount];\n\n if (!existingAccount) {\n throw new Error(`Can't find account at index ${accountCount}`);\n }\n\n return existingAccount;\n }\n\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the specified keyring.\n *\n * @param keyring - Keyring to add the account to.\n * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent.\n * @returns Promise resolving to the added account address\n */\n async addNewAccountForKeyring(\n keyring: EthKeyring,\n accountCount?: number,\n ): Promise {\n // READ THIS CAREFULLY:\n // We still uses `Hex` here, since we are not using this method when creating\n // and account using a \"Snap Keyring\". This function assume the `keyring` is\n // ethereum compatible, but \"Snap Keyring\" might not be.\n return this.#persistOrRollback(async () => {\n const oldAccounts = await this.#getAccountsFromKeyrings();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n\n const existingAccount = oldAccounts[accountCount];\n assertIsStrictHexString(existingAccount);\n\n return existingAccount;\n }\n\n await keyring.addAccounts(1);\n\n const addedAccountAddress = (await this.#getAccountsFromKeyrings()).find(\n (selectedAddress) => !oldAccounts.includes(selectedAddress),\n );\n assertIsStrictHexString(addedAccountAddress);\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences.\n *\n * @returns Promise resolving to the added account address.\n */\n async addNewAccountWithoutUpdate(): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n return addedAccountAddress;\n });\n }\n\n /**\n * Effectively the same as creating a new keychain then populating it\n * using the given seed phrase.\n *\n * @param password - Password to unlock keychain.\n * @param seed - A BIP39-compliant seed phrase as Uint8Array,\n * either as a string or an array of UTF-8 bytes that represent the string.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndRestore(\n password: string,\n seed: Uint8Array,\n ): Promise {\n return this.#persistOrRollback(async () => {\n assertIsValidPassword(password);\n\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n opts: {\n mnemonic: seed,\n numberOfAccounts: 1,\n },\n });\n });\n }\n\n /**\n * Create a new primary keychain and wipe any previous keychains.\n *\n * @param password - Password to unlock the new vault.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndKeychain(password: string) {\n return this.#persistOrRollback(async () => {\n const accounts = await this.#getAccountsFromKeyrings();\n if (!accounts.length) {\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n });\n }\n });\n }\n\n /**\n * Adds a new keyring of the given `type`.\n *\n * @param type - Keyring type name.\n * @param opts - Keyring options.\n * @throws If a builder for the given `type` does not exist.\n * @returns Promise resolving to the added keyring.\n */\n async addNewKeyring(\n type: KeyringTypes | string,\n opts?: unknown,\n ): Promise {\n if (type === KeyringTypes.qr) {\n return this.getOrAddQRKeyring();\n }\n\n return this.#persistOrRollback(async () => this.#newKeyring(type, opts));\n }\n\n /**\n * Method to verify a given password validity. Throws an\n * error if the password is invalid.\n *\n * @param password - Password of the keyring.\n */\n async verifyPassword(password: string) {\n if (!this.state.vault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n await this.#encryptor.decrypt(password, this.state.vault);\n }\n\n /**\n * Returns the status of the vault.\n *\n * @returns Boolean returning true if the vault is unlocked.\n */\n isUnlocked(): boolean {\n return this.state.isUnlocked;\n }\n\n /**\n * Gets the seed phrase of the HD keyring.\n *\n * @param password - Password of the keyring.\n * @returns Promise resolving to the seed phrase.\n */\n async exportSeedPhrase(password: string): Promise {\n await this.verifyPassword(password);\n assertHasUint8ArrayMnemonic(this.#keyrings[0]);\n return this.#keyrings[0].mnemonic;\n }\n\n /**\n * Gets the private key from the keyring controlling an address.\n *\n * @param password - Password of the keyring.\n * @param address - Address to export.\n * @returns Promise resolving to the private key for an address.\n */\n async exportAccount(password: string, address: string): Promise {\n await this.verifyPassword(password);\n\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.exportAccount) {\n throw new Error(KeyringControllerError.UnsupportedExportAccount);\n }\n\n return await keyring.exportAccount(normalize(address) as Hex);\n }\n\n /**\n * Returns the public addresses of all accounts from every keyring.\n *\n * @returns A promise resolving to an array of addresses.\n */\n async getAccounts(): Promise {\n return this.state.keyrings.reduce(\n (accounts, keyring) => accounts.concat(keyring.accounts),\n [],\n );\n }\n\n /**\n * Get encryption public key.\n *\n * @param account - An account address.\n * @param opts - Additional encryption options.\n * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method\n * @returns Promise resolving to encyption public key of the `account` if one exists.\n */\n async getEncryptionPublicKey(\n account: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(account) as Hex;\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n if (!keyring.getEncryptionPublicKey) {\n throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey);\n }\n\n return await keyring.getEncryptionPublicKey(address, opts);\n }\n\n /**\n * Attempts to decrypt the provided message parameters.\n *\n * @param messageParams - The decryption message parameters.\n * @param messageParams.from - The address of the account you want to use to decrypt the message.\n * @param messageParams.data - The encrypted data that you want to decrypt.\n * @returns The raw decryption result.\n */\n async decryptMessage(messageParams: {\n from: string;\n data: Eip1024EncryptedData;\n }): Promise {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.decryptMessage) {\n throw new Error(KeyringControllerError.UnsupportedDecryptMessage);\n }\n\n return keyring.decryptMessage(address, messageParams.data);\n }\n\n /**\n * Returns the currently initialized keyring that manages\n * the specified `address` if one exists.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param account - An account address.\n * @returns Promise resolving to keyring of the `account` if one exists.\n */\n async getKeyringForAccount(account: string): Promise {\n const address = normalize(account);\n\n const candidates = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n return Promise.all([keyring, keyring.getAccounts()]);\n }),\n );\n\n const winners = candidates.filter((candidate) => {\n const accounts = candidate[1].map(normalize);\n return accounts.includes(address);\n });\n\n if (winners.length && winners[0]?.length) {\n return winners[0][0];\n }\n\n // Adding more info to the error\n let errorInfo = '';\n if (!candidates.length) {\n errorInfo = 'There are no keyrings';\n } else if (!winners.length) {\n errorInfo = 'There are keyrings, but none match the address';\n }\n throw new Error(\n `${KeyringControllerError.NoKeyring}. Error info: ${errorInfo}`,\n );\n }\n\n /**\n * Returns all keyrings of the given type.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param type - Keyring type name.\n * @returns An array of keyrings of the given type.\n */\n getKeyringsByType(type: KeyringTypes | string): unknown[] {\n return this.#keyrings.filter((keyring) => keyring.type === type);\n }\n\n /**\n * Persist all serialized keyrings in the vault.\n *\n * @deprecated This method is being phased out in favor of `withKeyring`.\n * @returns Promise resolving with `true` value when the\n * operation completes.\n */\n async persistAllKeyrings(): Promise {\n return this.#persistOrRollback(async () => true);\n }\n\n /**\n * Imports an account with the specified import strategy.\n *\n * @param strategy - Import strategy name.\n * @param args - Array of arguments to pass to the underlying stategy.\n * @throws Will throw when passed an unrecognized strategy.\n * @returns Promise resolving to the imported account address.\n */\n async importAccountWithStrategy(\n strategy: AccountImportStrategy,\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[],\n ): Promise {\n return this.#persistOrRollback(async () => {\n let privateKey;\n switch (strategy) {\n case 'privateKey':\n const [importedKey] = args;\n if (!importedKey) {\n throw new Error('Cannot import an empty key.');\n }\n const prefixed = add0x(importedKey);\n\n let bufferedPrivateKey;\n try {\n bufferedPrivateKey = toBuffer(prefixed);\n } catch {\n throw new Error('Cannot import invalid private key.');\n }\n\n if (\n !isValidPrivate(bufferedPrivateKey) ||\n // ensures that the key is 64 bytes long\n getBinarySize(prefixed) !== 64 + '0x'.length\n ) {\n throw new Error('Cannot import invalid private key.');\n }\n\n privateKey = remove0x(prefixed);\n break;\n case 'json':\n let wallet;\n const [input, password] = args;\n try {\n wallet = importers.fromEtherWallet(input, password);\n } catch (e) {\n wallet = wallet || (await Wallet.fromV3(input, password, true));\n }\n privateKey = bytesToHex(wallet.getPrivateKey());\n break;\n default:\n throw new Error(`Unexpected import strategy: '${strategy}'`);\n }\n const newKeyring = (await this.#newKeyring(KeyringTypes.simple, [\n privateKey,\n ])) as EthKeyring;\n const accounts = await newKeyring.getAccounts();\n return accounts[0];\n });\n }\n\n /**\n * Removes an account from keyring state.\n *\n * @param address - Address of the account to remove.\n * @fires KeyringController:accountRemoved\n * @returns Promise resolving when the account is removed.\n */\n async removeAccount(address: string): Promise {\n await this.#persistOrRollback(async () => {\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n // Not all the keyrings support this, so we have to check\n if (!keyring.removeAccount) {\n throw new Error(KeyringControllerError.UnsupportedRemoveAccount);\n }\n\n // The `removeAccount` method of snaps keyring is async. We have to update\n // the interface of the other keyrings to be async as well.\n // eslint-disable-next-line @typescript-eslint/await-thenable\n // FIXME: We do cast to `Hex` to makes the type checker happy here, and\n // because `Keyring.removeAccount` requires address to be `Hex`. Those\n // type would need to be updated for a full non-EVM support.\n await keyring.removeAccount(address as Hex);\n\n const accounts = await keyring.getAccounts();\n // Check if this was the last/only account\n if (accounts.length === 0) {\n await this.#removeEmptyKeyrings();\n }\n });\n\n this.messagingSystem.publish(`${name}:accountRemoved`, address);\n }\n\n /**\n * Deallocates all secrets and locks the wallet.\n *\n * @returns Promise resolving when the operation completes.\n */\n async setLocked(): Promise {\n return this.#withRollback(async () => {\n this.#unsubscribeFromQRKeyringsEvents();\n\n this.#password = undefined;\n await this.#clearKeyrings();\n\n this.update((state) => {\n state.isUnlocked = false;\n state.keyrings = [];\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n\n this.messagingSystem.publish(`${name}:lock`);\n });\n }\n\n /**\n * Signs message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signMessage(messageParams: PersonalMessageParams): Promise {\n if (!messageParams.data) {\n throw new Error(\"Can't sign an empty message\");\n }\n\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignMessage);\n }\n\n return await keyring.signMessage(address, messageParams.data);\n }\n\n /**\n * Signs personal message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signPersonalMessage(messageParams: PersonalMessageParams) {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signPersonalMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignPersonalMessage);\n }\n\n const normalizedData = normalize(messageParams.data) as Hex;\n\n return await keyring.signPersonalMessage(address, normalizedData);\n }\n\n /**\n * Signs typed message by calling down into a specific keyring.\n *\n * @param messageParams - TypedMessageParams object to sign.\n * @param version - Compatibility version EIP712.\n * @throws Will throw when passed an unrecognized version.\n * @returns Promise resolving to a signed message string or an error if any.\n */\n async signTypedMessage(\n messageParams: TypedMessageParams,\n version: SignTypedDataVersion,\n ): Promise {\n try {\n if (\n ![\n SignTypedDataVersion.V1,\n SignTypedDataVersion.V3,\n SignTypedDataVersion.V4,\n ].includes(version)\n ) {\n throw new Error(`Unexpected signTypedMessage version: '${version}'`);\n }\n\n // Cast to `Hex` here is safe here because `messageParams.from` is not nullish.\n // `normalize` returns `Hex` unless given a nullish value.\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTypedData) {\n throw new Error(KeyringControllerError.UnsupportedSignTypedMessage);\n }\n\n return await keyring.signTypedData(\n address,\n version !== SignTypedDataVersion.V1 &&\n typeof messageParams.data === 'string'\n ? JSON.parse(messageParams.data)\n : messageParams.data,\n { version },\n );\n } catch (error) {\n throw new Error(`Keyring Controller signTypedMessage: ${error}`);\n }\n }\n\n /**\n * Signs a transaction by calling down into a specific keyring.\n *\n * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance.\n * @param from - Address to sign from, should be in keychain.\n * @param opts - An optional options object.\n * @returns Promise resolving to a signed transaction string.\n */\n async signTransaction(\n transaction: TypedTransaction,\n from: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTransaction) {\n throw new Error(KeyringControllerError.UnsupportedSignTransaction);\n }\n\n return await keyring.signTransaction(address, transaction, opts);\n }\n\n /**\n * Convert a base transaction to a base UserOperation.\n *\n * @param from - Address of the sender.\n * @param transactions - Base transactions to include in the UserOperation.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A pseudo-UserOperation that can be used to construct a real.\n */\n async prepareUserOperation(\n from: string,\n transactions: EthBaseTransaction[],\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.prepareUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPrepareUserOperation);\n }\n\n return await keyring.prepareUserOperation(\n address,\n transactions,\n executionContext,\n );\n }\n\n /**\n * Patches properties of a UserOperation. Currently, only the\n * `paymasterAndData` can be patched.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to patch.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A patch to apply to the UserOperation.\n */\n async patchUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.patchUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPatchUserOperation);\n }\n\n return await keyring.patchUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Signs an UserOperation.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to sign.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns The signature of the UserOperation.\n */\n async signUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.signUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedSignUserOperation);\n }\n\n return await keyring.signUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Changes the password used to encrypt the vault.\n *\n * @param password - The new password.\n * @returns Promise resolving when the operation completes.\n */\n changePassword(password: string): Promise {\n return this.#persistOrRollback(async () => {\n if (!this.state.isUnlocked) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n assertIsValidPassword(password);\n\n this.#password = password;\n // We need to clear encryption key and salt from state\n // to force the controller to re-encrypt the vault using\n // the new password.\n if (this.#cacheEncryptionKey) {\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n }\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given encryption key and salt.\n *\n * @param encryptionKey - Key to unlock the keychain.\n * @param encryptionSalt - Salt to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitEncryptionKey(\n encryptionKey: string,\n encryptionSalt: string,\n ): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(\n undefined,\n encryptionKey,\n encryptionSalt,\n );\n this.#setUnlocked();\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given password.\n *\n * @param password - Password to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitPassword(password: string): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(password);\n this.#setUnlocked();\n });\n }\n\n /**\n * Verifies the that the seed phrase restores the current keychain's accounts.\n *\n * @returns Promise resolving to the seed phrase as Uint8Array.\n */\n async verifySeedPhrase(): Promise {\n const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found.');\n }\n\n assertHasUint8ArrayMnemonic(primaryKeyring);\n\n const seedWords = primaryKeyring.mnemonic;\n const accounts = await primaryKeyring.getAccounts();\n /* istanbul ignore if */\n if (accounts.length === 0) {\n throw new Error('Cannot verify an empty keyring.');\n }\n\n // The HD Keyring Builder is a default keyring builder\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!;\n\n const hdKeyring = hdKeyringBuilder();\n // @ts-expect-error @metamask/eth-hd-keyring correctly handles\n // Uint8Array seed phrases in the `deserialize` method.\n await hdKeyring.deserialize({\n mnemonic: seedWords,\n numberOfAccounts: accounts.length,\n });\n const testAccounts = await hdKeyring.getAccounts();\n /* istanbul ignore if */\n if (testAccounts.length !== accounts.length) {\n throw new Error('Seed phrase imported incorrect number of accounts.');\n }\n\n testAccounts.forEach((account: string, i: number) => {\n /* istanbul ignore if */\n if (account.toLowerCase() !== accounts[i].toLowerCase()) {\n throw new Error('Seed phrase imported different accounts.');\n }\n });\n\n return seedWords;\n }\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @param options - Additional options.\n * @param options.createIfMissing - Whether to create a new keyring if the selected one is missing.\n * @param options.createWithData - Optional data to use when creating a new keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n * @deprecated This method overload is deprecated. Use `withKeyring` without options instead.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n // eslint-disable-next-line @typescript-eslint/unified-signatures\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown },\n ): Promise;\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n ): Promise;\n\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown } = {\n createIfMissing: false,\n },\n ): Promise {\n return this.#persistOrRollback(async () => {\n let keyring: SelectedKeyring | undefined;\n\n if ('address' in selector) {\n keyring = (await this.getKeyringForAccount(selector.address)) as\n | SelectedKeyring\n | undefined;\n } else {\n keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as\n | SelectedKeyring\n | undefined;\n\n if (!keyring && options.createIfMissing) {\n keyring = (await this.#newKeyring(\n selector.type,\n options.createWithData,\n )) as SelectedKeyring;\n }\n }\n\n if (!keyring) {\n throw new Error(KeyringControllerError.KeyringNotFound);\n }\n\n const result = await operation(keyring);\n\n if (Object.is(result, keyring)) {\n // Access to a keyring instance outside of controller safeguards\n // should be discouraged, as it can lead to unexpected behavior.\n // This error is thrown to prevent consumers using `withKeyring`\n // as a way to get a reference to a keyring instance.\n throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess);\n }\n\n return result;\n });\n }\n\n // QR Hardware related methods\n\n /**\n * Get QR Hardware keyring.\n *\n * @returns The QR Keyring if defined, otherwise undefined\n */\n getQRKeyring(): QRKeyring | undefined {\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return this.getKeyringsByType(KeyringTypes.qr)[0] as unknown as QRKeyring;\n }\n\n /**\n * Get QR hardware keyring. If it doesn't exist, add it.\n *\n * @returns The added keyring\n */\n async getOrAddQRKeyring(): Promise {\n return (\n this.getQRKeyring() ||\n (await this.#persistOrRollback(async () => this.#addQRKeyring()))\n );\n }\n\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async restoreQRKeyring(serialized: any): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n keyring.deserialize(serialized);\n });\n }\n\n async resetQRKeyringState(): Promise {\n (await this.getOrAddQRKeyring()).resetStore();\n }\n\n async getQRKeyringState(): Promise {\n return (await this.getOrAddQRKeyring()).getMemStore();\n }\n\n async submitQRCryptoHDKey(cryptoHDKey: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey);\n }\n\n async submitQRCryptoAccount(cryptoAccount: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount);\n }\n\n async submitQRSignature(\n requestId: string,\n ethSignature: string,\n ): Promise {\n (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature);\n }\n\n async cancelQRSignRequest(): Promise {\n (await this.getOrAddQRKeyring()).cancelSignRequest();\n }\n\n /**\n * Cancels qr keyring sync.\n */\n async cancelQRSynchronization(): Promise {\n // eslint-disable-next-line n/no-sync\n (await this.getOrAddQRKeyring()).cancelSync();\n }\n\n async connectQRHardware(\n page: number,\n ): Promise<{ balance: string; address: string; index: number }[]> {\n return this.#persistOrRollback(async () => {\n try {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n let accounts;\n switch (page) {\n case -1:\n accounts = await keyring.getPreviousPage();\n break;\n case 1:\n accounts = await keyring.getNextPage();\n break;\n default:\n accounts = await keyring.getFirstPage();\n }\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return accounts.map((account: any) => {\n return {\n ...account,\n balance: '0x0',\n };\n });\n } catch (e) {\n // TODO: Add test case for when keyring throws\n /* istanbul ignore next */\n throw new Error(`Unspecified error when connect QR Hardware, ${e}`);\n }\n });\n }\n\n async unlockQRHardwareWalletAccount(index: number): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n\n keyring.setAccountToUnlock(index);\n await keyring.addAccounts(1);\n });\n }\n\n async getAccountKeyringType(account: string): Promise {\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n return keyring.type;\n }\n\n async forgetQRDevice(): Promise<{\n removedAccounts: string[];\n remainingAccounts: string[];\n }> {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring();\n\n if (!keyring) {\n return { removedAccounts: [], remainingAccounts: [] };\n }\n\n const allAccounts = (await this.#getAccountsFromKeyrings()) as string[];\n keyring.forgetDevice();\n const remainingAccounts =\n (await this.#getAccountsFromKeyrings()) as string[];\n const removedAccounts = allAccounts.filter(\n (address: string) => !remainingAccounts.includes(address),\n );\n return { removedAccounts, remainingAccounts };\n });\n }\n\n /**\n * Constructor helper for registering this controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messagingSystem.registerActionHandler(\n `${name}:signMessage`,\n this.signMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signPersonalMessage`,\n this.signPersonalMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signTypedMessage`,\n this.signTypedMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:decryptMessage`,\n this.decryptMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getEncryptionPublicKey`,\n this.getEncryptionPublicKey.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getAccounts`,\n this.getAccounts.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringsByType`,\n this.getKeyringsByType.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringForAccount`,\n this.getKeyringForAccount.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:persistAllKeyrings`,\n this.persistAllKeyrings.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:prepareUserOperation`,\n this.prepareUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:patchUserOperation`,\n this.patchUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signUserOperation`,\n this.signUserOperation.bind(this),\n );\n }\n\n /**\n * Get the keyring builder for the given `type`.\n *\n * @param type - The type of keyring to get the builder for.\n * @returns The keyring builder, or undefined if none exists.\n */\n #getKeyringBuilderForType(\n type: string,\n ): { (): EthKeyring; type: string } | undefined {\n return this.#keyringBuilders.find(\n (keyringBuilder) => keyringBuilder.type === type,\n );\n }\n\n /**\n * Add qr hardware keyring.\n *\n * @returns The added keyring\n * @throws If a QRKeyring builder is not provided\n * when initializing the controller\n */\n async #addQRKeyring(): Promise {\n this.#assertControllerMutexIsLocked();\n\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return (await this.#newKeyring(KeyringTypes.qr)) as unknown as QRKeyring;\n }\n\n /**\n * Subscribe to a QRKeyring state change events and\n * forward them through the messaging system.\n *\n * @param qrKeyring - The QRKeyring instance to subscribe to\n */\n #subscribeToQRKeyringEvents(qrKeyring: QRKeyring) {\n this.#qrKeyringStateListener = (state) => {\n this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state);\n };\n\n qrKeyring.getMemStore().subscribe(this.#qrKeyringStateListener);\n }\n\n #unsubscribeFromQRKeyringsEvents() {\n const qrKeyrings = this.getKeyringsByType(\n KeyringTypes.qr,\n ) as unknown as QRKeyring[];\n\n qrKeyrings.forEach((qrKeyring) => {\n if (this.#qrKeyringStateListener) {\n qrKeyring.getMemStore().unsubscribe(this.#qrKeyringStateListener);\n }\n });\n }\n\n /**\n * Create new vault with an initial keyring\n *\n * Destroys any old encrypted storage,\n * creates a new encrypted store with the given password,\n * creates a new wallet with 1 account.\n *\n * @fires KeyringController:unlock\n * @param password - The password to encrypt the vault with.\n * @param keyring - A object containing the params to instantiate a new keyring.\n * @param keyring.type - The keyring type.\n * @param keyring.opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves to the state.\n */\n async #createNewVaultWithKeyring(\n password: string,\n keyring: {\n type: string;\n opts?: unknown;\n },\n ): Promise {\n this.#assertControllerMutexIsLocked();\n\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n\n this.#password = password;\n\n await this.#clearKeyrings();\n await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);\n this.#setUnlocked();\n }\n\n /**\n * Get the updated array of each keyring's type and\n * accounts list.\n *\n * @returns A promise resolving to the updated keyrings array.\n */\n async #getUpdatedKeyrings(): Promise {\n return Promise.all(this.#keyrings.map(displayForKeyring));\n }\n\n /**\n * Serialize the current array of keyring instances,\n * including unsupported keyrings by default.\n *\n * @param options - Method options.\n * @param options.includeUnsupported - Whether to include unsupported keyrings.\n * @returns The serialized keyrings.\n */\n async #getSerializedKeyrings(\n { includeUnsupported }: { includeUnsupported: boolean } = {\n includeUnsupported: true,\n },\n ): Promise {\n const serializedKeyrings = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n const [type, data] = await Promise.all([\n keyring.type,\n keyring.serialize(),\n ]);\n return { type, data };\n }),\n );\n\n if (includeUnsupported) {\n serializedKeyrings.push(...this.#unsupportedKeyrings);\n }\n\n return serializedKeyrings;\n }\n\n /**\n * Restore a serialized keyrings array.\n *\n * @param serializedKeyrings - The serialized keyrings array.\n */\n async #restoreSerializedKeyrings(\n serializedKeyrings: SerializedKeyring[],\n ): Promise {\n await this.#clearKeyrings();\n\n for (const serializedKeyring of serializedKeyrings) {\n await this.#restoreKeyring(serializedKeyring);\n }\n }\n\n /**\n * Unlock Keyrings, decrypting the vault and deserializing all\n * keyrings contained in it, using a password or an encryption key with salt.\n *\n * @param password - The keyring controller password.\n * @param encryptionKey - An exported key string to unlock keyrings with.\n * @param encryptionSalt - The salt used to encrypt the vault.\n * @returns A promise resolving to the deserialized keyrings array.\n */\n async #unlockKeyrings(\n password: string | undefined,\n encryptionKey?: string,\n encryptionSalt?: string,\n ): Promise[]> {\n return this.#withVaultLock(async ({ releaseLock }) => {\n const encryptedVault = this.state.vault;\n if (!encryptedVault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n\n let vault;\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (password) {\n const result = await this.#encryptor.decryptWithDetail(\n password,\n encryptedVault,\n );\n vault = result.vault;\n this.#password = password;\n\n updatedState.encryptionKey = result.exportedKeyString;\n updatedState.encryptionSalt = result.salt;\n } else {\n const parsedEncryptedVault = JSON.parse(encryptedVault);\n\n if (encryptionSalt !== parsedEncryptedVault.salt) {\n throw new Error(KeyringControllerError.ExpiredCredentials);\n }\n\n if (typeof encryptionKey !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n const key = await this.#encryptor.importKey(encryptionKey);\n vault = await this.#encryptor.decryptWithKey(\n key,\n parsedEncryptedVault,\n );\n\n // This call is required on the first call because encryptionKey\n // is not yet inside the memStore\n updatedState.encryptionKey = encryptionKey;\n // we can safely assume that encryptionSalt is defined here\n // because we compare it with the salt from the vault\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n updatedState.encryptionSalt = encryptionSalt!;\n }\n } else {\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n vault = await this.#encryptor.decrypt(password, encryptedVault);\n this.#password = password;\n }\n\n if (!isSerializedKeyringsArray(vault)) {\n throw new Error(KeyringControllerError.VaultDataError);\n }\n\n await this.#restoreSerializedKeyrings(vault);\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n\n this.update((state) => {\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey || updatedState.encryptionSalt) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = updatedState.encryptionSalt;\n }\n });\n\n if (\n this.#password &&\n (!this.#cacheEncryptionKey || !encryptionKey) &&\n this.#encryptor.isVaultUpdated &&\n !this.#encryptor.isVaultUpdated(encryptedVault)\n ) {\n // The lock needs to be released before persisting the keyrings\n // to avoid deadlock\n releaseLock();\n // Re-encrypt the vault with safer method if one is available\n await this.#updateVault();\n }\n\n return this.#keyrings;\n });\n }\n\n /**\n * Update the vault with the current keyrings.\n *\n * @returns A promise resolving to `true` if the operation is successful.\n */\n #updateVault(): Promise {\n return this.#withVaultLock(async () => {\n const { encryptionKey, encryptionSalt } = this.state;\n\n if (!this.#password && !encryptionKey) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n const serializedKeyrings = await this.#getSerializedKeyrings();\n\n if (\n !serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)\n ) {\n throw new Error(KeyringControllerError.NoHdKeyring);\n }\n\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (encryptionKey) {\n const key = await this.#encryptor.importKey(encryptionKey);\n const vaultJSON = await this.#encryptor.encryptWithKey(\n key,\n serializedKeyrings,\n );\n vaultJSON.salt = encryptionSalt;\n updatedState.vault = JSON.stringify(vaultJSON);\n } else if (this.#password) {\n const { vault: newVault, exportedKeyString } =\n await this.#encryptor.encryptWithDetail(\n this.#password,\n serializedKeyrings,\n );\n\n updatedState.vault = newVault;\n updatedState.encryptionKey = exportedKeyString;\n }\n } else {\n assertIsValidPassword(this.#password);\n updatedState.vault = await this.#encryptor.encrypt(\n this.#password,\n serializedKeyrings,\n );\n }\n\n if (!updatedState.vault) {\n throw new Error(KeyringControllerError.MissingVaultData);\n }\n\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n this.update((state) => {\n state.vault = updatedState.vault;\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = JSON.parse(updatedState.vault as string).salt;\n }\n });\n\n return true;\n });\n }\n\n /**\n * Retrieves all the accounts from keyrings instances\n * that are currently in memory.\n *\n * @returns A promise resolving to an array of accounts.\n */\n async #getAccountsFromKeyrings(): Promise {\n const keyrings = this.#keyrings;\n\n const keyringArrays = await Promise.all(\n keyrings.map(async (keyring) => keyring.getAccounts()),\n );\n const addresses = keyringArrays.reduce((res, arr) => {\n return res.concat(arr);\n }, []);\n\n // Cast to `string[]` here is safe here because `addresses` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n return addresses.map(normalize) as string[];\n }\n\n /**\n * Create a new keyring, ensuring that the first account is\n * also created.\n *\n * @param type - Keyring type to instantiate.\n * @param opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves if the operation is successful.\n */\n async #createKeyringWithFirstAccount(type: string, opts?: unknown) {\n this.#assertControllerMutexIsLocked();\n\n const keyring = (await this.#newKeyring(type, opts)) as EthKeyring;\n\n const [firstAccount] = await keyring.getAccounts();\n if (!firstAccount) {\n throw new Error(KeyringControllerError.NoFirstAccount);\n }\n }\n\n /**\n * Instantiate, initialize and return a new keyring of the given `type`,\n * using the given `opts`. The keyring is built using the keyring builder\n * registered for the given `type`.\n *\n *\n * @param type - The type of keyring to add.\n * @param data - The data to restore a previously serialized keyring.\n * @returns The new keyring.\n * @throws If the keyring includes duplicated accounts.\n */\n async #newKeyring(type: string, data?: unknown): Promise> {\n this.#assertControllerMutexIsLocked();\n\n const keyringBuilder = this.#getKeyringBuilderForType(type);\n\n if (!keyringBuilder) {\n throw new Error(\n `${KeyringControllerError.NoKeyringBuilder}. Keyring type: ${type}`,\n );\n }\n\n const keyring = keyringBuilder();\n\n // @ts-expect-error Enforce data type after updating clients\n await keyring.deserialize(data);\n\n if (keyring.init) {\n await keyring.init();\n }\n\n if (type === KeyringTypes.hd && (!isObject(data) || !data.mnemonic)) {\n if (!keyring.generateRandomMnemonic) {\n throw new Error(\n KeyringControllerError.UnsupportedGenerateRandomMnemonic,\n );\n }\n\n keyring.generateRandomMnemonic();\n await keyring.addAccounts(1);\n }\n\n await this.#checkForDuplicate(type, await keyring.getAccounts());\n\n if (type === KeyringTypes.qr) {\n // In case of a QR keyring type, we need to subscribe\n // to its events after creating it\n this.#subscribeToQRKeyringEvents(keyring as unknown as QRKeyring);\n }\n\n this.#keyrings.push(keyring);\n\n return keyring;\n }\n\n /**\n * Remove all managed keyrings, destroying all their\n * instances in memory.\n */\n async #clearKeyrings() {\n this.#assertControllerMutexIsLocked();\n for (const keyring of this.#keyrings) {\n await this.#destroyKeyring(keyring);\n }\n this.#keyrings = [];\n }\n\n /**\n * Restore a Keyring from a provided serialized payload.\n * On success, returns the resulting keyring instance.\n *\n * @param serialized - The serialized keyring.\n * @returns The deserialized keyring or undefined if the keyring type is unsupported.\n */\n async #restoreKeyring(\n serialized: SerializedKeyring,\n ): Promise | undefined> {\n this.#assertControllerMutexIsLocked();\n\n try {\n const { type, data } = serialized;\n return await this.#newKeyring(type, data);\n } catch (_) {\n this.#unsupportedKeyrings.push(serialized);\n return undefined;\n }\n }\n\n /**\n * Destroy Keyring\n *\n * Some keyrings support a method called `destroy`, that destroys the\n * keyring along with removing all its event listeners and, in some cases,\n * clears the keyring bridge iframe from the DOM.\n *\n * @param keyring - The keyring to destroy.\n */\n async #destroyKeyring(keyring: EthKeyring) {\n await keyring.destroy?.();\n }\n\n /**\n * Remove empty keyrings.\n *\n * Loops through the keyrings and removes the ones with empty accounts\n * (usually after removing the last / only account) from a keyring.\n */\n async #removeEmptyKeyrings(): Promise {\n this.#assertControllerMutexIsLocked();\n const validKeyrings: EthKeyring[] = [];\n\n // Since getAccounts returns a Promise\n // We need to wait to hear back form each keyring\n // in order to decide which ones are now valid (accounts.length > 0)\n\n await Promise.all(\n this.#keyrings.map(async (keyring: EthKeyring) => {\n const accounts = await keyring.getAccounts();\n if (accounts.length > 0) {\n validKeyrings.push(keyring);\n } else {\n await this.#destroyKeyring(keyring);\n }\n }),\n );\n this.#keyrings = validKeyrings;\n }\n\n /**\n * Checks for duplicate keypairs, using the the first account in the given\n * array. Rejects if a duplicate is found.\n *\n * Only supports 'Simple Key Pair'.\n *\n * @param type - The key pair type to check for.\n * @param newAccountArray - Array of new accounts.\n * @returns The account, if no duplicate is found.\n */\n async #checkForDuplicate(\n type: string,\n newAccountArray: string[],\n ): Promise {\n const accounts = await this.#getAccountsFromKeyrings();\n\n switch (type) {\n case KeyringTypes.simple: {\n const isIncluded = Boolean(\n accounts.find(\n (key) =>\n newAccountArray[0] &&\n (key === newAccountArray[0] ||\n key === remove0x(newAccountArray[0])),\n ),\n );\n\n if (isIncluded) {\n throw new Error(KeyringControllerError.DuplicatedAccount);\n }\n return newAccountArray;\n }\n\n default: {\n return newAccountArray;\n }\n }\n }\n\n /**\n * Set the `isUnlocked` to true and notify listeners\n * through the messenger.\n *\n * @fires KeyringController:unlock\n */\n #setUnlocked(): void {\n this.#assertControllerMutexIsLocked();\n\n this.update((state) => {\n state.isUnlocked = true;\n });\n this.messagingSystem.publish(`${name}:unlock`);\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and save the keyrings to state after it, or rollback to their\n * previous state in case of error.\n *\n * @param fn - The function to execute.\n * @returns The result of the function.\n */\n async #persistOrRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withRollback(async ({ releaseLock }) => {\n const callbackResult = await fn({ releaseLock });\n // State is committed only if the operation is successful\n await this.#updateVault();\n\n return callbackResult;\n });\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and rollback keyrings and password states in case of error.\n *\n * @param fn - The function to execute atomically.\n * @returns The result of the function.\n */\n async #withRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withControllerLock(async ({ releaseLock }) => {\n const currentSerializedKeyrings = await this.#getSerializedKeyrings();\n const currentPassword = this.#password;\n\n try {\n return await fn({ releaseLock });\n } catch (e) {\n // Keyrings and password are restored to their previous state\n await this.#restoreSerializedKeyrings(currentSerializedKeyrings);\n this.#password = currentPassword;\n\n throw e;\n }\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(KeyringControllerError.ControllerLockRequired);\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param fn - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock(fn: MutuallyExclusiveCallback): Promise {\n return withLock(this.#controllerOperationMutex, fn);\n }\n\n /**\n * Lock the vault mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This ensures that each operation that interacts with the vault\n * is executed in a mutually exclusive way.\n *\n * @param fn - The function to execute while the vault mutex is locked.\n * @returns The result of the function.\n */\n async #withVaultLock(fn: MutuallyExclusiveCallback): Promise {\n this.#assertControllerMutexIsLocked();\n\n return withLock(this.#vaultOperationMutex, fn);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param fn - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock(\n mutex: Mutex,\n fn: MutuallyExclusiveCallback,\n): Promise {\n const releaseLock = await mutex.acquire();\n\n try {\n return await fn({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n\nexport default KeyringController;\n"]} -\ No newline at end of file -diff --git a/dist/chunk-STFS4REY.mjs b/dist/chunk-STFS4REY.mjs -deleted file mode 100644 -index 58e38b7016380f616d2bed694a35a81f639d304b..0000000000000000000000000000000000000000 ---- a/dist/chunk-STFS4REY.mjs -+++ /dev/null -@@ -1,1500 +0,0 @@ --import { -- __privateAdd, -- __privateGet, -- __privateMethod, -- __privateSet --} from "./chunk-F64I344Z.mjs"; -- --// src/KeyringController.ts --import { isValidPrivate, toBuffer, getBinarySize } from "@ethereumjs/util"; --import { BaseController } from "@metamask/base-controller"; --import * as encryptorUtils from "@metamask/browser-passworder"; --import HDKeyring from "@metamask/eth-hd-keyring"; --import { normalize as ethNormalize } from "@metamask/eth-sig-util"; --import SimpleKeyring from "@metamask/eth-simple-keyring"; --import { -- add0x, -- assertIsStrictHexString, -- bytesToHex, -- hasProperty, -- isObject, -- isStrictHexString, -- isValidHexAddress, -- isValidJson, -- remove0x --} from "@metamask/utils"; --import { Mutex } from "async-mutex"; --import Wallet, { thirdparty as importers } from "ethereumjs-wallet"; --var name = "KeyringController"; --var KeyringTypes = /* @__PURE__ */ ((KeyringTypes2) => { -- KeyringTypes2["simple"] = "Simple Key Pair"; -- KeyringTypes2["hd"] = "HD Key Tree"; -- KeyringTypes2["qr"] = "QR Hardware Wallet Device"; -- KeyringTypes2["trezor"] = "Trezor Hardware"; -- KeyringTypes2["ledger"] = "Ledger Hardware"; -- KeyringTypes2["lattice"] = "Lattice Hardware"; -- KeyringTypes2["snap"] = "Snap Keyring"; -- return KeyringTypes2; --})(KeyringTypes || {}); --var isCustodyKeyring = (keyringType) => { -- return keyringType.startsWith("Custody"); --}; --var AccountImportStrategy = /* @__PURE__ */ ((AccountImportStrategy2) => { -- AccountImportStrategy2["privateKey"] = "privateKey"; -- AccountImportStrategy2["json"] = "json"; -- return AccountImportStrategy2; --})(AccountImportStrategy || {}); --var SignTypedDataVersion = /* @__PURE__ */ ((SignTypedDataVersion2) => { -- SignTypedDataVersion2["V1"] = "V1"; -- SignTypedDataVersion2["V3"] = "V3"; -- SignTypedDataVersion2["V4"] = "V4"; -- return SignTypedDataVersion2; --})(SignTypedDataVersion || {}); --function keyringBuilderFactory(KeyringConstructor) { -- const builder = () => new KeyringConstructor(); -- builder.type = KeyringConstructor.type; -- return builder; --} --var defaultKeyringBuilders = [ -- keyringBuilderFactory(SimpleKeyring), -- keyringBuilderFactory(HDKeyring) --]; --var getDefaultKeyringState = () => { -- return { -- isUnlocked: false, -- keyrings: [] -- }; --}; --function assertHasUint8ArrayMnemonic(keyring) { -- if (!(hasProperty(keyring, "mnemonic") && keyring.mnemonic instanceof Uint8Array)) { -- throw new Error("Can't get mnemonic bytes from keyring"); -- } --} --function assertIsExportableKeyEncryptor(encryptor) { -- if (!("importKey" in encryptor && typeof encryptor.importKey === "function" && "decryptWithKey" in encryptor && typeof encryptor.decryptWithKey === "function" && "encryptWithKey" in encryptor && typeof encryptor.encryptWithKey === "function")) { -- throw new Error("KeyringController - The encryptor does not support encryption key export." /* UnsupportedEncryptionKeyExport */); -- } --} --function assertIsValidPassword(password) { -- if (typeof password !== "string") { -- throw new Error("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- if (!password || !password.length) { -- throw new Error("KeyringController - Password cannot be empty." /* InvalidEmptyPassword */); -- } --} --function isSerializedKeyringsArray(array) { -- return typeof array === "object" && Array.isArray(array) && array.every((value) => value.type && isValidJson(value.data)); --} --async function displayForKeyring(keyring) { -- const accounts = await keyring.getAccounts(); -- return { -- type: keyring.type, -- // Cast to `string[]` here is safe here because `accounts` has no nullish -- // values, and `normalize` returns `string` unless given a nullish value -- accounts: accounts.map(normalize) -- }; --} --function isEthAddress(address) { -- return ( -- // NOTE: This function only checks for lowercased strings -- isStrictHexString(address.toLowerCase()) && // This checks for lowercased addresses and checksum addresses too -- isValidHexAddress(address) -- ); --} --function normalize(address) { -- return isEthAddress(address) ? ethNormalize(address) : address; --} --var _controllerOperationMutex, _vaultOperationMutex, _keyringBuilders, _keyrings, _unsupportedKeyrings, _password, _encryptor, _cacheEncryptionKey, _qrKeyringStateListener, _registerMessageHandlers, registerMessageHandlers_fn, _getKeyringBuilderForType, getKeyringBuilderForType_fn, _addQRKeyring, addQRKeyring_fn, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn, _getUpdatedKeyrings, getUpdatedKeyrings_fn, _getSerializedKeyrings, getSerializedKeyrings_fn, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn, _unlockKeyrings, unlockKeyrings_fn, _updateVault, updateVault_fn, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn, _newKeyring, newKeyring_fn, _clearKeyrings, clearKeyrings_fn, _restoreKeyring, restoreKeyring_fn, _destroyKeyring, destroyKeyring_fn, _removeEmptyKeyrings, removeEmptyKeyrings_fn, _checkForDuplicate, checkForDuplicate_fn, _setUnlocked, setUnlocked_fn, _persistOrRollback, persistOrRollback_fn, _withRollback, withRollback_fn, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn, _withControllerLock, withControllerLock_fn, _withVaultLock, withVaultLock_fn; --var KeyringController = class extends BaseController { -- /** -- * Creates a KeyringController instance. -- * -- * @param options - Initial options used to configure this controller -- * @param options.encryptor - An optional object for defining encryption schemes. -- * @param options.keyringBuilders - Set a new name for account. -- * @param options.cacheEncryptionKey - Whether to cache or not encryption key. -- * @param options.messenger - A restricted controller messenger. -- * @param options.state - Initial state to set on this controller. -- */ -- constructor(options) { -- const { -- encryptor = encryptorUtils, -- keyringBuilders, -- messenger, -- state -- } = options; -- super({ -- name, -- metadata: { -- vault: { persist: true, anonymous: false }, -- isUnlocked: { persist: false, anonymous: true }, -- keyrings: { persist: false, anonymous: false }, -- encryptionKey: { persist: false, anonymous: false }, -- encryptionSalt: { persist: false, anonymous: false } -- }, -- messenger, -- state: { -- ...getDefaultKeyringState(), -- ...state -- } -- }); -- /** -- * Constructor helper for registering this controller's messaging system -- * actions. -- */ -- __privateAdd(this, _registerMessageHandlers); -- /** -- * Get the keyring builder for the given `type`. -- * -- * @param type - The type of keyring to get the builder for. -- * @returns The keyring builder, or undefined if none exists. -- */ -- __privateAdd(this, _getKeyringBuilderForType); -- /** -- * Add qr hardware keyring. -- * -- * @returns The added keyring -- * @throws If a QRKeyring builder is not provided -- * when initializing the controller -- */ -- __privateAdd(this, _addQRKeyring); -- /** -- * Subscribe to a QRKeyring state change events and -- * forward them through the messaging system. -- * -- * @param qrKeyring - The QRKeyring instance to subscribe to -- */ -- __privateAdd(this, _subscribeToQRKeyringEvents); -- __privateAdd(this, _unsubscribeFromQRKeyringsEvents); -- /** -- * Create new vault with an initial keyring -- * -- * Destroys any old encrypted storage, -- * creates a new encrypted store with the given password, -- * creates a new wallet with 1 account. -- * -- * @fires KeyringController:unlock -- * @param password - The password to encrypt the vault with. -- * @param keyring - A object containing the params to instantiate a new keyring. -- * @param keyring.type - The keyring type. -- * @param keyring.opts - Optional parameters required to instantiate the keyring. -- * @returns A promise that resolves to the state. -- */ -- __privateAdd(this, _createNewVaultWithKeyring); -- /** -- * Get the updated array of each keyring's type and -- * accounts list. -- * -- * @returns A promise resolving to the updated keyrings array. -- */ -- __privateAdd(this, _getUpdatedKeyrings); -- /** -- * Serialize the current array of keyring instances, -- * including unsupported keyrings by default. -- * -- * @param options - Method options. -- * @param options.includeUnsupported - Whether to include unsupported keyrings. -- * @returns The serialized keyrings. -- */ -- __privateAdd(this, _getSerializedKeyrings); -- /** -- * Restore a serialized keyrings array. -- * -- * @param serializedKeyrings - The serialized keyrings array. -- */ -- __privateAdd(this, _restoreSerializedKeyrings); -- /** -- * Unlock Keyrings, decrypting the vault and deserializing all -- * keyrings contained in it, using a password or an encryption key with salt. -- * -- * @param password - The keyring controller password. -- * @param encryptionKey - An exported key string to unlock keyrings with. -- * @param encryptionSalt - The salt used to encrypt the vault. -- * @returns A promise resolving to the deserialized keyrings array. -- */ -- __privateAdd(this, _unlockKeyrings); -- /** -- * Update the vault with the current keyrings. -- * -- * @returns A promise resolving to `true` if the operation is successful. -- */ -- __privateAdd(this, _updateVault); -- /** -- * Retrieves all the accounts from keyrings instances -- * that are currently in memory. -- * -- * @returns A promise resolving to an array of accounts. -- */ -- __privateAdd(this, _getAccountsFromKeyrings); -- /** -- * Create a new keyring, ensuring that the first account is -- * also created. -- * -- * @param type - Keyring type to instantiate. -- * @param opts - Optional parameters required to instantiate the keyring. -- * @returns A promise that resolves if the operation is successful. -- */ -- __privateAdd(this, _createKeyringWithFirstAccount); -- /** -- * Instantiate, initialize and return a new keyring of the given `type`, -- * using the given `opts`. The keyring is built using the keyring builder -- * registered for the given `type`. -- * -- * -- * @param type - The type of keyring to add. -- * @param data - The data to restore a previously serialized keyring. -- * @returns The new keyring. -- * @throws If the keyring includes duplicated accounts. -- */ -- __privateAdd(this, _newKeyring); -- /** -- * Remove all managed keyrings, destroying all their -- * instances in memory. -- */ -- __privateAdd(this, _clearKeyrings); -- /** -- * Restore a Keyring from a provided serialized payload. -- * On success, returns the resulting keyring instance. -- * -- * @param serialized - The serialized keyring. -- * @returns The deserialized keyring or undefined if the keyring type is unsupported. -- */ -- __privateAdd(this, _restoreKeyring); -- /** -- * Destroy Keyring -- * -- * Some keyrings support a method called `destroy`, that destroys the -- * keyring along with removing all its event listeners and, in some cases, -- * clears the keyring bridge iframe from the DOM. -- * -- * @param keyring - The keyring to destroy. -- */ -- __privateAdd(this, _destroyKeyring); -- /** -- * Remove empty keyrings. -- * -- * Loops through the keyrings and removes the ones with empty accounts -- * (usually after removing the last / only account) from a keyring. -- */ -- __privateAdd(this, _removeEmptyKeyrings); -- /** -- * Checks for duplicate keypairs, using the the first account in the given -- * array. Rejects if a duplicate is found. -- * -- * Only supports 'Simple Key Pair'. -- * -- * @param type - The key pair type to check for. -- * @param newAccountArray - Array of new accounts. -- * @returns The account, if no duplicate is found. -- */ -- __privateAdd(this, _checkForDuplicate); -- /** -- * Set the `isUnlocked` to true and notify listeners -- * through the messenger. -- * -- * @fires KeyringController:unlock -- */ -- __privateAdd(this, _setUnlocked); -- /** -- * Execute the given function after acquiring the controller lock -- * and save the keyrings to state after it, or rollback to their -- * previous state in case of error. -- * -- * @param fn - The function to execute. -- * @returns The result of the function. -- */ -- __privateAdd(this, _persistOrRollback); -- /** -- * Execute the given function after acquiring the controller lock -- * and rollback keyrings and password states in case of error. -- * -- * @param fn - The function to execute atomically. -- * @returns The result of the function. -- */ -- __privateAdd(this, _withRollback); -- /** -- * Assert that the controller mutex is locked. -- * -- * @throws If the controller mutex is not locked. -- */ -- __privateAdd(this, _assertControllerMutexIsLocked); -- /** -- * Lock the controller mutex before executing the given function, -- * and release it after the function is resolved or after an -- * error is thrown. -- * -- * This wrapper ensures that each mutable operation that interacts with the -- * controller and that changes its state is executed in a mutually exclusive way, -- * preventing unsafe concurrent access that could lead to unpredictable behavior. -- * -- * @param fn - The function to execute while the controller mutex is locked. -- * @returns The result of the function. -- */ -- __privateAdd(this, _withControllerLock); -- /** -- * Lock the vault mutex before executing the given function, -- * and release it after the function is resolved or after an -- * error is thrown. -- * -- * This ensures that each operation that interacts with the vault -- * is executed in a mutually exclusive way. -- * -- * @param fn - The function to execute while the vault mutex is locked. -- * @returns The result of the function. -- */ -- __privateAdd(this, _withVaultLock); -- __privateAdd(this, _controllerOperationMutex, new Mutex()); -- __privateAdd(this, _vaultOperationMutex, new Mutex()); -- __privateAdd(this, _keyringBuilders, void 0); -- __privateAdd(this, _keyrings, void 0); -- __privateAdd(this, _unsupportedKeyrings, void 0); -- __privateAdd(this, _password, void 0); -- __privateAdd(this, _encryptor, void 0); -- __privateAdd(this, _cacheEncryptionKey, void 0); -- __privateAdd(this, _qrKeyringStateListener, void 0); -- __privateSet(this, _keyringBuilders, keyringBuilders ? defaultKeyringBuilders.concat(keyringBuilders) : defaultKeyringBuilders); -- __privateSet(this, _encryptor, encryptor); -- __privateSet(this, _keyrings, []); -- __privateSet(this, _unsupportedKeyrings, []); -- __privateSet(this, _cacheEncryptionKey, Boolean(options.cacheEncryptionKey)); -- if (__privateGet(this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(encryptor); -- } -- __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -- } -- /** -- * Adds a new account to the default (first) HD seed phrase keyring. -- * -- * @param accountCount - Number of accounts before adding a new one, used to -- * make the method idempotent. -- * @returns Promise resolving to the added account address. -- */ -- async addNewAccount(accountCount) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found"); -- } -- const oldAccounts = await primaryKeyring.getAccounts(); -- if (accountCount && oldAccounts.length !== accountCount) { -- if (accountCount > oldAccounts.length) { -- throw new Error("Account out of sequence"); -- } -- const existingAccount = oldAccounts[accountCount]; -- if (!existingAccount) { -- throw new Error(`Can't find account at index ${accountCount}`); -- } -- return existingAccount; -- } -- const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -- await this.verifySeedPhrase(); -- return addedAccountAddress; -- }); -- } -- /** -- * Adds a new account to the specified keyring. -- * -- * @param keyring - Keyring to add the account to. -- * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent. -- * @returns Promise resolving to the added account address -- */ -- async addNewAccountForKeyring(keyring, accountCount) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const oldAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- if (accountCount && oldAccounts.length !== accountCount) { -- if (accountCount > oldAccounts.length) { -- throw new Error("Account out of sequence"); -- } -- const existingAccount = oldAccounts[accountCount]; -- assertIsStrictHexString(existingAccount); -- return existingAccount; -- } -- await keyring.addAccounts(1); -- const addedAccountAddress = (await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this)).find( -- (selectedAddress) => !oldAccounts.includes(selectedAddress) -- ); -- assertIsStrictHexString(addedAccountAddress); -- return addedAccountAddress; -- }); -- } -- /** -- * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences. -- * -- * @returns Promise resolving to the added account address. -- */ -- async addNewAccountWithoutUpdate() { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree")[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found"); -- } -- const [addedAccountAddress] = await primaryKeyring.addAccounts(1); -- await this.verifySeedPhrase(); -- return addedAccountAddress; -- }); -- } -- /** -- * Effectively the same as creating a new keychain then populating it -- * using the given seed phrase. -- * -- * @param password - Password to unlock keychain. -- * @param seed - A BIP39-compliant seed phrase as Uint8Array, -- * either as a string or an array of UTF-8 bytes that represent the string. -- * @returns Promise resolving when the operation ends successfully. -- */ -- async createNewVaultAndRestore(password, seed) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- assertIsValidPassword(password); -- await __privateMethod(this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -- type: "HD Key Tree" /* hd */, -- opts: { -- mnemonic: seed, -- numberOfAccounts: 1 -- } -- }); -- }); -- } -- /** -- * Create a new primary keychain and wipe any previous keychains. -- * -- * @param password - Password to unlock the new vault. -- * @returns Promise resolving when the operation ends successfully. -- */ -- async createNewVaultAndKeychain(password) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const accounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- if (!accounts.length) { -- await __privateMethod(this, _createNewVaultWithKeyring, createNewVaultWithKeyring_fn).call(this, password, { -- type: "HD Key Tree" /* hd */ -- }); -- } -- }); -- } -- /** -- * Adds a new keyring of the given `type`. -- * -- * @param type - Keyring type name. -- * @param opts - Keyring options. -- * @throws If a builder for the given `type` does not exist. -- * @returns Promise resolving to the added keyring. -- */ -- async addNewKeyring(type, opts) { -- if (type === "QR Hardware Wallet Device" /* qr */) { -- return this.getOrAddQRKeyring(); -- } -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, opts)); -- } -- /** -- * Method to verify a given password validity. Throws an -- * error if the password is invalid. -- * -- * @param password - Password of the keyring. -- */ -- async verifyPassword(password) { -- if (!this.state.vault) { -- throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -- } -- await __privateGet(this, _encryptor).decrypt(password, this.state.vault); -- } -- /** -- * Returns the status of the vault. -- * -- * @returns Boolean returning true if the vault is unlocked. -- */ -- isUnlocked() { -- return this.state.isUnlocked; -- } -- /** -- * Gets the seed phrase of the HD keyring. -- * -- * @param password - Password of the keyring. -- * @returns Promise resolving to the seed phrase. -- */ -- async exportSeedPhrase(password) { -- await this.verifyPassword(password); -- assertHasUint8ArrayMnemonic(__privateGet(this, _keyrings)[0]); -- return __privateGet(this, _keyrings)[0].mnemonic; -- } -- /** -- * Gets the private key from the keyring controlling an address. -- * -- * @param password - Password of the keyring. -- * @param address - Address to export. -- * @returns Promise resolving to the private key for an address. -- */ -- async exportAccount(password, address) { -- await this.verifyPassword(password); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.exportAccount) { -- throw new Error("`KeyringController - The keyring for the current address does not support the method exportAccount" /* UnsupportedExportAccount */); -- } -- return await keyring.exportAccount(normalize(address)); -- } -- /** -- * Returns the public addresses of all accounts from every keyring. -- * -- * @returns A promise resolving to an array of addresses. -- */ -- async getAccounts() { -- return this.state.keyrings.reduce( -- (accounts, keyring) => accounts.concat(keyring.accounts), -- [] -- ); -- } -- /** -- * Get encryption public key. -- * -- * @param account - An account address. -- * @param opts - Additional encryption options. -- * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method -- * @returns Promise resolving to encyption public key of the `account` if one exists. -- */ -- async getEncryptionPublicKey(account, opts) { -- const address = ethNormalize(account); -- const keyring = await this.getKeyringForAccount( -- account -- ); -- if (!keyring.getEncryptionPublicKey) { -- throw new Error("KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey." /* UnsupportedGetEncryptionPublicKey */); -- } -- return await keyring.getEncryptionPublicKey(address, opts); -- } -- /** -- * Attempts to decrypt the provided message parameters. -- * -- * @param messageParams - The decryption message parameters. -- * @param messageParams.from - The address of the account you want to use to decrypt the message. -- * @param messageParams.data - The encrypted data that you want to decrypt. -- * @returns The raw decryption result. -- */ -- async decryptMessage(messageParams) { -- const address = ethNormalize(messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.decryptMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method decryptMessage." /* UnsupportedDecryptMessage */); -- } -- return keyring.decryptMessage(address, messageParams.data); -- } -- /** -- * Returns the currently initialized keyring that manages -- * the specified `address` if one exists. -- * -- * @deprecated Use of this method is discouraged as actions executed directly on -- * keyrings are not being reflected in the KeyringController state and not -- * persisted in the vault. Use `withKeyring` instead. -- * @param account - An account address. -- * @returns Promise resolving to keyring of the `account` if one exists. -- */ -- async getKeyringForAccount(account) { -- const address = normalize(account); -- const candidates = await Promise.all( -- __privateGet(this, _keyrings).map(async (keyring) => { -- return Promise.all([keyring, keyring.getAccounts()]); -- }) -- ); -- const winners = candidates.filter((candidate) => { -- const accounts = candidate[1].map(normalize); -- return accounts.includes(address); -- }); -- if (winners.length && winners[0]?.length) { -- return winners[0][0]; -- } -- let errorInfo = ""; -- if (!candidates.length) { -- errorInfo = "There are no keyrings"; -- } else if (!winners.length) { -- errorInfo = "There are keyrings, but none match the address"; -- } -- throw new Error( -- `${"KeyringController - No keyring found" /* NoKeyring */}. Error info: ${errorInfo}` -- ); -- } -- /** -- * Returns all keyrings of the given type. -- * -- * @deprecated Use of this method is discouraged as actions executed directly on -- * keyrings are not being reflected in the KeyringController state and not -- * persisted in the vault. Use `withKeyring` instead. -- * @param type - Keyring type name. -- * @returns An array of keyrings of the given type. -- */ -- getKeyringsByType(type) { -- return __privateGet(this, _keyrings).filter((keyring) => keyring.type === type); -- } -- /** -- * Persist all serialized keyrings in the vault. -- * -- * @deprecated This method is being phased out in favor of `withKeyring`. -- * @returns Promise resolving with `true` value when the -- * operation completes. -- */ -- async persistAllKeyrings() { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => true); -- } -- /** -- * Imports an account with the specified import strategy. -- * -- * @param strategy - Import strategy name. -- * @param args - Array of arguments to pass to the underlying stategy. -- * @throws Will throw when passed an unrecognized strategy. -- * @returns Promise resolving to the imported account address. -- */ -- async importAccountWithStrategy(strategy, args) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- let privateKey; -- switch (strategy) { -- case "privateKey": -- const [importedKey] = args; -- if (!importedKey) { -- throw new Error("Cannot import an empty key."); -- } -- const prefixed = add0x(importedKey); -- let bufferedPrivateKey; -- try { -- bufferedPrivateKey = toBuffer(prefixed); -- } catch { -- throw new Error("Cannot import invalid private key."); -- } -- if (!isValidPrivate(bufferedPrivateKey) || // ensures that the key is 64 bytes long -- getBinarySize(prefixed) !== 64 + "0x".length) { -- throw new Error("Cannot import invalid private key."); -- } -- privateKey = remove0x(prefixed); -- break; -- case "json": -- let wallet; -- const [input, password] = args; -- try { -- wallet = importers.fromEtherWallet(input, password); -- } catch (e) { -- wallet = wallet || await Wallet.fromV3(input, password, true); -- } -- privateKey = bytesToHex(wallet.getPrivateKey()); -- break; -- default: -- throw new Error(`Unexpected import strategy: '${strategy}'`); -- } -- const newKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "Simple Key Pair" /* simple */, [ -- privateKey -- ]); -- const accounts = await newKeyring.getAccounts(); -- return accounts[0]; -- }); -- } -- /** -- * Removes an account from keyring state. -- * -- * @param address - Address of the account to remove. -- * @fires KeyringController:accountRemoved -- * @returns Promise resolving when the account is removed. -- */ -- async removeAccount(address) { -- await __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.removeAccount) { -- throw new Error("`KeyringController - The keyring for the current address does not support the method removeAccount" /* UnsupportedRemoveAccount */); -- } -- await keyring.removeAccount(address); -- const accounts = await keyring.getAccounts(); -- if (accounts.length === 0) { -- await __privateMethod(this, _removeEmptyKeyrings, removeEmptyKeyrings_fn).call(this); -- } -- }); -- this.messagingSystem.publish(`${name}:accountRemoved`, address); -- } -- /** -- * Deallocates all secrets and locks the wallet. -- * -- * @returns Promise resolving when the operation completes. -- */ -- async setLocked() { -- return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -- __privateMethod(this, _unsubscribeFromQRKeyringsEvents, unsubscribeFromQRKeyringsEvents_fn).call(this); -- __privateSet(this, _password, void 0); -- await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -- this.update((state) => { -- state.isUnlocked = false; -- state.keyrings = []; -- }); -- this.messagingSystem.publish(`${name}:lock`); -- }); -- } -- /** -- * Signs message by calling down into a specific keyring. -- * -- * @param messageParams - PersonalMessageParams object to sign. -- * @returns Promise resolving to a signed message string. -- */ -- async signMessage(messageParams) { -- if (!messageParams.data) { -- throw new Error("Can't sign an empty message"); -- } -- const address = ethNormalize(messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signMessage." /* UnsupportedSignMessage */); -- } -- return await keyring.signMessage(address, messageParams.data); -- } -- /** -- * Signs personal message by calling down into a specific keyring. -- * -- * @param messageParams - PersonalMessageParams object to sign. -- * @returns Promise resolving to a signed message string. -- */ -- async signPersonalMessage(messageParams) { -- const address = ethNormalize(messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signPersonalMessage) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signPersonalMessage." /* UnsupportedSignPersonalMessage */); -- } -- const normalizedData = normalize(messageParams.data); -- return await keyring.signPersonalMessage(address, normalizedData); -- } -- /** -- * Signs typed message by calling down into a specific keyring. -- * -- * @param messageParams - TypedMessageParams object to sign. -- * @param version - Compatibility version EIP712. -- * @throws Will throw when passed an unrecognized version. -- * @returns Promise resolving to a signed message string or an error if any. -- */ -- async signTypedMessage(messageParams, version) { -- try { -- if (![ -- "V1" /* V1 */, -- "V3" /* V3 */, -- "V4" /* V4 */ -- ].includes(version)) { -- throw new Error(`Unexpected signTypedMessage version: '${version}'`); -- } -- const address = ethNormalize(messageParams.from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signTypedData) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signTypedMessage." /* UnsupportedSignTypedMessage */); -- } -- return await keyring.signTypedData( -- address, -- version !== "V1" /* V1 */ && typeof messageParams.data === "string" ? JSON.parse(messageParams.data) : messageParams.data, -- { version } -- ); -- } catch (error) { -- throw new Error(`Keyring Controller signTypedMessage: ${error}`); -- } -- } -- /** -- * Signs a transaction by calling down into a specific keyring. -- * -- * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance. -- * @param from - Address to sign from, should be in keychain. -- * @param opts - An optional options object. -- * @returns Promise resolving to a signed transaction string. -- */ -- async signTransaction(transaction, from, opts) { -- const address = ethNormalize(from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signTransaction) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signTransaction." /* UnsupportedSignTransaction */); -- } -- return await keyring.signTransaction(address, transaction, opts); -- } -- /** -- * Convert a base transaction to a base UserOperation. -- * -- * @param from - Address of the sender. -- * @param transactions - Base transactions to include in the UserOperation. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns A pseudo-UserOperation that can be used to construct a real. -- */ -- async prepareUserOperation(from, transactions, executionContext) { -- const address = ethNormalize(from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.prepareUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method prepareUserOperation." /* UnsupportedPrepareUserOperation */); -- } -- return await keyring.prepareUserOperation( -- address, -- transactions, -- executionContext -- ); -- } -- /** -- * Patches properties of a UserOperation. Currently, only the -- * `paymasterAndData` can be patched. -- * -- * @param from - Address of the sender. -- * @param userOp - UserOperation to patch. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns A patch to apply to the UserOperation. -- */ -- async patchUserOperation(from, userOp, executionContext) { -- const address = ethNormalize(from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.patchUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method patchUserOperation." /* UnsupportedPatchUserOperation */); -- } -- return await keyring.patchUserOperation(address, userOp, executionContext); -- } -- /** -- * Signs an UserOperation. -- * -- * @param from - Address of the sender. -- * @param userOp - UserOperation to sign. -- * @param executionContext - The execution context to use for the UserOperation. -- * @returns The signature of the UserOperation. -- */ -- async signUserOperation(from, userOp, executionContext) { -- const address = ethNormalize(from); -- const keyring = await this.getKeyringForAccount( -- address -- ); -- if (!keyring.signUserOperation) { -- throw new Error("KeyringController - The keyring for the current address does not support the method signUserOperation." /* UnsupportedSignUserOperation */); -- } -- return await keyring.signUserOperation(address, userOp, executionContext); -- } -- /** -- * Changes the password used to encrypt the vault. -- * -- * @param password - The new password. -- * @returns Promise resolving when the operation completes. -- */ -- changePassword(password) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- if (!this.state.isUnlocked) { -- throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -- } -- assertIsValidPassword(password); -- __privateSet(this, _password, password); -- if (__privateGet(this, _cacheEncryptionKey)) { -- this.update((state) => { -- delete state.encryptionKey; -- delete state.encryptionSalt; -- }); -- } -- }); -- } -- /** -- * Attempts to decrypt the current vault and load its keyrings, -- * using the given encryption key and salt. -- * -- * @param encryptionKey - Key to unlock the keychain. -- * @param encryptionSalt - Salt to unlock the keychain. -- * @returns Promise resolving when the operation completes. -- */ -- async submitEncryptionKey(encryptionKey, encryptionSalt) { -- return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -- __privateSet(this, _keyrings, await __privateMethod(this, _unlockKeyrings, unlockKeyrings_fn).call(this, void 0, encryptionKey, encryptionSalt)); -- __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); -- }); -- } -- /** -- * Attempts to decrypt the current vault and load its keyrings, -- * using the given password. -- * -- * @param password - Password to unlock the keychain. -- * @returns Promise resolving when the operation completes. -- */ -- async submitPassword(password) { -- return __privateMethod(this, _withRollback, withRollback_fn).call(this, async () => { -- __privateSet(this, _keyrings, await __privateMethod(this, _unlockKeyrings, unlockKeyrings_fn).call(this, password)); -- __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); -- }); -- } -- /** -- * Verifies the that the seed phrase restores the current keychain's accounts. -- * -- * @returns Promise resolving to the seed phrase as Uint8Array. -- */ -- async verifySeedPhrase() { -- const primaryKeyring = this.getKeyringsByType("HD Key Tree" /* hd */)[0]; -- if (!primaryKeyring) { -- throw new Error("No HD keyring found."); -- } -- assertHasUint8ArrayMnemonic(primaryKeyring); -- const seedWords = primaryKeyring.mnemonic; -- const accounts = await primaryKeyring.getAccounts(); -- if (accounts.length === 0) { -- throw new Error("Cannot verify an empty keyring."); -- } -- const hdKeyringBuilder = __privateMethod(this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, "HD Key Tree" /* hd */); -- const hdKeyring = hdKeyringBuilder(); -- await hdKeyring.deserialize({ -- mnemonic: seedWords, -- numberOfAccounts: accounts.length -- }); -- const testAccounts = await hdKeyring.getAccounts(); -- if (testAccounts.length !== accounts.length) { -- throw new Error("Seed phrase imported incorrect number of accounts."); -- } -- testAccounts.forEach((account, i) => { -- if (account.toLowerCase() !== accounts[i].toLowerCase()) { -- throw new Error("Seed phrase imported different accounts."); -- } -- }); -- return seedWords; -- } -- async withKeyring(selector, operation, options = { -- createIfMissing: false -- }) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- let keyring; -- if ("address" in selector) { -- keyring = await this.getKeyringForAccount(selector.address); -- } else { -- keyring = this.getKeyringsByType(selector.type)[selector.index || 0]; -- if (!keyring && options.createIfMissing) { -- keyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, selector.type, options.createWithData); -- } -- } -- if (!keyring) { -- throw new Error("KeyringController - Keyring not found." /* KeyringNotFound */); -- } -- const result = await operation(keyring); -- if (Object.is(result, keyring)) { -- throw new Error("KeyringController - Returning keyring instances is unsafe" /* UnsafeDirectKeyringAccess */); -- } -- return result; -- }); -- } -- // QR Hardware related methods -- /** -- * Get QR Hardware keyring. -- * -- * @returns The QR Keyring if defined, otherwise undefined -- */ -- getQRKeyring() { -- return this.getKeyringsByType("QR Hardware Wallet Device" /* qr */)[0]; -- } -- /** -- * Get QR hardware keyring. If it doesn't exist, add it. -- * -- * @returns The added keyring -- */ -- async getOrAddQRKeyring() { -- return this.getQRKeyring() || await __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this)); -- } -- // TODO: Replace `any` with type -- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- async restoreQRKeyring(serialized) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -- keyring.deserialize(serialized); -- }); -- } -- async resetQRKeyringState() { -- (await this.getOrAddQRKeyring()).resetStore(); -- } -- async getQRKeyringState() { -- return (await this.getOrAddQRKeyring()).getMemStore(); -- } -- async submitQRCryptoHDKey(cryptoHDKey) { -- (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey); -- } -- async submitQRCryptoAccount(cryptoAccount) { -- (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount); -- } -- async submitQRSignature(requestId, ethSignature) { -- (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature); -- } -- async cancelQRSignRequest() { -- (await this.getOrAddQRKeyring()).cancelSignRequest(); -- } -- /** -- * Cancels qr keyring sync. -- */ -- async cancelQRSynchronization() { -- (await this.getOrAddQRKeyring()).cancelSync(); -- } -- async connectQRHardware(page) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- try { -- const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -- let accounts; -- switch (page) { -- case -1: -- accounts = await keyring.getPreviousPage(); -- break; -- case 1: -- accounts = await keyring.getNextPage(); -- break; -- default: -- accounts = await keyring.getFirstPage(); -- } -- return accounts.map((account) => { -- return { -- ...account, -- balance: "0x0" -- }; -- }); -- } catch (e) { -- throw new Error(`Unspecified error when connect QR Hardware, ${e}`); -- } -- }); -- } -- async unlockQRHardwareWalletAccount(index) { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring() || await __privateMethod(this, _addQRKeyring, addQRKeyring_fn).call(this); -- keyring.setAccountToUnlock(index); -- await keyring.addAccounts(1); -- }); -- } -- async getAccountKeyringType(account) { -- const keyring = await this.getKeyringForAccount( -- account -- ); -- return keyring.type; -- } -- async forgetQRDevice() { -- return __privateMethod(this, _persistOrRollback, persistOrRollback_fn).call(this, async () => { -- const keyring = this.getQRKeyring(); -- if (!keyring) { -- return { removedAccounts: [], remainingAccounts: [] }; -- } -- const allAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- keyring.forgetDevice(); -- const remainingAccounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- const removedAccounts = allAccounts.filter( -- (address) => !remainingAccounts.includes(address) -- ); -- return { removedAccounts, remainingAccounts }; -- }); -- } --}; --_controllerOperationMutex = new WeakMap(); --_vaultOperationMutex = new WeakMap(); --_keyringBuilders = new WeakMap(); --_keyrings = new WeakMap(); --_unsupportedKeyrings = new WeakMap(); --_password = new WeakMap(); --_encryptor = new WeakMap(); --_cacheEncryptionKey = new WeakMap(); --_qrKeyringStateListener = new WeakMap(); --_registerMessageHandlers = new WeakSet(); --registerMessageHandlers_fn = function() { -- this.messagingSystem.registerActionHandler( -- `${name}:signMessage`, -- this.signMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signPersonalMessage`, -- this.signPersonalMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signTypedMessage`, -- this.signTypedMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:decryptMessage`, -- this.decryptMessage.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getEncryptionPublicKey`, -- this.getEncryptionPublicKey.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getAccounts`, -- this.getAccounts.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getKeyringsByType`, -- this.getKeyringsByType.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:getKeyringForAccount`, -- this.getKeyringForAccount.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:persistAllKeyrings`, -- this.persistAllKeyrings.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:prepareUserOperation`, -- this.prepareUserOperation.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:patchUserOperation`, -- this.patchUserOperation.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- `${name}:signUserOperation`, -- this.signUserOperation.bind(this) -- ); --}; --_getKeyringBuilderForType = new WeakSet(); --getKeyringBuilderForType_fn = function(type) { -- return __privateGet(this, _keyringBuilders).find( -- (keyringBuilder) => keyringBuilder.type === type -- ); --}; --_addQRKeyring = new WeakSet(); --addQRKeyring_fn = async function() { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- return await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */); --}; --_subscribeToQRKeyringEvents = new WeakSet(); --subscribeToQRKeyringEvents_fn = function(qrKeyring) { -- __privateSet(this, _qrKeyringStateListener, (state) => { -- this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state); -- }); -- qrKeyring.getMemStore().subscribe(__privateGet(this, _qrKeyringStateListener)); --}; --_unsubscribeFromQRKeyringsEvents = new WeakSet(); --unsubscribeFromQRKeyringsEvents_fn = function() { -- const qrKeyrings = this.getKeyringsByType( -- "QR Hardware Wallet Device" /* qr */ -- ); -- qrKeyrings.forEach((qrKeyring) => { -- if (__privateGet(this, _qrKeyringStateListener)) { -- qrKeyring.getMemStore().unsubscribe(__privateGet(this, _qrKeyringStateListener)); -- } -- }); --}; --_createNewVaultWithKeyring = new WeakSet(); --createNewVaultWithKeyring_fn = async function(password, keyring) { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- if (typeof password !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- __privateSet(this, _password, password); -- await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -- await __privateMethod(this, _createKeyringWithFirstAccount, createKeyringWithFirstAccount_fn).call(this, keyring.type, keyring.opts); -- __privateMethod(this, _setUnlocked, setUnlocked_fn).call(this); --}; --_getUpdatedKeyrings = new WeakSet(); --getUpdatedKeyrings_fn = async function() { -- return Promise.all(__privateGet(this, _keyrings).map(displayForKeyring)); --}; --_getSerializedKeyrings = new WeakSet(); --getSerializedKeyrings_fn = async function({ includeUnsupported } = { -- includeUnsupported: true --}) { -- const serializedKeyrings = await Promise.all( -- __privateGet(this, _keyrings).map(async (keyring) => { -- const [type, data] = await Promise.all([ -- keyring.type, -- keyring.serialize() -- ]); -- return { type, data }; -- }) -- ); -- if (includeUnsupported) { -- serializedKeyrings.push(...__privateGet(this, _unsupportedKeyrings)); -- } -- return serializedKeyrings; --}; --_restoreSerializedKeyrings = new WeakSet(); --restoreSerializedKeyrings_fn = async function(serializedKeyrings) { -- await __privateMethod(this, _clearKeyrings, clearKeyrings_fn).call(this); -- for (const serializedKeyring of serializedKeyrings) { -- await __privateMethod(this, _restoreKeyring, restoreKeyring_fn).call(this, serializedKeyring); -- } --}; --_unlockKeyrings = new WeakSet(); --unlockKeyrings_fn = async function(password, encryptionKey, encryptionSalt) { -- return __privateMethod(this, _withVaultLock, withVaultLock_fn).call(this, async ({ releaseLock }) => { -- const encryptedVault = this.state.vault; -- if (!encryptedVault) { -- throw new Error("KeyringController - Cannot unlock without a previous vault." /* VaultError */); -- } -- let vault; -- const updatedState = {}; -- if (__privateGet(this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(__privateGet(this, _encryptor)); -- if (password) { -- const result = await __privateGet(this, _encryptor).decryptWithDetail( -- password, -- encryptedVault -- ); -- vault = result.vault; -- __privateSet(this, _password, password); -- updatedState.encryptionKey = result.exportedKeyString; -- updatedState.encryptionSalt = result.salt; -- } else { -- const parsedEncryptedVault = JSON.parse(encryptedVault); -- if (encryptionSalt !== parsedEncryptedVault.salt) { -- throw new Error("KeyringController - Encryption key and salt provided are expired" /* ExpiredCredentials */); -- } -- if (typeof encryptionKey !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- const key = await __privateGet(this, _encryptor).importKey(encryptionKey); -- vault = await __privateGet(this, _encryptor).decryptWithKey( -- key, -- parsedEncryptedVault -- ); -- updatedState.encryptionKey = encryptionKey; -- updatedState.encryptionSalt = encryptionSalt; -- } -- } else { -- if (typeof password !== "string") { -- throw new TypeError("KeyringController - Password must be of type string." /* WrongPasswordType */); -- } -- vault = await __privateGet(this, _encryptor).decrypt(password, encryptedVault); -- __privateSet(this, _password, password); -- } -- if (!isSerializedKeyringsArray(vault)) { -- throw new Error("KeyringController - The decrypted vault has an unexpected shape." /* VaultDataError */); -- } -- await __privateMethod(this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, vault); -- const updatedKeyrings = await __privateMethod(this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -- this.update((state) => { -- state.keyrings = updatedKeyrings; -- if (updatedState.encryptionKey || updatedState.encryptionSalt) { -- state.encryptionKey = updatedState.encryptionKey; -- state.encryptionSalt = updatedState.encryptionSalt; -- } -- }); -- if (__privateGet(this, _password) && (!__privateGet(this, _cacheEncryptionKey) || !encryptionKey) && __privateGet(this, _encryptor).isVaultUpdated && !__privateGet(this, _encryptor).isVaultUpdated(encryptedVault)) { -- releaseLock(); -- await __privateMethod(this, _updateVault, updateVault_fn).call(this); -- } -- return __privateGet(this, _keyrings); -- }); --}; --_updateVault = new WeakSet(); --updateVault_fn = function() { -- return __privateMethod(this, _withVaultLock, withVaultLock_fn).call(this, async () => { -- const { encryptionKey, encryptionSalt } = this.state; -- if (!__privateGet(this, _password) && !encryptionKey) { -- throw new Error("KeyringController - Cannot persist vault without password and encryption key" /* MissingCredentials */); -- } -- const serializedKeyrings = await __privateMethod(this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -- if (!serializedKeyrings.some((keyring) => keyring.type === "HD Key Tree" /* hd */)) { -- throw new Error("KeyringController - No HD Keyring found" /* NoHdKeyring */); -- } -- const updatedState = {}; -- if (__privateGet(this, _cacheEncryptionKey)) { -- assertIsExportableKeyEncryptor(__privateGet(this, _encryptor)); -- if (encryptionKey) { -- const key = await __privateGet(this, _encryptor).importKey(encryptionKey); -- const vaultJSON = await __privateGet(this, _encryptor).encryptWithKey( -- key, -- serializedKeyrings -- ); -- vaultJSON.salt = encryptionSalt; -- updatedState.vault = JSON.stringify(vaultJSON); -- } else if (__privateGet(this, _password)) { -- const { vault: newVault, exportedKeyString } = await __privateGet(this, _encryptor).encryptWithDetail( -- __privateGet(this, _password), -- serializedKeyrings -- ); -- updatedState.vault = newVault; -- updatedState.encryptionKey = exportedKeyString; -- } -- } else { -- assertIsValidPassword(__privateGet(this, _password)); -- updatedState.vault = await __privateGet(this, _encryptor).encrypt( -- __privateGet(this, _password), -- serializedKeyrings -- ); -- } -- if (!updatedState.vault) { -- throw new Error("KeyringController - Cannot persist vault without vault information" /* MissingVaultData */); -- } -- const updatedKeyrings = await __privateMethod(this, _getUpdatedKeyrings, getUpdatedKeyrings_fn).call(this); -- this.update((state) => { -- state.vault = updatedState.vault; -- state.keyrings = updatedKeyrings; -- if (updatedState.encryptionKey) { -- state.encryptionKey = updatedState.encryptionKey; -- state.encryptionSalt = JSON.parse(updatedState.vault).salt; -- } -- }); -- return true; -- }); --}; --_getAccountsFromKeyrings = new WeakSet(); --getAccountsFromKeyrings_fn = async function() { -- const keyrings = __privateGet(this, _keyrings); -- const keyringArrays = await Promise.all( -- keyrings.map(async (keyring) => keyring.getAccounts()) -- ); -- const addresses = keyringArrays.reduce((res, arr) => { -- return res.concat(arr); -- }, []); -- return addresses.map(normalize); --}; --_createKeyringWithFirstAccount = new WeakSet(); --createKeyringWithFirstAccount_fn = async function(type, opts) { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const keyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, opts); -- const [firstAccount] = await keyring.getAccounts(); -- if (!firstAccount) { -- throw new Error("KeyringController - First Account not found." /* NoFirstAccount */); -- } --}; --_newKeyring = new WeakSet(); --newKeyring_fn = async function(type, data) { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const keyringBuilder = __privateMethod(this, _getKeyringBuilderForType, getKeyringBuilderForType_fn).call(this, type); -- if (!keyringBuilder) { -- throw new Error( -- `${"KeyringController - No keyringBuilder found for keyring" /* NoKeyringBuilder */}. Keyring type: ${type}` -- ); -- } -- const keyring = keyringBuilder(); -- await keyring.deserialize(data); -- if (keyring.init) { -- await keyring.init(); -- } -- if (type === "HD Key Tree" /* hd */ && (!isObject(data) || !data.mnemonic)) { -- if (!keyring.generateRandomMnemonic) { -- throw new Error( -- "KeyringController - The current keyring does not support the method generateRandomMnemonic." /* UnsupportedGenerateRandomMnemonic */ -- ); -- } -- keyring.generateRandomMnemonic(); -- await keyring.addAccounts(1); -- } -- await __privateMethod(this, _checkForDuplicate, checkForDuplicate_fn).call(this, type, await keyring.getAccounts()); -- if (type === "QR Hardware Wallet Device" /* qr */) { -- __privateMethod(this, _subscribeToQRKeyringEvents, subscribeToQRKeyringEvents_fn).call(this, keyring); -- } -- __privateGet(this, _keyrings).push(keyring); -- return keyring; --}; --_clearKeyrings = new WeakSet(); --clearKeyrings_fn = async function() { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- for (const keyring of __privateGet(this, _keyrings)) { -- await __privateMethod(this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -- } -- __privateSet(this, _keyrings, []); --}; --_restoreKeyring = new WeakSet(); --restoreKeyring_fn = async function(serialized) { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- try { -- const { type, data } = serialized; -- return await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, type, data); -- } catch (_) { -- __privateGet(this, _unsupportedKeyrings).push(serialized); -- return void 0; -- } --}; --_destroyKeyring = new WeakSet(); --destroyKeyring_fn = async function(keyring) { -- await keyring.destroy?.(); --}; --_removeEmptyKeyrings = new WeakSet(); --removeEmptyKeyrings_fn = async function() { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- const validKeyrings = []; -- await Promise.all( -- __privateGet(this, _keyrings).map(async (keyring) => { -- const accounts = await keyring.getAccounts(); -- if (accounts.length > 0) { -- validKeyrings.push(keyring); -- } else { -- await __privateMethod(this, _destroyKeyring, destroyKeyring_fn).call(this, keyring); -- } -- }) -- ); -- __privateSet(this, _keyrings, validKeyrings); --}; --_checkForDuplicate = new WeakSet(); --checkForDuplicate_fn = async function(type, newAccountArray) { -- const accounts = await __privateMethod(this, _getAccountsFromKeyrings, getAccountsFromKeyrings_fn).call(this); -- switch (type) { -- case "Simple Key Pair" /* simple */: { -- const isIncluded = Boolean( -- accounts.find( -- (key) => newAccountArray[0] && (key === newAccountArray[0] || key === remove0x(newAccountArray[0])) -- ) -- ); -- if (isIncluded) { -- throw new Error("KeyringController - The account you are trying to import is a duplicate" /* DuplicatedAccount */); -- } -- return newAccountArray; -- } -- default: { -- return newAccountArray; -- } -- } --}; --_setUnlocked = new WeakSet(); --setUnlocked_fn = function() { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- this.update((state) => { -- state.isUnlocked = true; -- }); -- this.messagingSystem.publish(`${name}:unlock`); --}; --_persistOrRollback = new WeakSet(); --persistOrRollback_fn = async function(fn) { -- return __privateMethod(this, _withRollback, withRollback_fn).call(this, async ({ releaseLock }) => { -- const callbackResult = await fn({ releaseLock }); -- await __privateMethod(this, _updateVault, updateVault_fn).call(this); -- return callbackResult; -- }); --}; --_withRollback = new WeakSet(); --withRollback_fn = async function(fn) { -- return __privateMethod(this, _withControllerLock, withControllerLock_fn).call(this, async ({ releaseLock }) => { -- const currentSerializedKeyrings = await __privateMethod(this, _getSerializedKeyrings, getSerializedKeyrings_fn).call(this); -- const currentPassword = __privateGet(this, _password); -- try { -- return await fn({ releaseLock }); -- } catch (e) { -- await __privateMethod(this, _restoreSerializedKeyrings, restoreSerializedKeyrings_fn).call(this, currentSerializedKeyrings); -- __privateSet(this, _password, currentPassword); -- throw e; -- } -- }); --}; --_assertControllerMutexIsLocked = new WeakSet(); --assertControllerMutexIsLocked_fn = function() { -- if (!__privateGet(this, _controllerOperationMutex).isLocked()) { -- throw new Error("KeyringController - attempt to update vault during a non mutually exclusive operation" /* ControllerLockRequired */); -- } --}; --_withControllerLock = new WeakSet(); --withControllerLock_fn = async function(fn) { -- return withLock(__privateGet(this, _controllerOperationMutex), fn); --}; --_withVaultLock = new WeakSet(); --withVaultLock_fn = async function(fn) { -- __privateMethod(this, _assertControllerMutexIsLocked, assertControllerMutexIsLocked_fn).call(this); -- return withLock(__privateGet(this, _vaultOperationMutex), fn); --}; --async function withLock(mutex, fn) { -- const releaseLock = await mutex.acquire(); -- try { -- return await fn({ releaseLock }); -- } finally { -- releaseLock(); -- } --} --var KeyringController_default = KeyringController; -- --export { -- KeyringTypes, -- isCustodyKeyring, -- AccountImportStrategy, -- SignTypedDataVersion, -- keyringBuilderFactory, -- getDefaultKeyringState, -- KeyringController, -- KeyringController_default --}; --//# sourceMappingURL=chunk-STFS4REY.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-STFS4REY.mjs.map b/dist/chunk-STFS4REY.mjs.map -deleted file mode 100644 -index 3ed91da3b2a1e05d7fc7decac48e949923ff760c..0000000000000000000000000000000000000000 ---- a/dist/chunk-STFS4REY.mjs.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/KeyringController.ts"],"sourcesContent":["import type { TxData, TypedTransaction } from '@ethereumjs/tx';\nimport { isValidPrivate, toBuffer, getBinarySize } from '@ethereumjs/util';\nimport type {\n MetaMaskKeyring as QRKeyring,\n IKeyringState as IQRKeyringState,\n} from '@keystonehq/metamask-airgapped-keyring';\nimport type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport * as encryptorUtils from '@metamask/browser-passworder';\nimport HDKeyring from '@metamask/eth-hd-keyring';\nimport { normalize as ethNormalize } from '@metamask/eth-sig-util';\nimport SimpleKeyring from '@metamask/eth-simple-keyring';\nimport type {\n EthBaseTransaction,\n EthBaseUserOperation,\n EthKeyring,\n EthUserOperation,\n EthUserOperationPatch,\n KeyringExecutionContext,\n} from '@metamask/keyring-api';\nimport type {\n PersonalMessageParams,\n TypedMessageParams,\n} from '@metamask/message-manager';\nimport type {\n Eip1024EncryptedData,\n Hex,\n Json,\n KeyringClass,\n} from '@metamask/utils';\nimport {\n add0x,\n assertIsStrictHexString,\n bytesToHex,\n hasProperty,\n isObject,\n isStrictHexString,\n isValidHexAddress,\n isValidJson,\n remove0x,\n} from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\nimport type { MutexInterface } from 'async-mutex';\nimport Wallet, { thirdparty as importers } from 'ethereumjs-wallet';\nimport type { Patch } from 'immer';\n\nimport { KeyringControllerError } from './constants';\n\nconst name = 'KeyringController';\n\n/**\n * Available keyring types\n */\nexport enum KeyringTypes {\n simple = 'Simple Key Pair',\n hd = 'HD Key Tree',\n qr = 'QR Hardware Wallet Device',\n trezor = 'Trezor Hardware',\n ledger = 'Ledger Hardware',\n lattice = 'Lattice Hardware',\n snap = 'Snap Keyring',\n}\n\n/**\n * Custody keyring types are a special case, as they are not a single type\n * but they all start with the prefix \"Custody\".\n * @param keyringType - The type of the keyring.\n * @returns Whether the keyring type is a custody keyring.\n */\nexport const isCustodyKeyring = (keyringType: string): boolean => {\n return keyringType.startsWith('Custody');\n};\n\n/**\n * @type KeyringControllerState\n *\n * Keyring controller state\n * @property vault - Encrypted string representing keyring data\n * @property isUnlocked - Whether vault is unlocked\n * @property keyringTypes - Account types\n * @property keyrings - Group of accounts\n * @property encryptionKey - Keyring encryption key\n * @property encryptionSalt - Keyring encryption salt\n */\nexport type KeyringControllerState = {\n vault?: string;\n isUnlocked: boolean;\n keyrings: KeyringObject[];\n encryptionKey?: string;\n encryptionSalt?: string;\n};\n\nexport type KeyringControllerMemState = Omit<\n KeyringControllerState,\n 'vault' | 'encryptionKey' | 'encryptionSalt'\n>;\n\nexport type KeyringControllerGetStateAction = {\n type: `${typeof name}:getState`;\n handler: () => KeyringControllerState;\n};\n\nexport type KeyringControllerSignMessageAction = {\n type: `${typeof name}:signMessage`;\n handler: KeyringController['signMessage'];\n};\n\nexport type KeyringControllerSignPersonalMessageAction = {\n type: `${typeof name}:signPersonalMessage`;\n handler: KeyringController['signPersonalMessage'];\n};\n\nexport type KeyringControllerSignTypedMessageAction = {\n type: `${typeof name}:signTypedMessage`;\n handler: KeyringController['signTypedMessage'];\n};\n\nexport type KeyringControllerDecryptMessageAction = {\n type: `${typeof name}:decryptMessage`;\n handler: KeyringController['decryptMessage'];\n};\n\nexport type KeyringControllerGetEncryptionPublicKeyAction = {\n type: `${typeof name}:getEncryptionPublicKey`;\n handler: KeyringController['getEncryptionPublicKey'];\n};\n\nexport type KeyringControllerGetKeyringsByTypeAction = {\n type: `${typeof name}:getKeyringsByType`;\n handler: KeyringController['getKeyringsByType'];\n};\n\nexport type KeyringControllerGetKeyringForAccountAction = {\n type: `${typeof name}:getKeyringForAccount`;\n handler: KeyringController['getKeyringForAccount'];\n};\n\nexport type KeyringControllerGetAccountsAction = {\n type: `${typeof name}:getAccounts`;\n handler: KeyringController['getAccounts'];\n};\n\nexport type KeyringControllerPersistAllKeyringsAction = {\n type: `${typeof name}:persistAllKeyrings`;\n handler: KeyringController['persistAllKeyrings'];\n};\n\nexport type KeyringControllerPrepareUserOperationAction = {\n type: `${typeof name}:prepareUserOperation`;\n handler: KeyringController['prepareUserOperation'];\n};\n\nexport type KeyringControllerPatchUserOperationAction = {\n type: `${typeof name}:patchUserOperation`;\n handler: KeyringController['patchUserOperation'];\n};\n\nexport type KeyringControllerSignUserOperationAction = {\n type: `${typeof name}:signUserOperation`;\n handler: KeyringController['signUserOperation'];\n};\n\nexport type KeyringControllerStateChangeEvent = {\n type: `${typeof name}:stateChange`;\n payload: [KeyringControllerState, Patch[]];\n};\n\nexport type KeyringControllerAccountRemovedEvent = {\n type: `${typeof name}:accountRemoved`;\n payload: [string];\n};\n\nexport type KeyringControllerLockEvent = {\n type: `${typeof name}:lock`;\n payload: [];\n};\n\nexport type KeyringControllerUnlockEvent = {\n type: `${typeof name}:unlock`;\n payload: [];\n};\n\nexport type KeyringControllerQRKeyringStateChangeEvent = {\n type: `${typeof name}:qrKeyringStateChange`;\n payload: [ReturnType];\n};\n\nexport type KeyringControllerActions =\n | KeyringControllerGetStateAction\n | KeyringControllerSignMessageAction\n | KeyringControllerSignPersonalMessageAction\n | KeyringControllerSignTypedMessageAction\n | KeyringControllerDecryptMessageAction\n | KeyringControllerGetEncryptionPublicKeyAction\n | KeyringControllerGetAccountsAction\n | KeyringControllerGetKeyringsByTypeAction\n | KeyringControllerGetKeyringForAccountAction\n | KeyringControllerPersistAllKeyringsAction\n | KeyringControllerPrepareUserOperationAction\n | KeyringControllerPatchUserOperationAction\n | KeyringControllerSignUserOperationAction;\n\nexport type KeyringControllerEvents =\n | KeyringControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent\n | KeyringControllerAccountRemovedEvent\n | KeyringControllerQRKeyringStateChangeEvent;\n\nexport type KeyringControllerMessenger = RestrictedControllerMessenger<\n typeof name,\n KeyringControllerActions,\n KeyringControllerEvents,\n never,\n never\n>;\n\nexport type KeyringControllerOptions = {\n keyringBuilders?: { (): EthKeyring; type: string }[];\n messenger: KeyringControllerMessenger;\n state?: { vault?: string };\n} & (\n | {\n cacheEncryptionKey: true;\n encryptor?: ExportableKeyEncryptor;\n }\n | {\n cacheEncryptionKey?: false;\n encryptor?: GenericEncryptor | ExportableKeyEncryptor;\n }\n);\n\n/**\n * @type KeyringObject\n *\n * Keyring object to return in fullUpdate\n * @property type - Keyring type\n * @property accounts - Associated accounts\n */\nexport type KeyringObject = {\n accounts: string[];\n type: string;\n};\n\n/**\n * A strategy for importing an account\n */\nexport enum AccountImportStrategy {\n privateKey = 'privateKey',\n json = 'json',\n}\n\n/**\n * The `signTypedMessage` version\n *\n * @see https://docs.metamask.io/guide/signing-data.html\n */\nexport enum SignTypedDataVersion {\n V1 = 'V1',\n V3 = 'V3',\n V4 = 'V4',\n}\n\n/**\n * A serialized keyring object.\n */\nexport type SerializedKeyring = {\n type: string;\n data: Json;\n};\n\n/**\n * A generic encryptor interface that supports encrypting and decrypting\n * serializable data with a password.\n */\nexport type GenericEncryptor = {\n /**\n * Encrypts the given object with the given password.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encrypted string.\n */\n encrypt: (password: string, object: Json) => Promise;\n /**\n * Decrypts the given encrypted string with the given password.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decrypt: (password: string, encryptedString: string) => Promise;\n /**\n * Optional vault migration helper. Checks if the provided vault is up to date\n * with the desired encryption algorithm.\n *\n * @param vault - The encrypted string to check.\n * @param targetDerivationParams - The desired target derivation params.\n * @returns The updated encrypted string.\n */\n isVaultUpdated?: (\n vault: string,\n targetDerivationParams?: encryptorUtils.KeyDerivationOptions,\n ) => boolean;\n};\n\n/**\n * An encryptor interface that supports encrypting and decrypting\n * serializable data with a password, and exporting and importing keys.\n */\nexport type ExportableKeyEncryptor = GenericEncryptor & {\n /**\n * Encrypts the given object with the given encryption key.\n *\n * @param key - The encryption key to encrypt with.\n * @param object - The object to encrypt.\n * @returns The encryption result.\n */\n encryptWithKey: (\n key: unknown,\n object: Json,\n ) => Promise;\n /**\n * Encrypts the given object with the given password, and returns the\n * encryption result and the exported key string.\n *\n * @param password - The password to encrypt with.\n * @param object - The object to encrypt.\n * @param salt - The optional salt to use for encryption.\n * @returns The encrypted string and the exported key string.\n */\n encryptWithDetail: (\n password: string,\n object: Json,\n salt?: string,\n ) => Promise;\n /**\n * Decrypts the given encrypted string with the given encryption key.\n *\n * @param key - The encryption key to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object.\n */\n decryptWithKey: (key: unknown, encryptedString: string) => Promise;\n /**\n * Decrypts the given encrypted string with the given password, and returns\n * the decrypted object and the salt and exported key string used for\n * encryption.\n *\n * @param password - The password to decrypt with.\n * @param encryptedString - The encrypted string to decrypt.\n * @returns The decrypted object and the salt and exported key string used for\n * encryption.\n */\n decryptWithDetail: (\n password: string,\n encryptedString: string,\n ) => Promise;\n /**\n * Generates an encryption key from exported key string.\n *\n * @param key - The exported key string.\n * @returns The encryption key.\n */\n importKey: (key: string) => Promise;\n};\n\nexport type KeyringSelector =\n | {\n type: string;\n index?: number;\n }\n | {\n address: Hex;\n };\n\n/**\n * A function executed within a mutually exclusive lock, with\n * a mutex releaser in its option bag.\n *\n * @param releaseLock - A function to release the lock.\n */\ntype MutuallyExclusiveCallback = ({\n releaseLock,\n}: {\n releaseLock: MutexInterface.Releaser;\n}) => Promise;\n\n/**\n * Get builder function for `Keyring`\n *\n * Returns a builder function for `Keyring` with a `type` property.\n *\n * @param KeyringConstructor - The Keyring class for the builder.\n * @returns A builder function for the given Keyring.\n */\nexport function keyringBuilderFactory(KeyringConstructor: KeyringClass) {\n const builder = () => new KeyringConstructor();\n\n builder.type = KeyringConstructor.type;\n\n return builder;\n}\n\nconst defaultKeyringBuilders = [\n keyringBuilderFactory(SimpleKeyring),\n keyringBuilderFactory(HDKeyring),\n];\n\nexport const getDefaultKeyringState = (): KeyringControllerState => {\n return {\n isUnlocked: false,\n keyrings: [],\n };\n};\n\n/**\n * Assert that the given keyring has an exportable\n * mnemonic.\n *\n * @param keyring - The keyring to check\n * @throws When the keyring does not have a mnemonic\n */\nfunction assertHasUint8ArrayMnemonic(\n keyring: EthKeyring,\n): asserts keyring is EthKeyring & { mnemonic: Uint8Array } {\n if (\n !(\n hasProperty(keyring, 'mnemonic') && keyring.mnemonic instanceof Uint8Array\n )\n ) {\n throw new Error(\"Can't get mnemonic bytes from keyring\");\n }\n}\n\n/**\n * Assert that the provided encryptor supports\n * encryption and encryption key export.\n *\n * @param encryptor - The encryptor to check.\n * @throws If the encryptor does not support key encryption.\n */\nfunction assertIsExportableKeyEncryptor(\n encryptor: GenericEncryptor | ExportableKeyEncryptor,\n): asserts encryptor is ExportableKeyEncryptor {\n if (\n !(\n 'importKey' in encryptor &&\n typeof encryptor.importKey === 'function' &&\n 'decryptWithKey' in encryptor &&\n typeof encryptor.decryptWithKey === 'function' &&\n 'encryptWithKey' in encryptor &&\n typeof encryptor.encryptWithKey === 'function'\n )\n ) {\n throw new Error(KeyringControllerError.UnsupportedEncryptionKeyExport);\n }\n}\n\n/**\n * Assert that the provided password is a valid non-empty string.\n *\n * @param password - The password to check.\n * @throws If the password is not a valid string.\n */\nfunction assertIsValidPassword(password: unknown): asserts password is string {\n if (typeof password !== 'string') {\n throw new Error(KeyringControllerError.WrongPasswordType);\n }\n\n if (!password || !password.length) {\n throw new Error(KeyringControllerError.InvalidEmptyPassword);\n }\n}\n\n/**\n * Checks if the provided value is a serialized keyrings array.\n *\n * @param array - The value to check.\n * @returns True if the value is a serialized keyrings array.\n */\nfunction isSerializedKeyringsArray(\n array: unknown,\n): array is SerializedKeyring[] {\n return (\n typeof array === 'object' &&\n Array.isArray(array) &&\n array.every((value) => value.type && isValidJson(value.data))\n );\n}\n\n/**\n * Display For Keyring\n *\n * Is used for adding the current keyrings to the state object.\n *\n * @param keyring - The keyring to display.\n * @returns A keyring display object, with type and accounts properties.\n */\nasync function displayForKeyring(\n keyring: EthKeyring,\n): Promise<{ type: string; accounts: string[] }> {\n const accounts = await keyring.getAccounts();\n\n return {\n type: keyring.type,\n // Cast to `string[]` here is safe here because `accounts` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n accounts: accounts.map(normalize) as string[],\n };\n}\n\n/**\n * Check if address is an ethereum address\n *\n * @param address - An address.\n * @returns Returns true if the address is an ethereum one, false otherwise.\n */\nfunction isEthAddress(address: string): boolean {\n // We first check if it's a matching `Hex` string, so that is narrows down\n // `address` as an `Hex` type, allowing us to use `isValidHexAddress`\n return (\n // NOTE: This function only checks for lowercased strings\n isStrictHexString(address.toLowerCase()) &&\n // This checks for lowercased addresses and checksum addresses too\n isValidHexAddress(address as Hex)\n );\n}\n\n/**\n * Normalize ethereum or non-EVM address.\n *\n * @param address - Ethereum or non-EVM address.\n * @returns The normalized address.\n */\nfunction normalize(address: string): string | undefined {\n // Since the `KeyringController` is only dealing with address, we have\n // no other way to get the associated account type with this address. So we\n // are down to check the actual address format for now\n // TODO: Find a better way to not have those runtime checks based on the\n // address value!\n return isEthAddress(address) ? ethNormalize(address) : address;\n}\n\n/**\n * Controller responsible for establishing and managing user identity.\n *\n * This class is a wrapper around the `eth-keyring-controller` package. The\n * `eth-keyring-controller` manages the \"vault\", which is an encrypted store of private keys, and\n * it manages the wallet \"lock\" state. This wrapper class has convenience methods for interacting\n * with the internal keyring controller and handling certain complex operations that involve the\n * keyrings.\n */\nexport class KeyringController extends BaseController<\n typeof name,\n KeyringControllerState,\n KeyringControllerMessenger\n> {\n readonly #controllerOperationMutex = new Mutex();\n\n readonly #vaultOperationMutex = new Mutex();\n\n #keyringBuilders: { (): EthKeyring; type: string }[];\n\n #keyrings: EthKeyring[];\n\n #unsupportedKeyrings: SerializedKeyring[];\n\n #password?: string;\n\n #encryptor: GenericEncryptor | ExportableKeyEncryptor;\n\n #cacheEncryptionKey: boolean;\n\n #qrKeyringStateListener?: (\n state: ReturnType,\n ) => void;\n\n /**\n * Creates a KeyringController instance.\n *\n * @param options - Initial options used to configure this controller\n * @param options.encryptor - An optional object for defining encryption schemes.\n * @param options.keyringBuilders - Set a new name for account.\n * @param options.cacheEncryptionKey - Whether to cache or not encryption key.\n * @param options.messenger - A restricted controller messenger.\n * @param options.state - Initial state to set on this controller.\n */\n constructor(options: KeyringControllerOptions) {\n const {\n encryptor = encryptorUtils,\n keyringBuilders,\n messenger,\n state,\n } = options;\n\n super({\n name,\n metadata: {\n vault: { persist: true, anonymous: false },\n isUnlocked: { persist: false, anonymous: true },\n keyrings: { persist: false, anonymous: false },\n encryptionKey: { persist: false, anonymous: false },\n encryptionSalt: { persist: false, anonymous: false },\n },\n messenger,\n state: {\n ...getDefaultKeyringState(),\n ...state,\n },\n });\n\n this.#keyringBuilders = keyringBuilders\n ? defaultKeyringBuilders.concat(keyringBuilders)\n : defaultKeyringBuilders;\n\n this.#encryptor = encryptor;\n this.#keyrings = [];\n this.#unsupportedKeyrings = [];\n\n // This option allows the controller to cache an exported key\n // for use in decrypting and encrypting data without password\n this.#cacheEncryptionKey = Boolean(options.cacheEncryptionKey);\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(encryptor);\n }\n\n this.#registerMessageHandlers();\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring.\n *\n * @param accountCount - Number of accounts before adding a new one, used to\n * make the method idempotent.\n * @returns Promise resolving to the added account address.\n */\n async addNewAccount(accountCount?: number): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const oldAccounts = await primaryKeyring.getAccounts();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n // we return the account already existing at index `accountCount`\n const existingAccount = oldAccounts[accountCount];\n\n if (!existingAccount) {\n throw new Error(`Can't find account at index ${accountCount}`);\n }\n\n return existingAccount;\n }\n\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the specified keyring.\n *\n * @param keyring - Keyring to add the account to.\n * @param accountCount - Number of accounts before adding a new one, used to make the method idempotent.\n * @returns Promise resolving to the added account address\n */\n async addNewAccountForKeyring(\n keyring: EthKeyring,\n accountCount?: number,\n ): Promise {\n // READ THIS CAREFULLY:\n // We still uses `Hex` here, since we are not using this method when creating\n // and account using a \"Snap Keyring\". This function assume the `keyring` is\n // ethereum compatible, but \"Snap Keyring\" might not be.\n return this.#persistOrRollback(async () => {\n const oldAccounts = await this.#getAccountsFromKeyrings();\n\n if (accountCount && oldAccounts.length !== accountCount) {\n if (accountCount > oldAccounts.length) {\n throw new Error('Account out of sequence');\n }\n\n const existingAccount = oldAccounts[accountCount];\n assertIsStrictHexString(existingAccount);\n\n return existingAccount;\n }\n\n await keyring.addAccounts(1);\n\n const addedAccountAddress = (await this.#getAccountsFromKeyrings()).find(\n (selectedAddress) => !oldAccounts.includes(selectedAddress),\n );\n assertIsStrictHexString(addedAccountAddress);\n\n return addedAccountAddress;\n });\n }\n\n /**\n * Adds a new account to the default (first) HD seed phrase keyring without updating identities in preferences.\n *\n * @returns Promise resolving to the added account address.\n */\n async addNewAccountWithoutUpdate(): Promise {\n return this.#persistOrRollback(async () => {\n const primaryKeyring = this.getKeyringsByType('HD Key Tree')[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found');\n }\n const [addedAccountAddress] = await primaryKeyring.addAccounts(1);\n await this.verifySeedPhrase();\n return addedAccountAddress;\n });\n }\n\n /**\n * Effectively the same as creating a new keychain then populating it\n * using the given seed phrase.\n *\n * @param password - Password to unlock keychain.\n * @param seed - A BIP39-compliant seed phrase as Uint8Array,\n * either as a string or an array of UTF-8 bytes that represent the string.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndRestore(\n password: string,\n seed: Uint8Array,\n ): Promise {\n return this.#persistOrRollback(async () => {\n assertIsValidPassword(password);\n\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n opts: {\n mnemonic: seed,\n numberOfAccounts: 1,\n },\n });\n });\n }\n\n /**\n * Create a new primary keychain and wipe any previous keychains.\n *\n * @param password - Password to unlock the new vault.\n * @returns Promise resolving when the operation ends successfully.\n */\n async createNewVaultAndKeychain(password: string) {\n return this.#persistOrRollback(async () => {\n const accounts = await this.#getAccountsFromKeyrings();\n if (!accounts.length) {\n await this.#createNewVaultWithKeyring(password, {\n type: KeyringTypes.hd,\n });\n }\n });\n }\n\n /**\n * Adds a new keyring of the given `type`.\n *\n * @param type - Keyring type name.\n * @param opts - Keyring options.\n * @throws If a builder for the given `type` does not exist.\n * @returns Promise resolving to the added keyring.\n */\n async addNewKeyring(\n type: KeyringTypes | string,\n opts?: unknown,\n ): Promise {\n if (type === KeyringTypes.qr) {\n return this.getOrAddQRKeyring();\n }\n\n return this.#persistOrRollback(async () => this.#newKeyring(type, opts));\n }\n\n /**\n * Method to verify a given password validity. Throws an\n * error if the password is invalid.\n *\n * @param password - Password of the keyring.\n */\n async verifyPassword(password: string) {\n if (!this.state.vault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n await this.#encryptor.decrypt(password, this.state.vault);\n }\n\n /**\n * Returns the status of the vault.\n *\n * @returns Boolean returning true if the vault is unlocked.\n */\n isUnlocked(): boolean {\n return this.state.isUnlocked;\n }\n\n /**\n * Gets the seed phrase of the HD keyring.\n *\n * @param password - Password of the keyring.\n * @returns Promise resolving to the seed phrase.\n */\n async exportSeedPhrase(password: string): Promise {\n await this.verifyPassword(password);\n assertHasUint8ArrayMnemonic(this.#keyrings[0]);\n return this.#keyrings[0].mnemonic;\n }\n\n /**\n * Gets the private key from the keyring controlling an address.\n *\n * @param password - Password of the keyring.\n * @param address - Address to export.\n * @returns Promise resolving to the private key for an address.\n */\n async exportAccount(password: string, address: string): Promise {\n await this.verifyPassword(password);\n\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.exportAccount) {\n throw new Error(KeyringControllerError.UnsupportedExportAccount);\n }\n\n return await keyring.exportAccount(normalize(address) as Hex);\n }\n\n /**\n * Returns the public addresses of all accounts from every keyring.\n *\n * @returns A promise resolving to an array of addresses.\n */\n async getAccounts(): Promise {\n return this.state.keyrings.reduce(\n (accounts, keyring) => accounts.concat(keyring.accounts),\n [],\n );\n }\n\n /**\n * Get encryption public key.\n *\n * @param account - An account address.\n * @param opts - Additional encryption options.\n * @throws If the `account` does not exist or does not support the `getEncryptionPublicKey` method\n * @returns Promise resolving to encyption public key of the `account` if one exists.\n */\n async getEncryptionPublicKey(\n account: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(account) as Hex;\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n if (!keyring.getEncryptionPublicKey) {\n throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey);\n }\n\n return await keyring.getEncryptionPublicKey(address, opts);\n }\n\n /**\n * Attempts to decrypt the provided message parameters.\n *\n * @param messageParams - The decryption message parameters.\n * @param messageParams.from - The address of the account you want to use to decrypt the message.\n * @param messageParams.data - The encrypted data that you want to decrypt.\n * @returns The raw decryption result.\n */\n async decryptMessage(messageParams: {\n from: string;\n data: Eip1024EncryptedData;\n }): Promise {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.decryptMessage) {\n throw new Error(KeyringControllerError.UnsupportedDecryptMessage);\n }\n\n return keyring.decryptMessage(address, messageParams.data);\n }\n\n /**\n * Returns the currently initialized keyring that manages\n * the specified `address` if one exists.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param account - An account address.\n * @returns Promise resolving to keyring of the `account` if one exists.\n */\n async getKeyringForAccount(account: string): Promise {\n const address = normalize(account);\n\n const candidates = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n return Promise.all([keyring, keyring.getAccounts()]);\n }),\n );\n\n const winners = candidates.filter((candidate) => {\n const accounts = candidate[1].map(normalize);\n return accounts.includes(address);\n });\n\n if (winners.length && winners[0]?.length) {\n return winners[0][0];\n }\n\n // Adding more info to the error\n let errorInfo = '';\n if (!candidates.length) {\n errorInfo = 'There are no keyrings';\n } else if (!winners.length) {\n errorInfo = 'There are keyrings, but none match the address';\n }\n throw new Error(\n `${KeyringControllerError.NoKeyring}. Error info: ${errorInfo}`,\n );\n }\n\n /**\n * Returns all keyrings of the given type.\n *\n * @deprecated Use of this method is discouraged as actions executed directly on\n * keyrings are not being reflected in the KeyringController state and not\n * persisted in the vault. Use `withKeyring` instead.\n * @param type - Keyring type name.\n * @returns An array of keyrings of the given type.\n */\n getKeyringsByType(type: KeyringTypes | string): unknown[] {\n return this.#keyrings.filter((keyring) => keyring.type === type);\n }\n\n /**\n * Persist all serialized keyrings in the vault.\n *\n * @deprecated This method is being phased out in favor of `withKeyring`.\n * @returns Promise resolving with `true` value when the\n * operation completes.\n */\n async persistAllKeyrings(): Promise {\n return this.#persistOrRollback(async () => true);\n }\n\n /**\n * Imports an account with the specified import strategy.\n *\n * @param strategy - Import strategy name.\n * @param args - Array of arguments to pass to the underlying stategy.\n * @throws Will throw when passed an unrecognized strategy.\n * @returns Promise resolving to the imported account address.\n */\n async importAccountWithStrategy(\n strategy: AccountImportStrategy,\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n args: any[],\n ): Promise {\n return this.#persistOrRollback(async () => {\n let privateKey;\n switch (strategy) {\n case 'privateKey':\n const [importedKey] = args;\n if (!importedKey) {\n throw new Error('Cannot import an empty key.');\n }\n const prefixed = add0x(importedKey);\n\n let bufferedPrivateKey;\n try {\n bufferedPrivateKey = toBuffer(prefixed);\n } catch {\n throw new Error('Cannot import invalid private key.');\n }\n\n if (\n !isValidPrivate(bufferedPrivateKey) ||\n // ensures that the key is 64 bytes long\n getBinarySize(prefixed) !== 64 + '0x'.length\n ) {\n throw new Error('Cannot import invalid private key.');\n }\n\n privateKey = remove0x(prefixed);\n break;\n case 'json':\n let wallet;\n const [input, password] = args;\n try {\n wallet = importers.fromEtherWallet(input, password);\n } catch (e) {\n wallet = wallet || (await Wallet.fromV3(input, password, true));\n }\n privateKey = bytesToHex(wallet.getPrivateKey());\n break;\n default:\n throw new Error(`Unexpected import strategy: '${strategy}'`);\n }\n const newKeyring = (await this.#newKeyring(KeyringTypes.simple, [\n privateKey,\n ])) as EthKeyring;\n const accounts = await newKeyring.getAccounts();\n return accounts[0];\n });\n }\n\n /**\n * Removes an account from keyring state.\n *\n * @param address - Address of the account to remove.\n * @fires KeyringController:accountRemoved\n * @returns Promise resolving when the account is removed.\n */\n async removeAccount(address: string): Promise {\n await this.#persistOrRollback(async () => {\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n // Not all the keyrings support this, so we have to check\n if (!keyring.removeAccount) {\n throw new Error(KeyringControllerError.UnsupportedRemoveAccount);\n }\n\n // The `removeAccount` method of snaps keyring is async. We have to update\n // the interface of the other keyrings to be async as well.\n // eslint-disable-next-line @typescript-eslint/await-thenable\n // FIXME: We do cast to `Hex` to makes the type checker happy here, and\n // because `Keyring.removeAccount` requires address to be `Hex`. Those\n // type would need to be updated for a full non-EVM support.\n await keyring.removeAccount(address as Hex);\n\n const accounts = await keyring.getAccounts();\n // Check if this was the last/only account\n if (accounts.length === 0) {\n await this.#removeEmptyKeyrings();\n }\n });\n\n this.messagingSystem.publish(`${name}:accountRemoved`, address);\n }\n\n /**\n * Deallocates all secrets and locks the wallet.\n *\n * @returns Promise resolving when the operation completes.\n */\n async setLocked(): Promise {\n return this.#withRollback(async () => {\n this.#unsubscribeFromQRKeyringsEvents();\n\n this.#password = undefined;\n await this.#clearKeyrings();\n\n this.update((state) => {\n state.isUnlocked = false;\n state.keyrings = [];\n });\n\n this.messagingSystem.publish(`${name}:lock`);\n });\n }\n\n /**\n * Signs message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signMessage(messageParams: PersonalMessageParams): Promise {\n if (!messageParams.data) {\n throw new Error(\"Can't sign an empty message\");\n }\n\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignMessage);\n }\n\n return await keyring.signMessage(address, messageParams.data);\n }\n\n /**\n * Signs personal message by calling down into a specific keyring.\n *\n * @param messageParams - PersonalMessageParams object to sign.\n * @returns Promise resolving to a signed message string.\n */\n async signPersonalMessage(messageParams: PersonalMessageParams) {\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signPersonalMessage) {\n throw new Error(KeyringControllerError.UnsupportedSignPersonalMessage);\n }\n\n const normalizedData = normalize(messageParams.data) as Hex;\n\n return await keyring.signPersonalMessage(address, normalizedData);\n }\n\n /**\n * Signs typed message by calling down into a specific keyring.\n *\n * @param messageParams - TypedMessageParams object to sign.\n * @param version - Compatibility version EIP712.\n * @throws Will throw when passed an unrecognized version.\n * @returns Promise resolving to a signed message string or an error if any.\n */\n async signTypedMessage(\n messageParams: TypedMessageParams,\n version: SignTypedDataVersion,\n ): Promise {\n try {\n if (\n ![\n SignTypedDataVersion.V1,\n SignTypedDataVersion.V3,\n SignTypedDataVersion.V4,\n ].includes(version)\n ) {\n throw new Error(`Unexpected signTypedMessage version: '${version}'`);\n }\n\n // Cast to `Hex` here is safe here because `messageParams.from` is not nullish.\n // `normalize` returns `Hex` unless given a nullish value.\n const address = ethNormalize(messageParams.from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTypedData) {\n throw new Error(KeyringControllerError.UnsupportedSignTypedMessage);\n }\n\n return await keyring.signTypedData(\n address,\n version !== SignTypedDataVersion.V1 &&\n typeof messageParams.data === 'string'\n ? JSON.parse(messageParams.data)\n : messageParams.data,\n { version },\n );\n } catch (error) {\n throw new Error(`Keyring Controller signTypedMessage: ${error}`);\n }\n }\n\n /**\n * Signs a transaction by calling down into a specific keyring.\n *\n * @param transaction - Transaction object to sign. Must be a `ethereumjs-tx` transaction instance.\n * @param from - Address to sign from, should be in keychain.\n * @param opts - An optional options object.\n * @returns Promise resolving to a signed transaction string.\n */\n async signTransaction(\n transaction: TypedTransaction,\n from: string,\n opts?: Record,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n if (!keyring.signTransaction) {\n throw new Error(KeyringControllerError.UnsupportedSignTransaction);\n }\n\n return await keyring.signTransaction(address, transaction, opts);\n }\n\n /**\n * Convert a base transaction to a base UserOperation.\n *\n * @param from - Address of the sender.\n * @param transactions - Base transactions to include in the UserOperation.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A pseudo-UserOperation that can be used to construct a real.\n */\n async prepareUserOperation(\n from: string,\n transactions: EthBaseTransaction[],\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.prepareUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPrepareUserOperation);\n }\n\n return await keyring.prepareUserOperation(\n address,\n transactions,\n executionContext,\n );\n }\n\n /**\n * Patches properties of a UserOperation. Currently, only the\n * `paymasterAndData` can be patched.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to patch.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns A patch to apply to the UserOperation.\n */\n async patchUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.patchUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedPatchUserOperation);\n }\n\n return await keyring.patchUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Signs an UserOperation.\n *\n * @param from - Address of the sender.\n * @param userOp - UserOperation to sign.\n * @param executionContext - The execution context to use for the UserOperation.\n * @returns The signature of the UserOperation.\n */\n async signUserOperation(\n from: string,\n userOp: EthUserOperation,\n executionContext: KeyringExecutionContext,\n ): Promise {\n const address = ethNormalize(from) as Hex;\n const keyring = (await this.getKeyringForAccount(\n address,\n )) as EthKeyring;\n\n if (!keyring.signUserOperation) {\n throw new Error(KeyringControllerError.UnsupportedSignUserOperation);\n }\n\n return await keyring.signUserOperation(address, userOp, executionContext);\n }\n\n /**\n * Changes the password used to encrypt the vault.\n *\n * @param password - The new password.\n * @returns Promise resolving when the operation completes.\n */\n changePassword(password: string): Promise {\n return this.#persistOrRollback(async () => {\n if (!this.state.isUnlocked) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n assertIsValidPassword(password);\n\n this.#password = password;\n // We need to clear encryption key and salt from state\n // to force the controller to re-encrypt the vault using\n // the new password.\n if (this.#cacheEncryptionKey) {\n this.update((state) => {\n delete state.encryptionKey;\n delete state.encryptionSalt;\n });\n }\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given encryption key and salt.\n *\n * @param encryptionKey - Key to unlock the keychain.\n * @param encryptionSalt - Salt to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitEncryptionKey(\n encryptionKey: string,\n encryptionSalt: string,\n ): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(\n undefined,\n encryptionKey,\n encryptionSalt,\n );\n this.#setUnlocked();\n });\n }\n\n /**\n * Attempts to decrypt the current vault and load its keyrings,\n * using the given password.\n *\n * @param password - Password to unlock the keychain.\n * @returns Promise resolving when the operation completes.\n */\n async submitPassword(password: string): Promise {\n return this.#withRollback(async () => {\n this.#keyrings = await this.#unlockKeyrings(password);\n this.#setUnlocked();\n });\n }\n\n /**\n * Verifies the that the seed phrase restores the current keychain's accounts.\n *\n * @returns Promise resolving to the seed phrase as Uint8Array.\n */\n async verifySeedPhrase(): Promise {\n const primaryKeyring = this.getKeyringsByType(KeyringTypes.hd)[0] as\n | EthKeyring\n | undefined;\n if (!primaryKeyring) {\n throw new Error('No HD keyring found.');\n }\n\n assertHasUint8ArrayMnemonic(primaryKeyring);\n\n const seedWords = primaryKeyring.mnemonic;\n const accounts = await primaryKeyring.getAccounts();\n /* istanbul ignore if */\n if (accounts.length === 0) {\n throw new Error('Cannot verify an empty keyring.');\n }\n\n // The HD Keyring Builder is a default keyring builder\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const hdKeyringBuilder = this.#getKeyringBuilderForType(KeyringTypes.hd)!;\n\n const hdKeyring = hdKeyringBuilder();\n // @ts-expect-error @metamask/eth-hd-keyring correctly handles\n // Uint8Array seed phrases in the `deserialize` method.\n await hdKeyring.deserialize({\n mnemonic: seedWords,\n numberOfAccounts: accounts.length,\n });\n const testAccounts = await hdKeyring.getAccounts();\n /* istanbul ignore if */\n if (testAccounts.length !== accounts.length) {\n throw new Error('Seed phrase imported incorrect number of accounts.');\n }\n\n testAccounts.forEach((account: string, i: number) => {\n /* istanbul ignore if */\n if (account.toLowerCase() !== accounts[i].toLowerCase()) {\n throw new Error('Seed phrase imported different accounts.');\n }\n });\n\n return seedWords;\n }\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @param options - Additional options.\n * @param options.createIfMissing - Whether to create a new keyring if the selected one is missing.\n * @param options.createWithData - Optional data to use when creating a new keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n * @deprecated This method overload is deprecated. Use `withKeyring` without options instead.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n // eslint-disable-next-line @typescript-eslint/unified-signatures\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown },\n ): Promise;\n\n /**\n * Select a keyring and execute the given operation with\n * the selected keyring, as a mutually exclusive atomic\n * operation.\n *\n * The method automatically persists changes at the end of the\n * function execution, or rolls back the changes if an error\n * is thrown.\n *\n * @param selector - Keyring selector object.\n * @param operation - Function to execute with the selected keyring.\n * @returns Promise resolving to the result of the function execution.\n * @template SelectedKeyring - The type of the selected keyring.\n * @template CallbackResult - The type of the value resolved by the callback function.\n */\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n ): Promise;\n\n async withKeyring<\n SelectedKeyring extends EthKeyring = EthKeyring,\n CallbackResult = void,\n >(\n selector: KeyringSelector,\n operation: (keyring: SelectedKeyring) => Promise,\n options:\n | { createIfMissing?: false }\n | { createIfMissing: true; createWithData?: unknown } = {\n createIfMissing: false,\n },\n ): Promise {\n return this.#persistOrRollback(async () => {\n let keyring: SelectedKeyring | undefined;\n\n if ('address' in selector) {\n keyring = (await this.getKeyringForAccount(selector.address)) as\n | SelectedKeyring\n | undefined;\n } else {\n keyring = this.getKeyringsByType(selector.type)[selector.index || 0] as\n | SelectedKeyring\n | undefined;\n\n if (!keyring && options.createIfMissing) {\n keyring = (await this.#newKeyring(\n selector.type,\n options.createWithData,\n )) as SelectedKeyring;\n }\n }\n\n if (!keyring) {\n throw new Error(KeyringControllerError.KeyringNotFound);\n }\n\n const result = await operation(keyring);\n\n if (Object.is(result, keyring)) {\n // Access to a keyring instance outside of controller safeguards\n // should be discouraged, as it can lead to unexpected behavior.\n // This error is thrown to prevent consumers using `withKeyring`\n // as a way to get a reference to a keyring instance.\n throw new Error(KeyringControllerError.UnsafeDirectKeyringAccess);\n }\n\n return result;\n });\n }\n\n // QR Hardware related methods\n\n /**\n * Get QR Hardware keyring.\n *\n * @returns The QR Keyring if defined, otherwise undefined\n */\n getQRKeyring(): QRKeyring | undefined {\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return this.getKeyringsByType(KeyringTypes.qr)[0] as unknown as QRKeyring;\n }\n\n /**\n * Get QR hardware keyring. If it doesn't exist, add it.\n *\n * @returns The added keyring\n */\n async getOrAddQRKeyring(): Promise {\n return (\n this.getQRKeyring() ||\n (await this.#persistOrRollback(async () => this.#addQRKeyring()))\n );\n }\n\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async restoreQRKeyring(serialized: any): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n keyring.deserialize(serialized);\n });\n }\n\n async resetQRKeyringState(): Promise {\n (await this.getOrAddQRKeyring()).resetStore();\n }\n\n async getQRKeyringState(): Promise {\n return (await this.getOrAddQRKeyring()).getMemStore();\n }\n\n async submitQRCryptoHDKey(cryptoHDKey: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoHDKey(cryptoHDKey);\n }\n\n async submitQRCryptoAccount(cryptoAccount: string): Promise {\n (await this.getOrAddQRKeyring()).submitCryptoAccount(cryptoAccount);\n }\n\n async submitQRSignature(\n requestId: string,\n ethSignature: string,\n ): Promise {\n (await this.getOrAddQRKeyring()).submitSignature(requestId, ethSignature);\n }\n\n async cancelQRSignRequest(): Promise {\n (await this.getOrAddQRKeyring()).cancelSignRequest();\n }\n\n /**\n * Cancels qr keyring sync.\n */\n async cancelQRSynchronization(): Promise {\n // eslint-disable-next-line n/no-sync\n (await this.getOrAddQRKeyring()).cancelSync();\n }\n\n async connectQRHardware(\n page: number,\n ): Promise<{ balance: string; address: string; index: number }[]> {\n return this.#persistOrRollback(async () => {\n try {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n let accounts;\n switch (page) {\n case -1:\n accounts = await keyring.getPreviousPage();\n break;\n case 1:\n accounts = await keyring.getNextPage();\n break;\n default:\n accounts = await keyring.getFirstPage();\n }\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return accounts.map((account: any) => {\n return {\n ...account,\n balance: '0x0',\n };\n });\n } catch (e) {\n // TODO: Add test case for when keyring throws\n /* istanbul ignore next */\n throw new Error(`Unspecified error when connect QR Hardware, ${e}`);\n }\n });\n }\n\n async unlockQRHardwareWalletAccount(index: number): Promise {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring() || (await this.#addQRKeyring());\n\n keyring.setAccountToUnlock(index);\n await keyring.addAccounts(1);\n });\n }\n\n async getAccountKeyringType(account: string): Promise {\n const keyring = (await this.getKeyringForAccount(\n account,\n )) as EthKeyring;\n return keyring.type;\n }\n\n async forgetQRDevice(): Promise<{\n removedAccounts: string[];\n remainingAccounts: string[];\n }> {\n return this.#persistOrRollback(async () => {\n const keyring = this.getQRKeyring();\n\n if (!keyring) {\n return { removedAccounts: [], remainingAccounts: [] };\n }\n\n const allAccounts = (await this.#getAccountsFromKeyrings()) as string[];\n keyring.forgetDevice();\n const remainingAccounts =\n (await this.#getAccountsFromKeyrings()) as string[];\n const removedAccounts = allAccounts.filter(\n (address: string) => !remainingAccounts.includes(address),\n );\n return { removedAccounts, remainingAccounts };\n });\n }\n\n /**\n * Constructor helper for registering this controller's messaging system\n * actions.\n */\n #registerMessageHandlers() {\n this.messagingSystem.registerActionHandler(\n `${name}:signMessage`,\n this.signMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signPersonalMessage`,\n this.signPersonalMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signTypedMessage`,\n this.signTypedMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:decryptMessage`,\n this.decryptMessage.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getEncryptionPublicKey`,\n this.getEncryptionPublicKey.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getAccounts`,\n this.getAccounts.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringsByType`,\n this.getKeyringsByType.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:getKeyringForAccount`,\n this.getKeyringForAccount.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:persistAllKeyrings`,\n this.persistAllKeyrings.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:prepareUserOperation`,\n this.prepareUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:patchUserOperation`,\n this.patchUserOperation.bind(this),\n );\n\n this.messagingSystem.registerActionHandler(\n `${name}:signUserOperation`,\n this.signUserOperation.bind(this),\n );\n }\n\n /**\n * Get the keyring builder for the given `type`.\n *\n * @param type - The type of keyring to get the builder for.\n * @returns The keyring builder, or undefined if none exists.\n */\n #getKeyringBuilderForType(\n type: string,\n ): { (): EthKeyring; type: string } | undefined {\n return this.#keyringBuilders.find(\n (keyringBuilder) => keyringBuilder.type === type,\n );\n }\n\n /**\n * Add qr hardware keyring.\n *\n * @returns The added keyring\n * @throws If a QRKeyring builder is not provided\n * when initializing the controller\n */\n async #addQRKeyring(): Promise {\n this.#assertControllerMutexIsLocked();\n\n // QRKeyring is not yet compatible with Keyring type from @metamask/utils\n return (await this.#newKeyring(KeyringTypes.qr)) as unknown as QRKeyring;\n }\n\n /**\n * Subscribe to a QRKeyring state change events and\n * forward them through the messaging system.\n *\n * @param qrKeyring - The QRKeyring instance to subscribe to\n */\n #subscribeToQRKeyringEvents(qrKeyring: QRKeyring) {\n this.#qrKeyringStateListener = (state) => {\n this.messagingSystem.publish(`${name}:qrKeyringStateChange`, state);\n };\n\n qrKeyring.getMemStore().subscribe(this.#qrKeyringStateListener);\n }\n\n #unsubscribeFromQRKeyringsEvents() {\n const qrKeyrings = this.getKeyringsByType(\n KeyringTypes.qr,\n ) as unknown as QRKeyring[];\n\n qrKeyrings.forEach((qrKeyring) => {\n if (this.#qrKeyringStateListener) {\n qrKeyring.getMemStore().unsubscribe(this.#qrKeyringStateListener);\n }\n });\n }\n\n /**\n * Create new vault with an initial keyring\n *\n * Destroys any old encrypted storage,\n * creates a new encrypted store with the given password,\n * creates a new wallet with 1 account.\n *\n * @fires KeyringController:unlock\n * @param password - The password to encrypt the vault with.\n * @param keyring - A object containing the params to instantiate a new keyring.\n * @param keyring.type - The keyring type.\n * @param keyring.opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves to the state.\n */\n async #createNewVaultWithKeyring(\n password: string,\n keyring: {\n type: string;\n opts?: unknown;\n },\n ): Promise {\n this.#assertControllerMutexIsLocked();\n\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n this.#password = password;\n\n await this.#clearKeyrings();\n await this.#createKeyringWithFirstAccount(keyring.type, keyring.opts);\n this.#setUnlocked();\n }\n\n /**\n * Get the updated array of each keyring's type and\n * accounts list.\n *\n * @returns A promise resolving to the updated keyrings array.\n */\n async #getUpdatedKeyrings(): Promise {\n return Promise.all(this.#keyrings.map(displayForKeyring));\n }\n\n /**\n * Serialize the current array of keyring instances,\n * including unsupported keyrings by default.\n *\n * @param options - Method options.\n * @param options.includeUnsupported - Whether to include unsupported keyrings.\n * @returns The serialized keyrings.\n */\n async #getSerializedKeyrings(\n { includeUnsupported }: { includeUnsupported: boolean } = {\n includeUnsupported: true,\n },\n ): Promise {\n const serializedKeyrings = await Promise.all(\n this.#keyrings.map(async (keyring) => {\n const [type, data] = await Promise.all([\n keyring.type,\n keyring.serialize(),\n ]);\n return { type, data };\n }),\n );\n\n if (includeUnsupported) {\n serializedKeyrings.push(...this.#unsupportedKeyrings);\n }\n\n return serializedKeyrings;\n }\n\n /**\n * Restore a serialized keyrings array.\n *\n * @param serializedKeyrings - The serialized keyrings array.\n */\n async #restoreSerializedKeyrings(\n serializedKeyrings: SerializedKeyring[],\n ): Promise {\n await this.#clearKeyrings();\n\n for (const serializedKeyring of serializedKeyrings) {\n await this.#restoreKeyring(serializedKeyring);\n }\n }\n\n /**\n * Unlock Keyrings, decrypting the vault and deserializing all\n * keyrings contained in it, using a password or an encryption key with salt.\n *\n * @param password - The keyring controller password.\n * @param encryptionKey - An exported key string to unlock keyrings with.\n * @param encryptionSalt - The salt used to encrypt the vault.\n * @returns A promise resolving to the deserialized keyrings array.\n */\n async #unlockKeyrings(\n password: string | undefined,\n encryptionKey?: string,\n encryptionSalt?: string,\n ): Promise[]> {\n return this.#withVaultLock(async ({ releaseLock }) => {\n const encryptedVault = this.state.vault;\n if (!encryptedVault) {\n throw new Error(KeyringControllerError.VaultError);\n }\n\n let vault;\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (password) {\n const result = await this.#encryptor.decryptWithDetail(\n password,\n encryptedVault,\n );\n vault = result.vault;\n this.#password = password;\n\n updatedState.encryptionKey = result.exportedKeyString;\n updatedState.encryptionSalt = result.salt;\n } else {\n const parsedEncryptedVault = JSON.parse(encryptedVault);\n\n if (encryptionSalt !== parsedEncryptedVault.salt) {\n throw new Error(KeyringControllerError.ExpiredCredentials);\n }\n\n if (typeof encryptionKey !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n const key = await this.#encryptor.importKey(encryptionKey);\n vault = await this.#encryptor.decryptWithKey(\n key,\n parsedEncryptedVault,\n );\n\n // This call is required on the first call because encryptionKey\n // is not yet inside the memStore\n updatedState.encryptionKey = encryptionKey;\n // we can safely assume that encryptionSalt is defined here\n // because we compare it with the salt from the vault\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n updatedState.encryptionSalt = encryptionSalt!;\n }\n } else {\n if (typeof password !== 'string') {\n throw new TypeError(KeyringControllerError.WrongPasswordType);\n }\n\n vault = await this.#encryptor.decrypt(password, encryptedVault);\n this.#password = password;\n }\n\n if (!isSerializedKeyringsArray(vault)) {\n throw new Error(KeyringControllerError.VaultDataError);\n }\n\n await this.#restoreSerializedKeyrings(vault);\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n\n this.update((state) => {\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey || updatedState.encryptionSalt) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = updatedState.encryptionSalt;\n }\n });\n\n if (\n this.#password &&\n (!this.#cacheEncryptionKey || !encryptionKey) &&\n this.#encryptor.isVaultUpdated &&\n !this.#encryptor.isVaultUpdated(encryptedVault)\n ) {\n // The lock needs to be released before persisting the keyrings\n // to avoid deadlock\n releaseLock();\n // Re-encrypt the vault with safer method if one is available\n await this.#updateVault();\n }\n\n return this.#keyrings;\n });\n }\n\n /**\n * Update the vault with the current keyrings.\n *\n * @returns A promise resolving to `true` if the operation is successful.\n */\n #updateVault(): Promise {\n return this.#withVaultLock(async () => {\n const { encryptionKey, encryptionSalt } = this.state;\n\n if (!this.#password && !encryptionKey) {\n throw new Error(KeyringControllerError.MissingCredentials);\n }\n\n const serializedKeyrings = await this.#getSerializedKeyrings();\n\n if (\n !serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)\n ) {\n throw new Error(KeyringControllerError.NoHdKeyring);\n }\n\n const updatedState: Partial = {};\n\n if (this.#cacheEncryptionKey) {\n assertIsExportableKeyEncryptor(this.#encryptor);\n\n if (encryptionKey) {\n const key = await this.#encryptor.importKey(encryptionKey);\n const vaultJSON = await this.#encryptor.encryptWithKey(\n key,\n serializedKeyrings,\n );\n vaultJSON.salt = encryptionSalt;\n updatedState.vault = JSON.stringify(vaultJSON);\n } else if (this.#password) {\n const { vault: newVault, exportedKeyString } =\n await this.#encryptor.encryptWithDetail(\n this.#password,\n serializedKeyrings,\n );\n\n updatedState.vault = newVault;\n updatedState.encryptionKey = exportedKeyString;\n }\n } else {\n assertIsValidPassword(this.#password);\n updatedState.vault = await this.#encryptor.encrypt(\n this.#password,\n serializedKeyrings,\n );\n }\n\n if (!updatedState.vault) {\n throw new Error(KeyringControllerError.MissingVaultData);\n }\n\n const updatedKeyrings = await this.#getUpdatedKeyrings();\n this.update((state) => {\n state.vault = updatedState.vault;\n state.keyrings = updatedKeyrings;\n if (updatedState.encryptionKey) {\n state.encryptionKey = updatedState.encryptionKey;\n state.encryptionSalt = JSON.parse(updatedState.vault as string).salt;\n }\n });\n\n return true;\n });\n }\n\n /**\n * Retrieves all the accounts from keyrings instances\n * that are currently in memory.\n *\n * @returns A promise resolving to an array of accounts.\n */\n async #getAccountsFromKeyrings(): Promise {\n const keyrings = this.#keyrings;\n\n const keyringArrays = await Promise.all(\n keyrings.map(async (keyring) => keyring.getAccounts()),\n );\n const addresses = keyringArrays.reduce((res, arr) => {\n return res.concat(arr);\n }, []);\n\n // Cast to `string[]` here is safe here because `addresses` has no nullish\n // values, and `normalize` returns `string` unless given a nullish value\n return addresses.map(normalize) as string[];\n }\n\n /**\n * Create a new keyring, ensuring that the first account is\n * also created.\n *\n * @param type - Keyring type to instantiate.\n * @param opts - Optional parameters required to instantiate the keyring.\n * @returns A promise that resolves if the operation is successful.\n */\n async #createKeyringWithFirstAccount(type: string, opts?: unknown) {\n this.#assertControllerMutexIsLocked();\n\n const keyring = (await this.#newKeyring(type, opts)) as EthKeyring;\n\n const [firstAccount] = await keyring.getAccounts();\n if (!firstAccount) {\n throw new Error(KeyringControllerError.NoFirstAccount);\n }\n }\n\n /**\n * Instantiate, initialize and return a new keyring of the given `type`,\n * using the given `opts`. The keyring is built using the keyring builder\n * registered for the given `type`.\n *\n *\n * @param type - The type of keyring to add.\n * @param data - The data to restore a previously serialized keyring.\n * @returns The new keyring.\n * @throws If the keyring includes duplicated accounts.\n */\n async #newKeyring(type: string, data?: unknown): Promise> {\n this.#assertControllerMutexIsLocked();\n\n const keyringBuilder = this.#getKeyringBuilderForType(type);\n\n if (!keyringBuilder) {\n throw new Error(\n `${KeyringControllerError.NoKeyringBuilder}. Keyring type: ${type}`,\n );\n }\n\n const keyring = keyringBuilder();\n\n // @ts-expect-error Enforce data type after updating clients\n await keyring.deserialize(data);\n\n if (keyring.init) {\n await keyring.init();\n }\n\n if (type === KeyringTypes.hd && (!isObject(data) || !data.mnemonic)) {\n if (!keyring.generateRandomMnemonic) {\n throw new Error(\n KeyringControllerError.UnsupportedGenerateRandomMnemonic,\n );\n }\n\n keyring.generateRandomMnemonic();\n await keyring.addAccounts(1);\n }\n\n await this.#checkForDuplicate(type, await keyring.getAccounts());\n\n if (type === KeyringTypes.qr) {\n // In case of a QR keyring type, we need to subscribe\n // to its events after creating it\n this.#subscribeToQRKeyringEvents(keyring as unknown as QRKeyring);\n }\n\n this.#keyrings.push(keyring);\n\n return keyring;\n }\n\n /**\n * Remove all managed keyrings, destroying all their\n * instances in memory.\n */\n async #clearKeyrings() {\n this.#assertControllerMutexIsLocked();\n for (const keyring of this.#keyrings) {\n await this.#destroyKeyring(keyring);\n }\n this.#keyrings = [];\n }\n\n /**\n * Restore a Keyring from a provided serialized payload.\n * On success, returns the resulting keyring instance.\n *\n * @param serialized - The serialized keyring.\n * @returns The deserialized keyring or undefined if the keyring type is unsupported.\n */\n async #restoreKeyring(\n serialized: SerializedKeyring,\n ): Promise | undefined> {\n this.#assertControllerMutexIsLocked();\n\n try {\n const { type, data } = serialized;\n return await this.#newKeyring(type, data);\n } catch (_) {\n this.#unsupportedKeyrings.push(serialized);\n return undefined;\n }\n }\n\n /**\n * Destroy Keyring\n *\n * Some keyrings support a method called `destroy`, that destroys the\n * keyring along with removing all its event listeners and, in some cases,\n * clears the keyring bridge iframe from the DOM.\n *\n * @param keyring - The keyring to destroy.\n */\n async #destroyKeyring(keyring: EthKeyring) {\n await keyring.destroy?.();\n }\n\n /**\n * Remove empty keyrings.\n *\n * Loops through the keyrings and removes the ones with empty accounts\n * (usually after removing the last / only account) from a keyring.\n */\n async #removeEmptyKeyrings(): Promise {\n this.#assertControllerMutexIsLocked();\n const validKeyrings: EthKeyring[] = [];\n\n // Since getAccounts returns a Promise\n // We need to wait to hear back form each keyring\n // in order to decide which ones are now valid (accounts.length > 0)\n\n await Promise.all(\n this.#keyrings.map(async (keyring: EthKeyring) => {\n const accounts = await keyring.getAccounts();\n if (accounts.length > 0) {\n validKeyrings.push(keyring);\n } else {\n await this.#destroyKeyring(keyring);\n }\n }),\n );\n this.#keyrings = validKeyrings;\n }\n\n /**\n * Checks for duplicate keypairs, using the the first account in the given\n * array. Rejects if a duplicate is found.\n *\n * Only supports 'Simple Key Pair'.\n *\n * @param type - The key pair type to check for.\n * @param newAccountArray - Array of new accounts.\n * @returns The account, if no duplicate is found.\n */\n async #checkForDuplicate(\n type: string,\n newAccountArray: string[],\n ): Promise {\n const accounts = await this.#getAccountsFromKeyrings();\n\n switch (type) {\n case KeyringTypes.simple: {\n const isIncluded = Boolean(\n accounts.find(\n (key) =>\n newAccountArray[0] &&\n (key === newAccountArray[0] ||\n key === remove0x(newAccountArray[0])),\n ),\n );\n\n if (isIncluded) {\n throw new Error(KeyringControllerError.DuplicatedAccount);\n }\n return newAccountArray;\n }\n\n default: {\n return newAccountArray;\n }\n }\n }\n\n /**\n * Set the `isUnlocked` to true and notify listeners\n * through the messenger.\n *\n * @fires KeyringController:unlock\n */\n #setUnlocked(): void {\n this.#assertControllerMutexIsLocked();\n\n this.update((state) => {\n state.isUnlocked = true;\n });\n this.messagingSystem.publish(`${name}:unlock`);\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and save the keyrings to state after it, or rollback to their\n * previous state in case of error.\n *\n * @param fn - The function to execute.\n * @returns The result of the function.\n */\n async #persistOrRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withRollback(async ({ releaseLock }) => {\n const callbackResult = await fn({ releaseLock });\n // State is committed only if the operation is successful\n await this.#updateVault();\n\n return callbackResult;\n });\n }\n\n /**\n * Execute the given function after acquiring the controller lock\n * and rollback keyrings and password states in case of error.\n *\n * @param fn - The function to execute atomically.\n * @returns The result of the function.\n */\n async #withRollback(fn: MutuallyExclusiveCallback): Promise {\n return this.#withControllerLock(async ({ releaseLock }) => {\n const currentSerializedKeyrings = await this.#getSerializedKeyrings();\n const currentPassword = this.#password;\n\n try {\n return await fn({ releaseLock });\n } catch (e) {\n // Keyrings and password are restored to their previous state\n await this.#restoreSerializedKeyrings(currentSerializedKeyrings);\n this.#password = currentPassword;\n\n throw e;\n }\n });\n }\n\n /**\n * Assert that the controller mutex is locked.\n *\n * @throws If the controller mutex is not locked.\n */\n #assertControllerMutexIsLocked() {\n if (!this.#controllerOperationMutex.isLocked()) {\n throw new Error(KeyringControllerError.ControllerLockRequired);\n }\n }\n\n /**\n * Lock the controller mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This wrapper ensures that each mutable operation that interacts with the\n * controller and that changes its state is executed in a mutually exclusive way,\n * preventing unsafe concurrent access that could lead to unpredictable behavior.\n *\n * @param fn - The function to execute while the controller mutex is locked.\n * @returns The result of the function.\n */\n async #withControllerLock(fn: MutuallyExclusiveCallback): Promise {\n return withLock(this.#controllerOperationMutex, fn);\n }\n\n /**\n * Lock the vault mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * This ensures that each operation that interacts with the vault\n * is executed in a mutually exclusive way.\n *\n * @param fn - The function to execute while the vault mutex is locked.\n * @returns The result of the function.\n */\n async #withVaultLock(fn: MutuallyExclusiveCallback): Promise {\n this.#assertControllerMutexIsLocked();\n\n return withLock(this.#vaultOperationMutex, fn);\n }\n}\n\n/**\n * Lock the given mutex before executing the given function,\n * and release it after the function is resolved or after an\n * error is thrown.\n *\n * @param mutex - The mutex to lock.\n * @param fn - The function to execute while the mutex is locked.\n * @returns The result of the function.\n */\nasync function withLock(\n mutex: Mutex,\n fn: MutuallyExclusiveCallback,\n): Promise {\n const releaseLock = await mutex.acquire();\n\n try {\n return await fn({ releaseLock });\n } finally {\n releaseLock();\n }\n}\n\nexport default KeyringController;\n"],"mappings":"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","names":["KeyringTypes","AccountImportStrategy","SignTypedDataVersion"]} -\ No newline at end of file -diff --git a/dist/index.js b/dist/index.js -index bf8713e2da2130714d42b1fa102fca536a58bd13..ee3560b42837d47440a3957c6403bb98e9e6330c 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -6,7 +6,7 @@ - - - --var _chunkBRS27QHFjs = require('./chunk-BRS27QHF.js'); -+var _chunkL4UUWIZAjs = require('./chunk-L4UUWIZA.js'); - require('./chunk-NOCGQCUM.js'); - - -@@ -16,5 +16,5 @@ require('./chunk-NOCGQCUM.js'); - - - --exports.AccountImportStrategy = _chunkBRS27QHFjs.AccountImportStrategy; exports.KeyringController = _chunkBRS27QHFjs.KeyringController; exports.KeyringTypes = _chunkBRS27QHFjs.KeyringTypes; exports.SignTypedDataVersion = _chunkBRS27QHFjs.SignTypedDataVersion; exports.getDefaultKeyringState = _chunkBRS27QHFjs.getDefaultKeyringState; exports.isCustodyKeyring = _chunkBRS27QHFjs.isCustodyKeyring; exports.keyringBuilderFactory = _chunkBRS27QHFjs.keyringBuilderFactory; -+exports.AccountImportStrategy = _chunkL4UUWIZAjs.AccountImportStrategy; exports.KeyringController = _chunkL4UUWIZAjs.KeyringController; exports.KeyringTypes = _chunkL4UUWIZAjs.KeyringTypes; exports.SignTypedDataVersion = _chunkL4UUWIZAjs.SignTypedDataVersion; exports.getDefaultKeyringState = _chunkL4UUWIZAjs.getDefaultKeyringState; exports.isCustodyKeyring = _chunkL4UUWIZAjs.isCustodyKeyring; exports.keyringBuilderFactory = _chunkL4UUWIZAjs.keyringBuilderFactory; - //# sourceMappingURL=index.js.map -\ No newline at end of file -diff --git a/dist/index.mjs b/dist/index.mjs -index 8bbc57c7e56445cfea5b1ef6134d7b53ab941c9c..3b1a052f121db8ee7f10650f81332a2bbc0ae83d 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -6,7 +6,7 @@ import { - getDefaultKeyringState, - isCustodyKeyring, - keyringBuilderFactory --} from "./chunk-STFS4REY.mjs"; -+} from "./chunk-7A7D7THR.mjs"; - import "./chunk-F64I344Z.mjs"; - export { - AccountImportStrategy, -diff --git a/dist/tsconfig.build.tsbuildinfo b/dist/tsconfig.build.tsbuildinfo -index 6cdcb0f629fb0ec66d4d98132e42d9d2dda0fbb2..9642fe3738f9b52ac9acc1a22f8dd81190372ddb 100644 ---- a/dist/tsconfig.build.tsbuildinfo -+++ b/dist/tsconfig.build.tsbuildinfo -@@ -1 +1 @@ --{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createProvider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/dom-events.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asyncEventEmitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacyTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/baseTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionFactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/patchCBOR.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/DataItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/cbor-sync.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/index.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/ur.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/urEncoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/fountainEncoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/fountainDecoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/urDecoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/RegistryType.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/RegistryItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoCoinInfo.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/PathComponent.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoKeypath.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/types.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoHDKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoECKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/Bytes.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/MultiKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/ScriptExpression.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoOutput.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoPSBT.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoAccount.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/Decoder/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/CryptoMultiAccounts.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/errors/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/DerivationSchema.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/KeyDerivation.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/QRHardwareCall.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/utils.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/EthSignRequest.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/EthSignature.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/ETHNFTItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/utlis.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/index.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/InteractionProvider.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/BaseKeyring.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@metamask/safe-event-emitter/dist/cjs/index.d.ts","../../../node_modules/@metamask/obs-store/dist/ObservableStore.d.ts","../../../node_modules/@metamask/obs-store/dist/asStream.d.ts","../../../node_modules/@metamask/obs-store/dist/ComposedStore.d.ts","../../../node_modules/@metamask/obs-store/dist/MergedStore.d.ts","../../../node_modules/@metamask/obs-store/dist/transform.d.ts","../../../node_modules/@metamask/obs-store/dist/index.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/MetaMaskInteractionProvider.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/MetaMaskKeyring.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/index.d.ts","../../base-controller/dist/types/BaseControllerV1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerClass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/RestrictedControllerMessenger.d.ts","../../base-controller/dist/types/ControllerMessenger.d.ts","../../base-controller/dist/types/BaseControllerV2.d.ts","../../base-controller/dist/types/index.d.ts","../../../node_modules/@metamask/browser-passworder/dist/index.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/personal-sign.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/sign-typed-data.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/encryption.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/utils.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/index.d.ts","../../../node_modules/@metamask/eth-simple-keyring/dist/simple-keyring.d.ts","../../../node_modules/@metamask/eth-simple-keyring/dist/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/base-types.d.ts","../../../node_modules/@metamask/keyring-api/dist/btc/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/btc/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/superstruct.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/erc4337/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/erc4337/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/api.d.ts","../../../node_modules/@metamask/keyring-api/dist/contexts.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/eth/EthKeyring.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/eth/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/events.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/rpc.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/utils.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/api.d.ts","../../../node_modules/@metamask/keyring-api/dist/events.d.ts","../../../node_modules/@metamask/keyring-api/dist/JsonRpcRequest.d.ts","../../../node_modules/@metamask/keyring-api/dist/KeyringClient.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/utils.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/classes.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/errors.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/error-constants.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/index.d.ts","../../json-rpc-engine/src/JsonRpcEngine.ts","../../json-rpc-engine/src/createAsyncMiddleware.ts","../../json-rpc-engine/src/createScaffoldMiddleware.ts","../../json-rpc-engine/src/getUniqueId.ts","../../json-rpc-engine/src/idRemapMiddleware.ts","../../json-rpc-engine/src/mergeMiddleware.ts","../../json-rpc-engine/src/index.ts","../../../node_modules/@metamask/providers/dist/types/utils.d.ts","../../../node_modules/@metamask/providers/dist/types/BaseProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/EIP6963.d.ts","../../../node_modules/@metamask/providers/dist/types/StreamProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/extension-provider/createExternalExtensionProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/MetaMaskInpageProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/initializeInpageProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/shimWeb3.d.ts","../../../node_modules/@metamask/providers/dist/types/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/KeyringSnapRpcClient.d.ts","../../../node_modules/@metamask/keyring-api/dist/rpc-handler.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/errors.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/error-wrappers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/errors.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/helpers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/structs.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/create-interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/dialog.d.ts","../../../node_modules/@metamask/key-tree/dist/constants.d.cts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/modular.d.ts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/utils.d.ts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/curve.d.ts","../../../node_modules/@metamask/key-tree/dist/curves/ed25519.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/ed25519Bip32.d.cts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/weierstrass.d.ts","../../../node_modules/@metamask/key-tree/dist/curves/secp256k1.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/curve.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/index.d.cts","../../../node_modules/@metamask/key-tree/dist/utils.d.cts","../../../node_modules/@metamask/key-tree/dist/BIP44CoinTypeNode.d.cts","../../../node_modules/@metamask/key-tree/dist/SLIP10Node.d.cts","../../../node_modules/@metamask/key-tree/dist/BIP44Node.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/bip32.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/bip39.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/cip3.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/slip10.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/index.d.cts","../../../node_modules/@metamask/key-tree/dist/index.d.cts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/caip.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/permissions.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip32-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip32-public-key.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip44-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-client-status.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-file.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/component.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Address.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Box.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Copyable.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Divider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Button.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Field.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Form.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/Bold.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/Italic.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Heading.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Image.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Link.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Text.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Row.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Spinner.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/jsx-runtime.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/jsx-dev-runtime.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/validation.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/nodes.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/address.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/copyable.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/divider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/heading.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/image.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/panel.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/spinner.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/text.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/row.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/button.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/form.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/component.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-interface-state.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-locale.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/snap.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-snaps.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/invoke-snap.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/invoke-keyring.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/manage-accounts.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/manage-state.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/notify.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/request-snaps.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/update-interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/methods.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/provider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/global.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/images.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/cronjob.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/home-page.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/keyring.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/lifecycle.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/name-lookup.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/rpc-request.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/transaction.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/signature.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/user-input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/jsx.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/svg.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/error-wrappers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/images.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/snap-utils.d.ts","../../../node_modules/@metamask/keyring-api/dist/index.d.ts","../../message-manager/dist/types/AbstractMessageManager.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../message-manager/dist/types/PersonalMessageManager.d.ts","../../message-manager/dist/types/TypedMessageManager.d.ts","../../message-manager/dist/types/EncryptionPublicKeyManager.d.ts","../../message-manager/dist/types/DecryptMessageManager.d.ts","../../message-manager/dist/types/index.d.ts","../../../node_modules/async-mutex/lib/MutexInterface.d.ts","../../../node_modules/async-mutex/lib/Mutex.d.ts","../../../node_modules/async-mutex/lib/SemaphoreInterface.d.ts","../../../node_modules/async-mutex/lib/Semaphore.d.ts","../../../node_modules/async-mutex/lib/withTimeout.d.ts","../../../node_modules/async-mutex/lib/tryAcquire.d.ts","../../../node_modules/async-mutex/lib/errors.d.ts","../../../node_modules/async-mutex/lib/index.d.ts","../../../node_modules/ethereumjs-wallet/dist/hdkey.d.ts","../../../node_modules/ethereumjs-wallet/dist/thirdparty.d.ts","../../../node_modules/ethereumjs-wallet/dist/index.d.ts","../src/constants.ts","../src/KeyringController.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupSemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/diffLines.d.ts","../../../node_modules/jest-diff/build/printDiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","5b3cd03ae354ea96eff1f74d7c410fe4852e6382227e8b0ecf87ab5e3a5bbcd4","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"056097110efd16869ec118cedb44ecbac9a019576eee808d61304ca6d5cb2cbe","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"6fb8358e10ed92a7f515b7d79da3904c955a3ffd4e14aa9df6f0ea113041f1cf","affectsGlobalScope":true},"45c831238c6dac21c72da5f335747736a56a3847192bf03c84b958a7e9ec93e2","661a11d16ad2e3543a77c53bcd4017ee9a450f47ab7def3ab493a86eae4d550c",{"version":"8cdc646cec7819581ef343b83855b1bfe4fe674f2c84f4fb8dc90d82fb56bd3a","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","9dd56225cc2d8cb8fe5ceb0043ff386987637e12fecc6078896058a99deae284","2375ed4b439215aa3b6d0c6fd175c78a4384b30cb43cbadaecbf0a18954c98cb","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","41231da15bb5e3e806a8395bd15c7befd2ec90f9f4e3c9d0ae1356bccb76dbb0","fccfef201d057cb407fa515311bd608549bab6c7b8adcf8f2df31f5d3b796478",{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true},"5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"464762c6213566d072f1ced5e8e9a954785ec5e53883b7397198abb5ef5b8f71","affectsGlobalScope":true},"6387920dc3e18927335b086deec75bf8e50f879a5e273d32ee7bb7a55ba50572","9bba37424094688c4663c177a1379b229f919b8912889a472f32fdc5f08ddb4d","29a4be13b3a30d3e66667b75c58ec61fb2df8fa0422534fdee3cfb30c5dbf450","83366d901beda79d6eb37aaaf6ca248dcd88946302b2a7d975590783be51e88e","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","43ec77c369473e92e2ecebf0554a0fdaa9c256644a6070f28228dfcceec77351",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"95ed02bacb4502c985b69742ec82a4576d4ff4a6620ecc91593f611d502ae546","bf755525c4e6f85a970b98c4755d98e8aa1b6dbd83a5d8fcc57d3d497351b936","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"28084e15b63e6211769db2fe646d8bc5c4c6776321e0deffe2d12eefd52cb6b9","affectsGlobalScope":true},{"version":"aed37dabf86c99d6c8508700576ecede86688397bc12523541858705a0c737c2","affectsGlobalScope":true},"cc6ef5733d4ea6d2e06310a32dffd2c16418b467c5033d49cecc4f3a25de7497","94768454c3348b6ebe48e45fbad8c92e2bb7af4a35243edbe2b90823d0bd7f9a","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","84da3c28344e621fd1d591f2c09e9595292d2b70018da28a553268ac122597d4","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","6e191fea1db6e9e4fa828259cf489e820ec9170effff57fb081a2f3295db4722","aed943465fbce1efe49ee16b5ea409050f15cd8eaf116f6fadb64ef0772e7d95","70d08483a67bf7050dbedace398ef3fee9f436fcd60517c97c4c1e22e3c6f3e8","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"e933de8143e1d12dd51d89b398760fd5a9081896be366dad88a922d0b29f3c69","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b38d55d08708c2410a3039687db70b4a5bfa69fc4845617c313b5a10d9c5c637","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"a05e2d784c9be7051c4ac87a407c66d2106e23490c18c038bbd0712bde7602fd","affectsGlobalScope":true},{"version":"df90b9d0e9980762da8daf8adf6ffa0c853e76bfd269c377be0d07a9ad87acd2","affectsGlobalScope":true},"cf434b5c04792f62d6f4bdd5e2c8673f36e638e910333c172614d5def9b17f98","1d65d4798df9c2df008884035c41d3e67731f29db5ecb64cd7378797c7c53a2f","0faee6b555890a1cb106e2adc5d3ffd89545b1da894d474e9d436596d654998f","c6c01ea1c42508edf11a36d13b70f6e35774f74355ba5d358354d4a77cc67ea1","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b0297b09e607bec9698cac7cf55463d6731406efb1161ee4d448293b47397c84","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","25139d6a726e0e19d9fc4fa3197367b4a82ec34a08a5ecf23963e142c202c0f3","e3328bffc8eab74665a4fe9c59d6f12f4c8570c3d858497e241eb37efe17dfcf","29389551e426a46421134b55182d6fcf5b143670998bf81db2619c1228235392","c18f7e16982695bdd04e3e183a327d116185f77f1a37b9b2e849d7d93269cd74","2cfb37011989c21dc70b91d521a2d5a4e0f18507f5f536b5dfe504edb15916e8","bb5e02df7aaec7a4ea642639a9963b24b8d9fd6798351f07d8c58616942fbcbf","299a899cb4d061f5d83843ec453e936e9659b2c435746823f90c40eddaef4745","d5610c0fd12870f644b0f42c1bcc4fa2295ac3e3ca01916bdb42c3bdc4c80c36","2c56a8e249b1f45dbdf973100cd37fe2ea68709573cf1fdf2e3052c593be68d8","3553da417ee7b07e388b13bd12a70a1c03e65a6132ba5427fe68f5b362373e6f","612358502042d351c227ba779fdcf6d875d827e424930e60297c533524e50668","d2b5be376ef162aa0c24a826e7dd2d77671a045c085e16d1c1276db4bdccbac7","c4138d8dcccedaff6621e009cf0a54a7bed2a5ad4c509a3513bccc4f417ef939","ad8747fe978dff3e80f4b12b48d37cc8dff11b61d04c035aefbc982ce21201ce","b154f789fd65298e1ba6cbba6944ea892d564c95f3d3700ed85baf8f80748473","c660265aedd7c5b236e2017e53095cb98da66200eb0e8d023b5bf713c36494e8","0efc36bf5c0daca6217fec7063359ccdab8c3a23bb405d25340fae22cf72d74f","5abff0c87d4f9c89715107042d4c73b68ef7a128759f451c8a0fc450cbaaf660","5a03308fbd1af441065149a84c692931bebc7e7735afc23be8684f4e10d3aa06","c787bf4f8f0abbf815cfbd348be41046f2b8f270be24fe7aa8a8fcdd2b7df8c2","e7a5191c663a3228f30104961d548b372e51c5936c01ffc8eddd262bb98d7d7c","43fdc9abe6f8640fda4cdc55a1ee5f666d3fce554277043df925c383137ddf69","f0b09665c9d52de465687fbd3cfb65111d3ffc59ae00c6f42654150f3db05518","72f8c078d06cff690e24ff2b0e118a9de2833dcebf7c53e762dcb505ddf36a68","9705efb0fd901180de84ca4dd11d86f87fd73f99d6a5660a664c048a7487e385","f9b9d0950fdfb90f57e3f045fe73dce7fa6e7921b37622fc12e64fcd90afbd0f","e61b36e7fde608f8bb4b9c973d81556553a715eaef42a181a16ddd7a28da4ac7","03b8389b222af729eae0fb3c33366dcbb1f5a0102ce319bf1d7d5ee987e59fd0","2bf6be7c04db280fdd9b786764f8650c23f9f4d533791cb25a11b25314b76a55","dbb5fc7edd36bfba95cc4dd564e4458276ced30eed18bc05fbda948b3fda8686","c2b556c7cff0dabce2e31cb373ac61c14d8ebc35f1086dff30b39e9ec5357d0d","f958af01131076e8af55d28c4835a51063226ab488ca8738fdee38aeef7d0d33","9f3797b01e3d83d4e4b875699ae984f380ca86aa0a0c9df43ac5bba1cb1f8b7b","752b15ad1b34887adeaa838fc55f5d4ca399026afd266d4ed4db0e3db02eae4e","778331eaea1093451e50be9844bd2b6937c3bb81b0b1ee700624c9774ecfcf2b","0ca0dfc9f657d0822eca9530c7870b22a1d2a5fc48182bdd4d0e6e88e4ad9c35","5c746f034288e6842dd1589b169dcfcc16c5ce5abbd928889ab67aea4fe0b501","92ce6dbbcc135cffd09a58e19fef34bf351391bec92c40d849e4e9997d475769","99e77d092fed72b6b8578d00c7af004f76e98b30ba99f1947535eb4c04a51676","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","b5ef52a9f9083724decc5d060f0b34e3a480deed71c32d55ca16c214eb4cc928","5d3d7938b2d7d0a9f851276741327c2ae4c013e7eb420fc3f7caed3b79c8f37f","14df6b81e50a035e9e391558cf30a0420d03e2eb42c7db9c57f44b818e5d5179","f100912a3785eed4a3d29c12f5910b101af9353454de5ddba9b4d43456c56dd1","446439eacf81a163fd7dfc53b28f80deca3d13b250d67639739aa25aa4491090","98034cd285344125f7165a3bb68246d38ab35fabe7f6d6a7c8f80407d31f548d","06b4a23064991251512df4edc12341d5bc69a17b942da18372312d383c90eee7","0f898802705f9a534b537f1be6c57265080e0abd6993d935554c255e6d56cc1a","745efa7b6e27b7216cccede166a822b56acc41b10a8090966c8cf2c96239cb83","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","5615ccf831db2ffc82145243081ebdb60ea8e1005ee8f975d1c0c1401a9c894e","38682ed3630bb6ecdace80d5a9adc811fc20a419f1940446e306c3a020d083b9","cc182e6e4f691cd6f7bf7cb491247a4c7818f9f1cb2db1d45c65ff906e3f741b","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","e0eda929c6b9b628cdeb0e54cd3582cb97e64f28aab34612fc1431c545899584","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","68cc8d6fcc2f270d7108f02f3ebc59480a54615be3e09a47e14527f349e9d53e","3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","6ab2a6257ae7bb05559841100c786c845fe465a90be7b904db9096c2fb14696b","e87de5e2e71fe0513d6fbd5951a5f8e35595243bbb88fe00b6b2d9383f62fe59","ebb4f551ea58b96443365f487e5c49396c4811dc51e2fd5c3c45928e93047278","7cd0fabd9e9ae5a8faabc2f70d6d9ddd89c65719a30917eacdae9049c16e8a16","bb665725dcc2e2406c572a63038791a7118802ebd947c0e76b0eb38ccd99926c","0c58b5a414a48f68bfea86556a22f505bac4ce0e67ddd5e40e387a4641ce2b78","13de2d120d9122bbff92dca664ebd180241a856d23e705598eb259621a838f0f","2d072cf71b05c374b0406736ae1f402f4ebac96fab8e9e4d6a2ea4d147b1c26e","1507881fb12592d860e8b731a79cccd5880a9e3a3fdb71c8eeb08987420f7c8d","63e64a301fdbb7fb0b58e81b282608594b700e1af51d509de949e88e711a70e8","d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5","50aa290ee8f3ba75c7a3653613ead6594e2e034a7627b249c9a400858cac79f5","9138338d4fff57ba42d57c7316ad1d055b72b90c9e1acbbfa6cfe16db201802a","d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5","863416d62eb7dfa12b63b23c112fd0831aa330816c39d64ca88898ebe5fd62a3","9325a5ce0f09132607e27e599b568e3a67f80ea932f6bbb08bdc1bb7727e11a3","6a8649609161e2794b383ba275b0a6cb4a072dde7b954648f83dc6cdf1bfe4a8","6d3101b183ea67ef606b93fe42127f30b2db5ac3b72c34ca9d6d8b00eb85d0f6","f5d7a36ff056cc314b0f61c89a03c4c36a24183b246e61d958e75e86521304cd","f961ddb0abe88c9815a81132cc9984f0294fd920174fccbdde73d156b8a4ab39","6c951235cbf3c324a20c0e2dfdd013d7b226b0c2a72dbd84925682a8d7199237","aba578ce97acb630b406ffb6ed31302dbd8d2ffcfd671194b1d8704825086e05","3a971ea3e36685b96f24fbd53a94ad8dc061711b84e51fde4cf201f7041e618d","77234d8682b67d78748cb61a63407104dc2c8e3196dcf15a454aae26b42f3ee7","c8be9283a381044a392a0687af5d98d3f51cbada2320b1801a82c948b6e39499","d5cdc145bf5ec321674e31804c075ad408a68c86877ce293970e03634d3709f1","85052c71d72b9b017c88179f57a464d66e22619c7acd7d83b117a79cf1608979","9b6c162d20e2ad4abdcff61a24082564ac59e63092220618162aef6e440c9228","b0874729266d9f7fafb9ff1127fcbad2cf7972b5dcc1fdc104be79266a708bc2","9f9e5bae412fa5909fae636d6733aee27a108cc2ed5b13980611016336774d3c","662fe197bba64bd3f17ee118058cd2d0d2dbe33d7c0c865fd6365d90bfc44e1e","030519c351f800551cac2658038804969ca4584d2c0175a710602ac234ca1340","0278a6939ca83cd040b08ff8c5fc7838b6693ddc52f22526bf158e6b10e0246c","c2d6206e5ba4fd3063b01218c2b3b997afc1cfbeb49fcee991fa8595842ce53d","e9d61a89974e5d8231edab199fe1a5f912a344febc74146611c013435f906de3","813e6dc3098dc7b331ed17bb4a3f96cda83748e989a41c469160ef23776af0f4","3c0913724967da385abf69e4f50dc51d5de5ad7c13d8bedd6746063aab7dd6e0","1fdd00179cddca98c8cfa1c403016f6703a8c34858cfeda33769d86da188ff8d","47ca123995420c8579f010179d3d5e61399ce538fc6386f2438193d48c0013b9","599d6ebec95d21df7ee33d8eb8f0b791ffac4a32026f32cf91fdf417153be473","323a75e01c89a50bb8827d1d624e72c20b0d81d4647a30ee6a695dbb4d76f3b5","d1f010c19eb9c8190bd0859fa3b6f4975543b912b8b85e20bbb0b5bfbdf4d2b3","de4ccc96cef3f97fab148640799abb32a24b567a902a8233913f98481e3131bf",{"version":"801934aa449fe6df584bccdcc5d5b9280295cb7ac84918b6014fc5086e6f9ff6","affectsGlobalScope":true},"6af760fb9ea02dc807c5053d8aee86389c4fce72fbb26af7b9568cac6c4710d5","c62c4ba5e910b4523f7e7adf4a55ec45c2bac99d9d8e9b0fe0c2a800a6f641b9","92131434f876fdd6fcbc40bd54a9d7500c66974362b16bd42641f990468587f4","8cf023c0bd57992fdd2ce6a7030a1874f49c8edc62eaffa9bfffcf18d2a2a1a2","8ea8f3040e38fb50d7dc3653f3b8a0dbb5244e82111576f99ce096bdc0fbf94c","48ed788ad126545a6156fcc37cd3bcf17de18a3e3fe6b6ef62cfb8140d1a45a2","63c271a745f628ffd4bd7ad0a63b021c362c9bd6bf8b18441a7162892395a214","a867ba47f71fe3993cef54246893ff8f01411e12e411d8cf1bd038a448b36404","6a8096993458a3d71229031aa7415974eb5b47b320213e29660adfb519d6a3f4","cb7996a1af5b1d276483cd0c9b9de6540eff021abc90a720511ff4464519a2ff","9df6ec68878d65bc690ea3a33ce3ef5aa8254c36bc5f8346c0c2fd1f3b88a35c","a4fad04c4acc8a4b195cbbccef4c55019104753d547d5c94441643ccc89108a0","0244c23ea642361f7c192c1f0cfff9c12cfa5f51f9b155edd5c0a89fef308d34","ac5da520487547013c3abae0933d6366f51db6df31d1993ddb931ce04b083269","3c69a83bde847af6fc3a53e1bb6b13cd06d38a27a142814b8dacc374f3b93284","5b46f7113f54565e7ffc83f2b474f557a1f54c7e5946769d5be220454656be73","fb58035d39c5759283cb73cfb3548aefe370aa3ad4e81fdb4e46f0979eb7669f","1311c325948b2d5576cebc70b1bf968d3446b4630802bef54120daf04ce1f625","d0b3609e8e7afed0fd0570152255458407e67249b94f6603afdfd68599423f21","17f4c5a1d6eaa87ea27eadcdff9085af3190533d98f799dda79a3af6f9a630ea","3e6f734ddf40e2e99ff7fff9568b7d9720663af9a0632c26a352c8d3270a3f0e","ec13f78303abcf550c5569dfae1446b8ceb89050f68ce04491481e72e8122ae2","a3fc57dbaa7f1efb010399ad4ef4fd9b462aa4e93bf74a9a34b099b97ffcc9cb","ffddd7ec6a450b0cb6f2f73f80de1df963ead312d7c81a8440268f34146ecb87","5d6a36ca0087fd6876df654d1b4192f0e402adfde994ad47e5c065da33692f9c","eb157a09c5f543d98644e2a99a785f9e0e91f054f9fecbf1c3e15831ff5d63a7","edd5530e2b1ccdf65093296e40a8634fcb11ecda3c164c31383a8c34cb04bc9d","9dfaf96d090fe8d96143465d85b4837661ae535143eea9ef99cd20df2e66338e","209d45c27e03c1417c42985252de6c25a2ec23abdc199d88e6139c88b93abd11","0ee5cdba58cfde3012bb9ff2e9edcc4e35a651373a2aa2c83ff9eb7df635419a","540f4dca27ea5a232828b6d91e1b2fce2720bdabaa4c1f3fbf59b672cc58bd8a","ba086b99d545ec6c9ff356989f076b5652ea1b09bcc65b87dfc43a5195a2efcc","c85d9776b36166b928ab1488d9224ebf970d41b0a35f09a3ee0b9bee3e698061","683196f606c5dab1c8c4a24a66d26e00f16f2d4b2a5abe25ebedd37d2954f930","9c3a1b01cba1238fb723ce06b6c163ef6c53be755394406782564d5c42c636b2","6e795e6270d39e918c7a0e62ac73793cda06fcf4b3692ee46583e15f5bf57ab8","0e821ef1eb67fa6144ea4de4277d913f5b1982d7407afd5f93754a8239d41554","5c09195ef359ffa9c6bbdb4fefb101d87ede4b9e9c28213faf5b45d102e4c609","80b4d93a4dcc90a12f6f4bb7c6851a8182ae29e556716d0d80b5c012a5ef554a","2556ef9d1820e0b6bbca6dd65a50ea64f525c4d8247ab50dff44c3f0d14a5643","cbd1c836db190d6e3add07165afc228f04e1f6170e1fe3aa5e6fc24a7e9573a3","9b13881feb958237232586d888a10a39d47cdffe3ee34688ed41888fa7baad94","122fe82cf5af80f0b26832b258b537b7dfe3ec28449c301b259ab10204b50d45","c467dada8fea6d60dff8a8be2675f737cacc76e14e50b72daa0f0710376df84b","9cb80bba611c2dd155a446ce424fe4bb1df2129751bc9416b7e42c055d1ddbff","44f41abb29bf3f4c52270d8119a96c935131e42a9186da15216a76b35e793b4e","043783bebe87efb440183c9ebc8c4fdc1bb92060a5a0f7ce847e30dee7013ac3","e3dc0a97a59dea936b4fb7b1f6f4117b4aac9c86d0cd08b69bab2d0532a8a5e3","5d897601f8a4fe913057019d8211b99b06e3138f625a0cfb601d074f4278271d","cfde5d194dd858ad68f910defaed5b0d28730f8bf38359a9265a93ab29bc7bef","16b21bbe6ad41019c071677877b8fc5dbc8d39a8b0406f020261c5f6f3894be3","f20aae41b169cddcbf3fde8ac380443182c8d7225194e788c404d9e11e6dc75d","87fd9a98cb1e689320ab89adc65e85d140a61260b4f66d12c777f4bd7cae2060","c48566cb13403fca44192b4528e3f2ac993869d39526bd42cd2f2167c0285add","efae20e0c581240c7522e04829da4f0453ca263068596554d4b0e27878c7dfac","3af68ef927788cda7daab34be513fa4508229fdc6e5130d564a0a1ccb3fefafe","bbbd2cbb15a37d5f4dd54ad8c7c537d3df8352117523030fcec7dcbe62a05a58","b50d24ebc117f8805332e7e260e9587f572bb7b2ff0ca1ff6cfafb38015781f3","5cc8b8e18fe7fefab4b3c53a39467b5a0deb4200abae7f063ff0624b9e856c51","8e990781eb0107c25429b1274a31a4f3866a9a46290cce40f354b2a6e71c6c21","8616706e4bd72987bd86c1b4afafa90fa2d4ef2f71708de03a823ab4e9b48e60","b9ce4613536386a98897f1e3d8f61a851ce6cb34dc3c9db4f2ef5f55f007e9e1","77fe56751d7615743937268c72d797fba28309f13ec9079c018b232040fca86a","31b5f53e3d57470830e87f9e03c02d4569ac81d4a758fdda75092f9a3f58beba","d765fbab22fd7003a65ed670100362ec1c90d55a772e6773a774135594e7ea41","1bf86149ef215f258d479695aa35ac89a3d34a6356a6df04e1b5db869289e563","58f4da9e99a4bdbd2f54eeb9303d5b5634b25423d729d44abb3fc55c925495b3","f75cd30f162c2af5e5aca39c01c1a521bfa034fae523793de872815a3468bc08","0cf1123db73dabd86466a462375a6addae52f58d23030c6033f8aadc23539a36","e29cef4158591ed213b1c2cba8988237b1ff369f7a6ecd8cb8ac0302bad1fba8","5307876e4d0021ea01235eb2f7c24671f3d8b37590f4b446cd132a4e1dc9a335","92550acd737790dc60c4c130e6aac78656dd48a8334a4882f40e7f86bdf7a590","3df821880914f8bb3c8107b1107be75c8ddbe2120a2cefabbaf9b65936b5f4dd","20626e4260b7d621745b2e78e883d9de7cc94ec346ef13344dd96eb479813870","078b7043bea0968860374bf4671ed74dd9f6be4e28ab659517d81f74be463c51","68b139ebb9a7f3ee4ded6286d74f978a47968727665120f3bfc560476ce33c4d","56d02c29b2fd39b1b1a1265df291f3f98e6ec3e6119aff9f4cfa44fe888efaa7","2d01884891da6495cb4a2f060e4898209a507e711464c4c1480df85264e863ed","620eb3b3aafe33839ee0f50e2cb237450f066fd88c8367cd15d75d02f7c9146f","6a5a3a7ae4e448668f8986632d2b6adfeebfdc06b0f9256f35c10ec148fa01f0","080b1aa93227952b4dd74b9d2c6e4f6002eb8403533749116a1c53bb9961c02d","874087eec1d457f6e3baf5ac46c42ea200e55040b394fac667aa3a64c49f5f6c","6e8a5b04a18abb192abc89d7219b9c6f633cb3136777ec808673a65f111ca749","6db505486e882a6688c5525cb65f6f06d3c5f16f03f329fbdec01dd379c97f96","d74d2a92b54f95e47d2b76bd5ee516aab7ae93afb79cd34c6681dd29eb09e72a","747e6326a724bc54f799a466a5b5c4978a601a04a063a5bdabe150af2f25b9e2","b57e22e53b56cca7a57bfcfb234aa6a66f9b9e4c07159d7388f94f17a3eaee2c","e47709ec4d1618ef429648cd8ef967aef2005526b34fcbfac33037add347dc71","b81abb3e47fbbb3af41fa75bada89bbcfa4b0feed9a0d6d4b19ed1ce1033b53c","15b330546e9784461058e5fd6e2346bf272140fa6f0cda34e193ae501d8b17b1","4d8ce72fd080bf9a46bdcc274bcbacccedd66d84e203966b197ac25a96932183","73327e6ae34e3f6591877fb75b451cf620cbbd76ee2b678213a9f793633cd0d3","3f1ba2f69944fa346789db7f60d53c9bec00032de0d797967978dea42e77b941","3f5df31539fee4816b97d4e45b4344fbdaf3ca59f6df941f8d780ee441e92cc1","50aaf44eb4d0e086af13729b3471a0a7dce95ea35ebd21c762ba26e203134b2e","3857c1773b8503c3ca45b7bc09ac89c3930c85ce93021054503f73d5d9101b5c","72702bd07fd6fb3ef64aadbcb909103aadfe71ee76e9fdeb11e0c92693cff6cb","f0dd6f7c9783637655478db7d7caf6becd41a79d54482aa59578ce88ab38e9bf",{"version":"cd756ccdabf433dd02b84d755383e489f14b3c1aede0477783aa04830fd5d695","affectsGlobalScope":true},"a4c88dbecdf8ee0c79f5b7c2bf31cd77e593f5d78384e2b674f67d754a549a9e","9cbdff04326da794ba008c0fc977ab062d1fe3fa2e9759654c72ffbe54b64a7c","aa60f8d20d36116fe05edaab24adee3c275209f71b65e272692cf99daf9489e1","150855f967a6490161d5aeed4cc4adf31fcb8f5dbe54b75799c12b8687fc9cc2","cf08b7139adc21b94204e3d4b3daf9946e3462a9e3fdc3e94c87e767e7936e20","47ddb601df40bfa01cebdd06ee8b87d0b72aa1259a4ceba3ad3b5cf68130112a","6b6392704ddb3f50e647dbbb716782bdd0cf8ea9cc134aae256a26223e632b47","afc3ad2a50f7f4de908e26fcf467e09ab8528c0e90f91e602b4865d953839228","df90b0c6b1d81851364c4d97fa23b91a993482bcf4a7bed7c7a24aa41632d494","03c0bc80f67c6f75b02341fbeb9f6ee92c66b90597729377f478885e6ad15a88","11ee9ab699b4619d217c640d917ca198f58066a86bd58c2917197d62aa6601e0","cf9d589d9e73bf32c8e7a6cae6b4a1cf9bef39e5594072533fdce985581a6ddc","959544feb1ca2df29eec6c500f27ea10f4885df245ebd8418fb4b87914614383","6548ab4b57eb9d092471a04513091673345f2fd95d5b876f600402ea8d603ee0","2793e8c6a023d26f78d6777a6d7f20fae3a9a8169863d46d8d54c73071851232","d0f11e830aa1350a31d9c00a0197243e9711e4882947aef53a96c629f405cb10","6610b9f45f1f71d2b1fb67df49cbcabe3f9e668a1ccb7d8328a51407b259ffb3","abbcc437e0792ab2fe08797ceca1ec85a95ec413c51612313b18ab8e75f690f6","e29d76ef1183ac0edf94b4712b6e51730c447c7e773e75ceb44a720b0c9a9fd9","4ee6dc3424998eede9a2a9b114acaaf7969cdda67baf82ba2c9cf88a8eec0ab1","26958d6f77e6db2425ca65df0fbfaba439396ef7f4457f5643fc32e4b62568a6","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","89b040dec8fcfc1de98827e1f4d4977e6ff5d3302c6790e9f10b54b916e1c742","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","e0de9f50e80fed1cc161b50e8e68dc056e38df75a4ef667a06b1922e372de169","6a8b31be08b212d1fc96de0ddd1ea49f32382ba712fea24c70bb56447f643f82","19ac6d624e4c18de4584db4bbdbc55387dbe3d19b3c134e50346bdf165658a17","54e3798c2801e8f3bc7a825d3d26c6a80ce763e19e6cb0b714594c430ef72332","70b8333214aadaccda8d38435911d3e3a686e503837dfda6b8c3f8c83e05729b","f3815045e126ec1b9d224782805a915ae01876a1c7d1eb9b3e320ffadbd63535","d07557f21b2ad690bfe37864aa28090bd7d01c7152b77938d92d97c8419c7144","b843ea5227a9873512aa1226b546a7e52ea5e922b89461f8b202a2f2a3f0b013","64b4d440f905da272e0568224ef8d62c5cd730755c6d453043f2e606e060ec5a","d6b58d955981bc1742501b792f1ab9f4cba0c4611f28dcf1c99376c1c33c9f9c","f0b9f6d5db82c3d1679f71b187c4451dbc2875ba734ce416a4804ad47390970a","a5c38939c3e22954a7166d80ab931ac6757283737b000f1e6dc924c6f4402b88","31a863da9da2a3edec16665695bdbc3134e853195f82dafec58e98c8e1bb3119","b382a659f417df3606f2fbd2d39a02f0aa81d846cd361e79656e135a7896b779","af21e37363b40696508be1e0f1189664d17bc215ac5e64c05f7eb086a6f2ea72","df470b1c65fc51db9486ced8ff89d19c5fa2cfc5c6b3eb32d6cbab354499801e",{"version":"a6703b8328a763c5148eddf07c5801c4b67de507bc25459532d0c0c6622d11c2","signature":"68260f4ebe8f11c39b1d43d6ea75d76da4e81e2965414db9b3bd5ef2a21cdf0e"},{"version":"47b156c2b6951459da66a7dad60da07fee0f3f5e15096362e01fb2c67b99f641","signature":"acf1e448964971d594529ec272a231c39326bafde595da38cd0797266d3b446f"},"40d81f5f052d5954b51f4f5ec258a2231cdba79232e823ba93dc6dce2af4a7ff","4489c6a9fde8934733aa7df6f7911461ee6e9e4ad092736bd416f6b2cc20b2c6","2c8e55457aaf4902941dfdba4061935922e8ee6e120539c9801cd7b400fae050","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","9d38964b57191567a14b396422c87488cecd48f405c642daa734159875ee81d9","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","8c95f96ccd4be0674944077aec1e4f2cccd515ca06d4327562dd017250e7d3fc",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","bc3cba7b0af2d52e7425299aee518db479d44004eff6fbbd206d1ee7e5ec3fb5","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","035a5df183489c2e22f3cf59fc1ed2b043d27f357eecc0eb8d8e840059d44245","a4809f4d92317535e6b22b01019437030077a76fec1d93b9881c9ed4738fcc54","5f53fa0bd22096d2a78533f94e02c899143b8f0f9891a46965294ee8b91a9434","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","b8442e9db28157344d1bc5d8a5a256f1692de213f0c0ddeb84359834015a008c","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","da2b6356b84a40111aaecb18304ea4e4fcb43d70efb1c13ca7d7a906445ee0d3","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","6f294731b495c65ecf46a5694f0082954b961cf05463bea823f8014098eaffa0","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","68a0d0c508e1b6d8d23a519a8a0a3303dc5baa4849ca049f21e5bad41945e3fc","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","b03afe4bec768ae333582915146f48b161e567a81b5ebc31c4d78af089770ac9","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","916be7d770b0ae0406be9486ac12eb9825f21514961dd050594c4b250617d5a8","254d9fb8c872d73d34594be8a200fd7311dbfa10a4116bfc465fba408052f2b3","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","2ccea88888048bbfcacbc9531a5596ea48a3e7dcd0a25f531a81bb717903ba4f","d8f7109e14f20eb735225a62fd3f8366da1a8349e90331cdad57f4b04caf6c5a","cf3d384d082b933d987c4e2fe7bfb8710adfd9dc8155190056ed6695a25a559e","9871b7ee672bc16c78833bdab3052615834b08375cb144e4d2cba74473f4a589","c863198dae89420f3c552b5a03da6ed6d0acfa3807a64772b895db624b0de707","8b03a5e327d7db67112ebbc93b4f744133eda2c1743dbb0a990c61a8007823ef","86c73f2ee1752bac8eeeece234fd05dfcf0637a4fbd8032e4f5f43102faa8eec","42fad1f540271e35ca37cecda12c4ce2eef27f0f5cf0f8dd761d723c744d3159","ff3743a5de32bee10906aff63d1de726f6a7fd6ee2da4b8229054dfa69de2c34","83acd370f7f84f203e71ebba33ba61b7f1291ca027d7f9a662c6307d74e4ac22","1445cec898f90bdd18b2949b9590b3c012f5b7e1804e6e329fb0fe053946d5ec","0e5318ec2275d8da858b541920d9306650ae6ac8012f0e872fe66eb50321a669","cf530297c3fb3a92ec9591dd4fa229d58b5981e45fe6702a0bd2bea53a5e59be","c1f6f7d08d42148ddfe164d36d7aba91f467dbcb3caa715966ff95f55048b3a4","f4e9bf9103191ef3b3612d3ec0044ca4044ca5be27711fe648ada06fad4bcc85","0c1ee27b8f6a00097c2d6d91a21ee4d096ab52c1e28350f6362542b55380059a","7677d5b0db9e020d3017720f853ba18f415219fb3a9597343b1b1012cfd699f7","bc1c6bc119c1784b1a2be6d9c47addec0d83ef0d52c8fbe1f14a51b4dfffc675","52cf2ce99c2a23de70225e252e9822a22b4e0adb82643ab0b710858810e00bf1","770625067bb27a20b9826255a8d47b6b5b0a2d3dfcbd21f89904c731f671ba77","d1ed6765f4d7906a05968fb5cd6d1db8afa14dbe512a4884e8ea5c0f5e142c80","799c0f1b07c092626cf1efd71d459997635911bb5f7fc1196efe449bba87e965","2a184e4462b9914a30b1b5c41cf80c6d3428f17b20d3afb711fff3f0644001fd","9eabde32a3aa5d80de34af2c2206cdc3ee094c6504a8d0c2d6d20c7c179503cc","397c8051b6cfcb48aa22656f0faca2553c5f56187262135162ee79d2b2f6c966","a8ead142e0c87dcd5dc130eba1f8eeed506b08952d905c47621dc2f583b1bff9","a02f10ea5f73130efca046429254a4e3c06b5475baecc8f7b99a0014731be8b3","c2576a4083232b0e2d9bd06875dd43d371dee2e090325a9eac0133fd5650c1cb","4c9a0564bb317349de6a24eb4efea8bb79898fa72ad63a1809165f5bd42970dd","f40ac11d8859092d20f953aae14ba967282c3bb056431a37fced1866ec7a2681","cc11e9e79d4746cc59e0e17473a59d6f104692fd0eeea1bdb2e206eabed83b03","b444a410d34fb5e98aa5ee2b381362044f4884652e8bc8a11c8fe14bbd85518e","c35808c1f5e16d2c571aa65067e3cb95afeff843b259ecfa2fc107a9519b5392","14d5dc055143e941c8743c6a21fa459f961cbc3deedf1bfe47b11587ca4b3ef5","a3ad4e1fc542751005267d50a6298e6765928c0c3a8dce1572f2ba6ca518661c","f237e7c97a3a89f4591afd49ecb3bd8d14f51a1c4adc8fcae3430febedff5eb6","3ffdfbec93b7aed71082af62b8c3e0cc71261cc68d796665faa1e91604fbae8f","662201f943ed45b1ad600d03a90dffe20841e725203ced8b708c91fcd7f9379a","c9ef74c64ed051ea5b958621e7fb853fe3b56e8787c1587aefc6ea988b3c7e79","2462ccfac5f3375794b861abaa81da380f1bbd9401de59ffa43119a0b644253d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","7d8ddf0f021c53099e34ee831a06c394d50371816caa98684812f089b4c6b3d4","7d2a0ba1297be385a89b5515b88cd31b4a1eeef5236f710166dc1b36b1741e1b","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","ab82804a14454734010dcdcd43f564ff7b0389bee4c5692eec76ff5b30d4cf66","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e","bae8d023ef6b23df7da26f51cea44321f95817c190342a36882e93b80d07a960","ae271d475b632ce7b03fea6d9cf6da72439e57a109672671cbc79f54e1386938"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[434],[72,108,109,110,125],[109,110,126,127],[108,109],[108,125,128,131],[108,128,131,132],[129,130,131,133,134],[108,131],[108,125,128,129,130,133],[108,116],[108],[72,108],[60,108],[112,113,114,115,116,117,118,119,120,121,122,123,124],[108,114,115],[108,114,116],[108,135,172,173],[172],[173,174],[108,167],[167,168,169,170,171],[108,139,146,147],[108,139,146,147,167],[108,139,146,147,151],[108,139,146,147,148,150,151],[108,139,146,147,149],[108,139,146,147,152,153,155,156],[145,167],[137,146,147,152,153],[139,145,146],[108,139,146,147,152],[108,139,146,147,150],[108,139,146,147,163],[108,139,146,147,164],[111,136,139,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166],[137],[137,138],[72,108,172,175,184],[175,185],[185,186],[242,243,244,245],[108,125],[247],[135,219,246],[300,309,310,313],[300,309,312],[300,309,311,313],[301,304,305,307],[301,302,303],[304,305,307,308],[301,302,306],[312,318],[300,309,312,318],[309,312,318],[309,312,314,315,316,317],[300,309,310,311,312,313,318],[300,309],[301],[196,219,252],[219,266,268],[219,268,269,290],[196,219,249,251,252,265],[196,219],[250],[253],[196,252],[254,255,264],[263],[251,252,257,263,265,266,267,269,291,292,402],[196,219,403],[219,257,265],[258],[256,259,260,261,262],[196,219,250,252,255],[219,267,401],[190,196],[179],[178],[108,177,179],[179,180,181,182,183],[108,177],[178,219,281,282],[283],[108,177,219,283,285],[108,177,178,219,281,283],[285],[282,283,284,285,286,287,288,289],[108,177,284,287],[282,287],[219,281],[219,270],[270,271],[270,271,272,273],[219],[108,219,293],[219,398],[367],[219,293,367,395,398,399,400],[108,219,274,293],[294,295,296,297,396,397],[190,196,395],[190,196,296],[328],[328,340],[328,329,342,344],[328,340,343],[328,334],[328,333,335],[333,334,335,336],[338,339],[329,330,331,332,337,340,341,342,343,344,345,346],[328,347,348,349,350],[328,347,398],[382],[395],[385,386,387,388,389,390,391,392,393],[219,320],[367,391,398],[320,395,398],[196],[320,321,368,371,381,382,383,394],[196,351,367],[395,398],[319,321],[321],[398],[368],[219,371],[298,299,322,323,324,325,326,327,369,370,372,373,374,375,376,377,378,379,380],[219,373],[298,299,322,323,324,325,326,327,369,370,372,373,374,375,376,377,378,379,398],[219,319,320],[290,381],[219,321],[365],[196,352],[353,354,355,356,357,358,359,360,361,362,363,364],[352,365,366],[199],[196,199],[197,198,199,200,201,202,203,204,205,206,207,208,211,212,213,214,215,216,217,218],[190,196,197],[135,199,205,207],[210],[199,200],[196,214],[108,142],[140,141,144],[140,143],[108,140],[410,411],[434,435,436,437,438],[434,436],[209],[441,442,443],[73,108],[446],[447],[458],[452,457],[461,463,464,465,466,467,468,469,470,471,472,473],[461,462,464,465,466,467,468,469,470,471,472,473],[462,463,464,465,466,467,468,469,470,471,472,473],[461,462,463,465,466,467,468,469,470,471,472,473],[461,462,463,464,466,467,468,469,470,471,472,473],[461,462,463,464,465,467,468,469,470,471,472,473],[461,462,463,464,465,466,468,469,470,471,472,473],[461,462,463,464,465,466,467,469,470,471,472,473],[461,462,463,464,465,466,467,468,470,471,472,473],[461,462,463,464,465,466,467,468,469,471,472,473],[461,462,463,464,465,466,467,468,469,470,472,473],[461,462,463,464,465,466,467,468,469,470,471,473],[461,462,463,464,465,466,467,468,469,470,471,472],[56],[59],[60,65,92],[61,72,73,80,89,100],[61,62,72,80],[63,101],[64,65,73,81],[65,89,97],[66,68,72,80],[67],[68,69],[72],[71,72],[59,72],[72,73,74,89,100],[72,73,74,89],[72,75,80,89,100],[72,73,75,76,80,89,97,100],[75,77,89,97,100],[56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107],[72,78],[79,100,105],[68,72,80,89],[81],[82],[59,83],[84,99,105],[85],[86],[72,87],[87,88,101,103],[60,72,89,90,91],[60,89,91],[89,90],[92],[93],[72,95,96],[95,96],[65,80,89,97],[98],[80,99],[60,75,86,100],[65,101],[89,102],[103],[104],[60,65,72,74,83,89,100,103,105],[89,106],[108,176],[480,519],[480,504,519],[519],[480],[480,505,519],[480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518],[505,519],[520],[524],[420],[422],[420,421,422,423,424,425,426],[420,422],[108,430],[108,428,429],[430],[231],[231,232,233,234,235],[220,221,222,223,224,225,226,227,228,229,230],[450,453],[450,453,454,455],[452],[449,456],[451],[189,191,192,193,194,195],[189,190],[191],[190,191],[189,191],[219,236,237,238],[237],[238],[188,237,238,239],[405],[405,406,409,413],[412],[219,407,408],[178,219,274],[219,275],[219,275,278],[275,276,277,278,279,280],[48,49,125,135,187,219,236,240,241,246,403,419,427,430,431],[432],[72,108,219,240],[404],[404,414],[404,415,416,417,418],[135,187,219,236,240,241,403,419]],"referencedMap":[[436,1],[126,2],[128,3],[110,4],[132,5],[133,6],[129,6],[135,7],[130,6],[134,8],[131,9],[117,10],[114,11],[121,12],[115,10],[112,13],[125,14],[119,11],[116,15],[118,16],[174,17],[173,18],[175,19],[170,20],[168,20],[169,20],[172,21],[154,22],[159,23],[148,22],[153,24],[152,25],[150,26],[157,27],[158,22],[160,28],[155,29],[147,30],[161,31],[163,32],[164,33],[165,34],[167,35],[138,36],[139,37],[185,38],[186,39],[187,40],[246,41],[242,42],[243,11],[245,42],[248,43],[247,44],[311,45],[313,46],[312,47],[308,48],[304,49],[305,49],[309,50],[307,51],[314,52],[315,53],[316,54],[318,55],[317,52],[319,56],[310,57],[303,58],[306,49],[268,59],[269,60],[291,61],[266,62],[249,63],[251,64],[250,63],[254,65],[253,66],[265,67],[255,63],[264,68],[403,69],[256,70],[258,71],[259,72],[260,63],[263,73],[262,74],[292,60],[402,75],[252,76],[181,77],[182,77],[179,78],[180,79],[184,80],[183,81],[283,82],[284,83],[287,84],[285,85],[286,86],[290,87],[288,88],[289,89],[282,90],[271,91],[272,92],[274,93],[270,94],[178,12],[399,95],[293,96],[400,97],[401,98],[294,99],[295,94],[296,94],[398,100],[396,101],[297,102],[328,94],[329,103],[330,103],[331,103],[332,103],[341,103],[342,103],[343,104],[345,105],[346,103],[344,106],[333,103],[335,107],[336,108],[334,103],[337,109],[338,103],[339,103],[340,110],[347,111],[351,112],[349,103],[348,103],[350,113],[383,114],[385,94],[386,115],[394,116],[387,94],[388,94],[389,117],[390,94],[392,118],[391,119],[393,120],[395,121],[368,122],[298,115],[299,123],[322,124],[323,125],[324,124],[326,94],[327,126],[369,127],[372,128],[381,129],[374,130],[373,94],[375,94],[376,96],[380,131],[377,126],[378,128],[379,115],[321,132],[382,133],[371,134],[366,135],[353,136],[362,136],[354,136],[355,136],[364,136],[356,136],[357,136],[365,137],[363,136],[358,136],[361,136],[359,136],[360,136],[367,138],[352,120],[197,120],[198,120],[200,139],[201,120],[202,120],[203,140],[199,120],[219,141],[207,142],[208,143],[211,144],[217,145],[218,146],[143,147],[142,11],[145,148],[140,11],[144,149],[141,150],[412,151],[439,152],[435,1],[437,153],[438,1],[408,11],[210,154],[444,155],[445,156],[447,157],[448,158],[459,159],[458,160],[462,161],[463,162],[461,163],[464,164],[465,165],[466,166],[467,167],[468,168],[469,169],[470,170],[471,171],[472,172],[473,173],[56,174],[57,174],[59,175],[60,176],[61,177],[62,178],[63,179],[64,180],[65,181],[66,182],[67,183],[68,184],[69,184],[70,185],[71,186],[72,187],[73,188],[74,189],[75,190],[76,191],[77,192],[108,193],[78,194],[79,195],[80,196],[81,197],[82,198],[83,199],[84,200],[85,201],[86,202],[87,203],[88,204],[89,205],[91,206],[90,207],[92,208],[93,209],[95,210],[96,211],[97,212],[98,213],[99,214],[100,215],[101,216],[102,217],[103,218],[104,219],[105,220],[106,221],[476,11],[177,222],[479,11],[504,223],[505,224],[480,225],[483,225],[502,223],[503,223],[493,223],[492,226],[490,223],[485,223],[498,223],[496,223],[500,223],[484,223],[497,223],[501,223],[486,223],[487,223],[499,223],[481,223],[488,223],[489,223],[491,223],[495,223],[506,227],[494,223],[482,223],[519,228],[513,227],[515,229],[514,227],[507,227],[508,227],[510,227],[512,227],[516,229],[517,229],[509,229],[511,229],[521,230],[525,231],[421,232],[423,233],[427,234],[425,235],[424,235],[428,236],[430,237],[429,238],[227,239],[229,239],[228,239],[226,239],[236,240],[231,241],[222,239],[223,239],[224,239],[225,239],[454,242],[456,243],[455,242],[453,244],[457,245],[452,246],[196,247],[191,248],[192,249],[193,249],[194,250],[195,250],[190,251],[239,252],[238,253],[237,254],[240,255],[406,256],[414,257],[413,258],[409,259],[275,260],[276,261],[277,261],[279,262],[281,263],[280,261],[432,264],[433,265],[404,266],[418,267],[417,267],[415,268],[416,267],[419,269]],"exportedModulesMap":[[436,1],[126,2],[128,3],[110,4],[132,5],[133,6],[129,6],[135,7],[130,6],[134,8],[131,9],[117,10],[114,11],[121,12],[115,10],[112,13],[125,14],[119,11],[116,15],[118,16],[174,17],[173,18],[175,19],[170,20],[168,20],[169,20],[172,21],[154,22],[159,23],[148,22],[153,24],[152,25],[150,26],[157,27],[158,22],[160,28],[155,29],[147,30],[161,31],[163,32],[164,33],[165,34],[167,35],[138,36],[139,37],[185,38],[186,39],[187,40],[246,41],[242,42],[243,11],[245,42],[248,43],[247,44],[311,45],[313,46],[312,47],[308,48],[304,49],[305,49],[309,50],[307,51],[314,52],[315,53],[316,54],[318,55],[317,52],[319,56],[310,57],[303,58],[306,49],[268,59],[269,60],[291,61],[266,62],[249,63],[251,64],[250,63],[254,65],[253,66],[265,67],[255,63],[264,68],[403,69],[256,70],[258,71],[259,72],[260,63],[263,73],[262,74],[292,60],[402,75],[252,76],[181,77],[182,77],[179,78],[180,79],[184,80],[183,81],[283,82],[284,83],[287,84],[285,85],[286,86],[290,87],[288,88],[289,89],[282,90],[271,91],[272,92],[274,93],[270,94],[178,12],[399,95],[293,96],[400,97],[401,98],[294,99],[295,94],[296,94],[398,100],[396,101],[297,102],[328,94],[329,103],[330,103],[331,103],[332,103],[341,103],[342,103],[343,104],[345,105],[346,103],[344,106],[333,103],[335,107],[336,108],[334,103],[337,109],[338,103],[339,103],[340,110],[347,111],[351,112],[349,103],[348,103],[350,113],[383,114],[385,94],[386,115],[394,116],[387,94],[388,94],[389,117],[390,94],[392,118],[391,119],[393,120],[395,121],[368,122],[298,115],[299,123],[322,124],[323,125],[324,124],[326,94],[327,126],[369,127],[372,128],[381,129],[374,130],[373,94],[375,94],[376,96],[380,131],[377,126],[378,128],[379,115],[321,132],[382,133],[371,134],[366,135],[353,136],[362,136],[354,136],[355,136],[364,136],[356,136],[357,136],[365,137],[363,136],[358,136],[361,136],[359,136],[360,136],[367,138],[352,120],[197,120],[198,120],[200,139],[201,120],[202,120],[203,140],[199,120],[219,141],[207,142],[208,143],[211,144],[217,145],[218,146],[143,147],[142,11],[145,148],[140,11],[144,149],[141,150],[412,151],[439,152],[435,1],[437,153],[438,1],[408,11],[210,154],[444,155],[445,156],[447,157],[448,158],[459,159],[458,160],[462,161],[463,162],[461,163],[464,164],[465,165],[466,166],[467,167],[468,168],[469,169],[470,170],[471,171],[472,172],[473,173],[56,174],[57,174],[59,175],[60,176],[61,177],[62,178],[63,179],[64,180],[65,181],[66,182],[67,183],[68,184],[69,184],[70,185],[71,186],[72,187],[73,188],[74,189],[75,190],[76,191],[77,192],[108,193],[78,194],[79,195],[80,196],[81,197],[82,198],[83,199],[84,200],[85,201],[86,202],[87,203],[88,204],[89,205],[91,206],[90,207],[92,208],[93,209],[95,210],[96,211],[97,212],[98,213],[99,214],[100,215],[101,216],[102,217],[103,218],[104,219],[105,220],[106,221],[476,11],[177,222],[479,11],[504,223],[505,224],[480,225],[483,225],[502,223],[503,223],[493,223],[492,226],[490,223],[485,223],[498,223],[496,223],[500,223],[484,223],[497,223],[501,223],[486,223],[487,223],[499,223],[481,223],[488,223],[489,223],[491,223],[495,223],[506,227],[494,223],[482,223],[519,228],[513,227],[515,229],[514,227],[507,227],[508,227],[510,227],[512,227],[516,229],[517,229],[509,229],[511,229],[521,230],[525,231],[421,232],[423,233],[427,234],[425,235],[424,235],[428,236],[430,237],[429,238],[227,239],[229,239],[228,239],[226,239],[236,240],[231,241],[222,239],[223,239],[224,239],[225,239],[454,242],[456,243],[455,242],[453,244],[457,245],[452,246],[196,247],[191,248],[192,249],[193,249],[194,250],[195,250],[190,251],[239,252],[238,253],[237,254],[240,255],[406,256],[414,257],[413,258],[409,259],[275,260],[276,261],[277,261],[279,262],[281,263],[280,261],[432,270],[433,265],[404,266],[418,267],[417,267],[415,268],[416,267],[419,269]],"semanticDiagnosticsPerFile":[436,434,126,109,128,110,127,132,133,129,135,130,134,131,117,114,121,115,112,120,125,122,123,124,119,116,113,118,174,173,175,170,168,169,172,171,154,159,148,153,152,150,157,158,160,155,149,147,146,156,162,161,163,164,165,167,137,138,139,136,151,166,185,186,187,241,407,244,246,242,243,245,248,247,311,313,312,300,308,304,305,309,307,314,315,316,318,317,319,310,303,301,302,306,268,269,291,266,249,251,250,257,254,253,265,255,264,267,403,256,258,259,260,263,261,262,292,402,252,181,182,179,180,184,183,283,284,287,285,286,290,288,289,282,271,273,272,274,270,178,399,293,400,401,294,295,296,398,396,297,397,328,329,330,331,332,341,342,343,345,346,344,333,335,336,334,337,338,339,340,347,351,349,348,350,320,383,385,386,394,387,388,389,390,392,391,393,384,395,368,298,299,322,323,324,325,326,327,369,370,372,381,374,373,375,376,380,377,378,379,321,382,371,366,353,362,354,355,364,356,357,365,363,358,361,359,360,367,352,197,198,200,201,202,203,204,205,206,199,219,207,208,211,212,213,214,215,216,217,218,143,142,145,140,144,141,410,412,411,439,435,437,438,408,210,440,441,444,442,445,446,447,448,459,458,443,460,462,463,461,464,465,466,467,468,469,470,471,472,473,474,209,56,57,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,58,107,75,76,77,108,78,79,80,81,82,83,84,85,86,87,88,89,91,90,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,475,476,477,478,177,176,479,504,505,480,483,502,503,493,492,490,485,498,496,500,484,497,501,486,487,499,481,488,489,491,495,506,494,482,519,518,513,515,514,507,508,510,512,516,517,509,511,521,520,522,523,524,525,421,420,423,422,426,427,425,424,111,449,428,430,429,230,227,229,228,226,236,231,235,232,234,233,222,223,224,220,221,225,450,454,456,455,453,457,452,451,189,196,191,192,193,194,195,190,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,188,239,238,237,240,406,414,413,405,409,275,276,277,278,279,281,280,432,431,433,404,418,417,415,416,419,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -+{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createProvider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/dom-events.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asyncEventEmitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacyTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/baseTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionFactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/patchCBOR.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/DataItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/cbor-sync.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/lib/index.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/ur.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/urEncoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/fountainEncoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/fountainDecoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/urDecoder.d.ts","../../../node_modules/@ngraveio/bc-ur/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/RegistryType.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/RegistryItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoCoinInfo.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/PathComponent.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoKeypath.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/types.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoHDKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoECKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/Bytes.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/MultiKey.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/ScriptExpression.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoOutput.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoPSBT.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/CryptoAccount.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/Decoder/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/CryptoMultiAccounts.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/errors/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/DerivationSchema.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/KeyDerivation.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/extended/QRHardwareCall.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/utils.d.ts","../../../node_modules/@keystonehq/bc-ur-registry/dist/index.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/EthSignRequest.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/EthSignature.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/ETHNFTItem.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/utlis.d.ts","../../../node_modules/@keystonehq/bc-ur-registry-eth/dist/index.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/InteractionProvider.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/BaseKeyring.d.ts","../../../node_modules/@keystonehq/base-eth-keyring/dist/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@metamask/safe-event-emitter/dist/cjs/index.d.ts","../../../node_modules/@metamask/obs-store/dist/ObservableStore.d.ts","../../../node_modules/@metamask/obs-store/dist/asStream.d.ts","../../../node_modules/@metamask/obs-store/dist/ComposedStore.d.ts","../../../node_modules/@metamask/obs-store/dist/MergedStore.d.ts","../../../node_modules/@metamask/obs-store/dist/transform.d.ts","../../../node_modules/@metamask/obs-store/dist/index.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/MetaMaskInteractionProvider.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/MetaMaskKeyring.d.ts","../../../node_modules/@keystonehq/metamask-airgapped-keyring/dist/index.d.ts","../../base-controller/dist/types/BaseControllerV1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerClass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/RestrictedControllerMessenger.d.ts","../../base-controller/dist/types/ControllerMessenger.d.ts","../../base-controller/dist/types/BaseControllerV2.d.ts","../../base-controller/dist/types/index.d.ts","../../../node_modules/@metamask/browser-passworder/dist/index.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/personal-sign.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/sign-typed-data.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/encryption.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/utils.d.ts","../../../node_modules/@metamask/eth-sig-util/dist/index.d.ts","../../../node_modules/@metamask/eth-simple-keyring/dist/simple-keyring.d.ts","../../../node_modules/@metamask/eth-simple-keyring/dist/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/base-types.d.ts","../../../node_modules/@metamask/keyring-api/dist/btc/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/btc/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/superstruct.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/erc4337/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/erc4337/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/api.d.ts","../../../node_modules/@metamask/keyring-api/dist/contexts.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/eth/EthKeyring.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/eth/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/events.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/rpc.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/types.d.ts","../../../node_modules/@metamask/keyring-api/dist/internal/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/utils.d.ts","../../../node_modules/@metamask/keyring-api/dist/eth/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/api.d.ts","../../../node_modules/@metamask/keyring-api/dist/events.d.ts","../../../node_modules/@metamask/keyring-api/dist/JsonRpcRequest.d.ts","../../../node_modules/@metamask/keyring-api/dist/KeyringClient.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/utils.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/classes.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/errors.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/error-constants.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/index.d.ts","../../json-rpc-engine/src/JsonRpcEngine.ts","../../json-rpc-engine/src/createAsyncMiddleware.ts","../../json-rpc-engine/src/createScaffoldMiddleware.ts","../../json-rpc-engine/src/getUniqueId.ts","../../json-rpc-engine/src/idRemapMiddleware.ts","../../json-rpc-engine/src/mergeMiddleware.ts","../../json-rpc-engine/src/index.ts","../../../node_modules/@metamask/providers/dist/types/utils.d.ts","../../../node_modules/@metamask/providers/dist/types/BaseProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/EIP6963.d.ts","../../../node_modules/@metamask/providers/dist/types/StreamProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/extension-provider/createExternalExtensionProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/MetaMaskInpageProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/initializeInpageProvider.d.ts","../../../node_modules/@metamask/providers/dist/types/shimWeb3.d.ts","../../../node_modules/@metamask/providers/dist/types/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/KeyringSnapRpcClient.d.ts","../../../node_modules/@metamask/keyring-api/dist/rpc-handler.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/errors.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/error-wrappers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/errors.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/helpers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/structs.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/create-interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/dialog.d.ts","../../../node_modules/@metamask/key-tree/dist/constants.d.cts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/modular.d.ts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/utils.d.ts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/curve.d.ts","../../../node_modules/@metamask/key-tree/dist/curves/ed25519.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/ed25519Bip32.d.cts","../../../node_modules/@metamask/key-tree/node_modules/@noble/curves/abstract/weierstrass.d.ts","../../../node_modules/@metamask/key-tree/dist/curves/secp256k1.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/curve.d.cts","../../../node_modules/@metamask/key-tree/dist/curves/index.d.cts","../../../node_modules/@metamask/key-tree/dist/utils.d.cts","../../../node_modules/@metamask/key-tree/dist/BIP44CoinTypeNode.d.cts","../../../node_modules/@metamask/key-tree/dist/SLIP10Node.d.cts","../../../node_modules/@metamask/key-tree/dist/BIP44Node.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/bip32.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/bip39.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/cip3.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/slip10.d.cts","../../../node_modules/@metamask/key-tree/dist/derivers/index.d.cts","../../../node_modules/@metamask/key-tree/dist/index.d.cts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/caip.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/permissions.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip32-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip32-public-key.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-bip44-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-client-status.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-entropy.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-file.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/component.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Address.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Box.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Copyable.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Divider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Button.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Field.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/Form.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/form/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/Bold.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/Italic.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/formatting/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Heading.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Image.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Link.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Text.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Row.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/Spinner.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/components/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/jsx-runtime.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/jsx-dev-runtime.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/validation.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/jsx/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/nodes.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/address.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/copyable.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/divider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/heading.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/image.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/panel.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/spinner.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/text.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/row.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/button.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/form.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/components/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/component.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/ui/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-interface-state.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-locale.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/snap.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/get-snaps.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/invoke-snap.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/invoke-keyring.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/manage-accounts.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/manage-state.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/notify.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/request-snaps.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/update-interface.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/methods.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/methods/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/provider.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/global.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/images.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/cronjob.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/home-page.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/keyring.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/lifecycle.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/name-lookup.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/rpc-request.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/transaction.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/signature.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/user-input.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/handlers/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/types/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/jsx.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/svg.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/internals/index.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/error-wrappers.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/images.d.ts","../../../node_modules/@metamask/snaps-sdk/dist/types/index.d.ts","../../../node_modules/@metamask/keyring-api/dist/snap-utils.d.ts","../../../node_modules/@metamask/keyring-api/dist/index.d.ts","../../message-manager/dist/types/AbstractMessageManager.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../message-manager/dist/types/PersonalMessageManager.d.ts","../../message-manager/dist/types/TypedMessageManager.d.ts","../../message-manager/dist/types/EncryptionPublicKeyManager.d.ts","../../message-manager/dist/types/DecryptMessageManager.d.ts","../../message-manager/dist/types/index.d.ts","../../../node_modules/async-mutex/lib/MutexInterface.d.ts","../../../node_modules/async-mutex/lib/Mutex.d.ts","../../../node_modules/async-mutex/lib/SemaphoreInterface.d.ts","../../../node_modules/async-mutex/lib/Semaphore.d.ts","../../../node_modules/async-mutex/lib/withTimeout.d.ts","../../../node_modules/async-mutex/lib/tryAcquire.d.ts","../../../node_modules/async-mutex/lib/errors.d.ts","../../../node_modules/async-mutex/lib/index.d.ts","../../../node_modules/ethereumjs-wallet/dist/hdkey.d.ts","../../../node_modules/ethereumjs-wallet/dist/thirdparty.d.ts","../../../node_modules/ethereumjs-wallet/dist/index.d.ts","../src/constants.ts","../src/KeyringController.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupSemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/diffLines.d.ts","../../../node_modules/jest-diff/build/printDiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","5b3cd03ae354ea96eff1f74d7c410fe4852e6382227e8b0ecf87ab5e3a5bbcd4","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"056097110efd16869ec118cedb44ecbac9a019576eee808d61304ca6d5cb2cbe","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"6fb8358e10ed92a7f515b7d79da3904c955a3ffd4e14aa9df6f0ea113041f1cf","affectsGlobalScope":true},"45c831238c6dac21c72da5f335747736a56a3847192bf03c84b958a7e9ec93e2","661a11d16ad2e3543a77c53bcd4017ee9a450f47ab7def3ab493a86eae4d550c",{"version":"8cdc646cec7819581ef343b83855b1bfe4fe674f2c84f4fb8dc90d82fb56bd3a","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","9dd56225cc2d8cb8fe5ceb0043ff386987637e12fecc6078896058a99deae284","2375ed4b439215aa3b6d0c6fd175c78a4384b30cb43cbadaecbf0a18954c98cb","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","41231da15bb5e3e806a8395bd15c7befd2ec90f9f4e3c9d0ae1356bccb76dbb0","fccfef201d057cb407fa515311bd608549bab6c7b8adcf8f2df31f5d3b796478",{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true},"5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"464762c6213566d072f1ced5e8e9a954785ec5e53883b7397198abb5ef5b8f71","affectsGlobalScope":true},"6387920dc3e18927335b086deec75bf8e50f879a5e273d32ee7bb7a55ba50572","9bba37424094688c4663c177a1379b229f919b8912889a472f32fdc5f08ddb4d","29a4be13b3a30d3e66667b75c58ec61fb2df8fa0422534fdee3cfb30c5dbf450","83366d901beda79d6eb37aaaf6ca248dcd88946302b2a7d975590783be51e88e","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","43ec77c369473e92e2ecebf0554a0fdaa9c256644a6070f28228dfcceec77351",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"95ed02bacb4502c985b69742ec82a4576d4ff4a6620ecc91593f611d502ae546","bf755525c4e6f85a970b98c4755d98e8aa1b6dbd83a5d8fcc57d3d497351b936","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"28084e15b63e6211769db2fe646d8bc5c4c6776321e0deffe2d12eefd52cb6b9","affectsGlobalScope":true},{"version":"aed37dabf86c99d6c8508700576ecede86688397bc12523541858705a0c737c2","affectsGlobalScope":true},"cc6ef5733d4ea6d2e06310a32dffd2c16418b467c5033d49cecc4f3a25de7497","94768454c3348b6ebe48e45fbad8c92e2bb7af4a35243edbe2b90823d0bd7f9a","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","84da3c28344e621fd1d591f2c09e9595292d2b70018da28a553268ac122597d4","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","6e191fea1db6e9e4fa828259cf489e820ec9170effff57fb081a2f3295db4722","aed943465fbce1efe49ee16b5ea409050f15cd8eaf116f6fadb64ef0772e7d95","70d08483a67bf7050dbedace398ef3fee9f436fcd60517c97c4c1e22e3c6f3e8","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"e933de8143e1d12dd51d89b398760fd5a9081896be366dad88a922d0b29f3c69","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b38d55d08708c2410a3039687db70b4a5bfa69fc4845617c313b5a10d9c5c637","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"a05e2d784c9be7051c4ac87a407c66d2106e23490c18c038bbd0712bde7602fd","affectsGlobalScope":true},{"version":"df90b9d0e9980762da8daf8adf6ffa0c853e76bfd269c377be0d07a9ad87acd2","affectsGlobalScope":true},"cf434b5c04792f62d6f4bdd5e2c8673f36e638e910333c172614d5def9b17f98","1d65d4798df9c2df008884035c41d3e67731f29db5ecb64cd7378797c7c53a2f","0faee6b555890a1cb106e2adc5d3ffd89545b1da894d474e9d436596d654998f","c6c01ea1c42508edf11a36d13b70f6e35774f74355ba5d358354d4a77cc67ea1","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"b0297b09e607bec9698cac7cf55463d6731406efb1161ee4d448293b47397c84","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","25139d6a726e0e19d9fc4fa3197367b4a82ec34a08a5ecf23963e142c202c0f3","e3328bffc8eab74665a4fe9c59d6f12f4c8570c3d858497e241eb37efe17dfcf","29389551e426a46421134b55182d6fcf5b143670998bf81db2619c1228235392","c18f7e16982695bdd04e3e183a327d116185f77f1a37b9b2e849d7d93269cd74","2cfb37011989c21dc70b91d521a2d5a4e0f18507f5f536b5dfe504edb15916e8","bb5e02df7aaec7a4ea642639a9963b24b8d9fd6798351f07d8c58616942fbcbf","299a899cb4d061f5d83843ec453e936e9659b2c435746823f90c40eddaef4745","d5610c0fd12870f644b0f42c1bcc4fa2295ac3e3ca01916bdb42c3bdc4c80c36","2c56a8e249b1f45dbdf973100cd37fe2ea68709573cf1fdf2e3052c593be68d8","3553da417ee7b07e388b13bd12a70a1c03e65a6132ba5427fe68f5b362373e6f","612358502042d351c227ba779fdcf6d875d827e424930e60297c533524e50668","d2b5be376ef162aa0c24a826e7dd2d77671a045c085e16d1c1276db4bdccbac7","c4138d8dcccedaff6621e009cf0a54a7bed2a5ad4c509a3513bccc4f417ef939","ad8747fe978dff3e80f4b12b48d37cc8dff11b61d04c035aefbc982ce21201ce","b154f789fd65298e1ba6cbba6944ea892d564c95f3d3700ed85baf8f80748473","c660265aedd7c5b236e2017e53095cb98da66200eb0e8d023b5bf713c36494e8","0efc36bf5c0daca6217fec7063359ccdab8c3a23bb405d25340fae22cf72d74f","5abff0c87d4f9c89715107042d4c73b68ef7a128759f451c8a0fc450cbaaf660","5a03308fbd1af441065149a84c692931bebc7e7735afc23be8684f4e10d3aa06","c787bf4f8f0abbf815cfbd348be41046f2b8f270be24fe7aa8a8fcdd2b7df8c2","e7a5191c663a3228f30104961d548b372e51c5936c01ffc8eddd262bb98d7d7c","43fdc9abe6f8640fda4cdc55a1ee5f666d3fce554277043df925c383137ddf69","f0b09665c9d52de465687fbd3cfb65111d3ffc59ae00c6f42654150f3db05518","72f8c078d06cff690e24ff2b0e118a9de2833dcebf7c53e762dcb505ddf36a68","9705efb0fd901180de84ca4dd11d86f87fd73f99d6a5660a664c048a7487e385","f9b9d0950fdfb90f57e3f045fe73dce7fa6e7921b37622fc12e64fcd90afbd0f","e61b36e7fde608f8bb4b9c973d81556553a715eaef42a181a16ddd7a28da4ac7","03b8389b222af729eae0fb3c33366dcbb1f5a0102ce319bf1d7d5ee987e59fd0","2bf6be7c04db280fdd9b786764f8650c23f9f4d533791cb25a11b25314b76a55","dbb5fc7edd36bfba95cc4dd564e4458276ced30eed18bc05fbda948b3fda8686","c2b556c7cff0dabce2e31cb373ac61c14d8ebc35f1086dff30b39e9ec5357d0d","f958af01131076e8af55d28c4835a51063226ab488ca8738fdee38aeef7d0d33","9f3797b01e3d83d4e4b875699ae984f380ca86aa0a0c9df43ac5bba1cb1f8b7b","752b15ad1b34887adeaa838fc55f5d4ca399026afd266d4ed4db0e3db02eae4e","778331eaea1093451e50be9844bd2b6937c3bb81b0b1ee700624c9774ecfcf2b","0ca0dfc9f657d0822eca9530c7870b22a1d2a5fc48182bdd4d0e6e88e4ad9c35","5c746f034288e6842dd1589b169dcfcc16c5ce5abbd928889ab67aea4fe0b501","92ce6dbbcc135cffd09a58e19fef34bf351391bec92c40d849e4e9997d475769","99e77d092fed72b6b8578d00c7af004f76e98b30ba99f1947535eb4c04a51676","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","b5ef52a9f9083724decc5d060f0b34e3a480deed71c32d55ca16c214eb4cc928","5d3d7938b2d7d0a9f851276741327c2ae4c013e7eb420fc3f7caed3b79c8f37f","14df6b81e50a035e9e391558cf30a0420d03e2eb42c7db9c57f44b818e5d5179","f100912a3785eed4a3d29c12f5910b101af9353454de5ddba9b4d43456c56dd1","446439eacf81a163fd7dfc53b28f80deca3d13b250d67639739aa25aa4491090","98034cd285344125f7165a3bb68246d38ab35fabe7f6d6a7c8f80407d31f548d","06b4a23064991251512df4edc12341d5bc69a17b942da18372312d383c90eee7","0f898802705f9a534b537f1be6c57265080e0abd6993d935554c255e6d56cc1a","745efa7b6e27b7216cccede166a822b56acc41b10a8090966c8cf2c96239cb83","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","5615ccf831db2ffc82145243081ebdb60ea8e1005ee8f975d1c0c1401a9c894e","38682ed3630bb6ecdace80d5a9adc811fc20a419f1940446e306c3a020d083b9","cc182e6e4f691cd6f7bf7cb491247a4c7818f9f1cb2db1d45c65ff906e3f741b","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","e0eda929c6b9b628cdeb0e54cd3582cb97e64f28aab34612fc1431c545899584","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","68cc8d6fcc2f270d7108f02f3ebc59480a54615be3e09a47e14527f349e9d53e","3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","6ab2a6257ae7bb05559841100c786c845fe465a90be7b904db9096c2fb14696b","e87de5e2e71fe0513d6fbd5951a5f8e35595243bbb88fe00b6b2d9383f62fe59","ebb4f551ea58b96443365f487e5c49396c4811dc51e2fd5c3c45928e93047278","7cd0fabd9e9ae5a8faabc2f70d6d9ddd89c65719a30917eacdae9049c16e8a16","bb665725dcc2e2406c572a63038791a7118802ebd947c0e76b0eb38ccd99926c","0c58b5a414a48f68bfea86556a22f505bac4ce0e67ddd5e40e387a4641ce2b78","13de2d120d9122bbff92dca664ebd180241a856d23e705598eb259621a838f0f","2d072cf71b05c374b0406736ae1f402f4ebac96fab8e9e4d6a2ea4d147b1c26e","1507881fb12592d860e8b731a79cccd5880a9e3a3fdb71c8eeb08987420f7c8d","63e64a301fdbb7fb0b58e81b282608594b700e1af51d509de949e88e711a70e8","d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5","50aa290ee8f3ba75c7a3653613ead6594e2e034a7627b249c9a400858cac79f5","9138338d4fff57ba42d57c7316ad1d055b72b90c9e1acbbfa6cfe16db201802a","d5c19655468e29f60c871b21e73af8ebc653f736e7123ade916f22c4a5f80ce5","863416d62eb7dfa12b63b23c112fd0831aa330816c39d64ca88898ebe5fd62a3","9325a5ce0f09132607e27e599b568e3a67f80ea932f6bbb08bdc1bb7727e11a3","6a8649609161e2794b383ba275b0a6cb4a072dde7b954648f83dc6cdf1bfe4a8","6d3101b183ea67ef606b93fe42127f30b2db5ac3b72c34ca9d6d8b00eb85d0f6","f5d7a36ff056cc314b0f61c89a03c4c36a24183b246e61d958e75e86521304cd","f961ddb0abe88c9815a81132cc9984f0294fd920174fccbdde73d156b8a4ab39","6c951235cbf3c324a20c0e2dfdd013d7b226b0c2a72dbd84925682a8d7199237","aba578ce97acb630b406ffb6ed31302dbd8d2ffcfd671194b1d8704825086e05","3a971ea3e36685b96f24fbd53a94ad8dc061711b84e51fde4cf201f7041e618d","77234d8682b67d78748cb61a63407104dc2c8e3196dcf15a454aae26b42f3ee7","c8be9283a381044a392a0687af5d98d3f51cbada2320b1801a82c948b6e39499","d5cdc145bf5ec321674e31804c075ad408a68c86877ce293970e03634d3709f1","85052c71d72b9b017c88179f57a464d66e22619c7acd7d83b117a79cf1608979","9b6c162d20e2ad4abdcff61a24082564ac59e63092220618162aef6e440c9228","b0874729266d9f7fafb9ff1127fcbad2cf7972b5dcc1fdc104be79266a708bc2","9f9e5bae412fa5909fae636d6733aee27a108cc2ed5b13980611016336774d3c","662fe197bba64bd3f17ee118058cd2d0d2dbe33d7c0c865fd6365d90bfc44e1e","030519c351f800551cac2658038804969ca4584d2c0175a710602ac234ca1340","0278a6939ca83cd040b08ff8c5fc7838b6693ddc52f22526bf158e6b10e0246c","c2d6206e5ba4fd3063b01218c2b3b997afc1cfbeb49fcee991fa8595842ce53d","e9d61a89974e5d8231edab199fe1a5f912a344febc74146611c013435f906de3","813e6dc3098dc7b331ed17bb4a3f96cda83748e989a41c469160ef23776af0f4","3c0913724967da385abf69e4f50dc51d5de5ad7c13d8bedd6746063aab7dd6e0","1fdd00179cddca98c8cfa1c403016f6703a8c34858cfeda33769d86da188ff8d","47ca123995420c8579f010179d3d5e61399ce538fc6386f2438193d48c0013b9","599d6ebec95d21df7ee33d8eb8f0b791ffac4a32026f32cf91fdf417153be473","323a75e01c89a50bb8827d1d624e72c20b0d81d4647a30ee6a695dbb4d76f3b5","d1f010c19eb9c8190bd0859fa3b6f4975543b912b8b85e20bbb0b5bfbdf4d2b3","de4ccc96cef3f97fab148640799abb32a24b567a902a8233913f98481e3131bf",{"version":"801934aa449fe6df584bccdcc5d5b9280295cb7ac84918b6014fc5086e6f9ff6","affectsGlobalScope":true},"6af760fb9ea02dc807c5053d8aee86389c4fce72fbb26af7b9568cac6c4710d5","c62c4ba5e910b4523f7e7adf4a55ec45c2bac99d9d8e9b0fe0c2a800a6f641b9","92131434f876fdd6fcbc40bd54a9d7500c66974362b16bd42641f990468587f4","8cf023c0bd57992fdd2ce6a7030a1874f49c8edc62eaffa9bfffcf18d2a2a1a2","8ea8f3040e38fb50d7dc3653f3b8a0dbb5244e82111576f99ce096bdc0fbf94c","48ed788ad126545a6156fcc37cd3bcf17de18a3e3fe6b6ef62cfb8140d1a45a2","63c271a745f628ffd4bd7ad0a63b021c362c9bd6bf8b18441a7162892395a214","a867ba47f71fe3993cef54246893ff8f01411e12e411d8cf1bd038a448b36404","6a8096993458a3d71229031aa7415974eb5b47b320213e29660adfb519d6a3f4","cb7996a1af5b1d276483cd0c9b9de6540eff021abc90a720511ff4464519a2ff","9df6ec68878d65bc690ea3a33ce3ef5aa8254c36bc5f8346c0c2fd1f3b88a35c","a4fad04c4acc8a4b195cbbccef4c55019104753d547d5c94441643ccc89108a0","0244c23ea642361f7c192c1f0cfff9c12cfa5f51f9b155edd5c0a89fef308d34","ac5da520487547013c3abae0933d6366f51db6df31d1993ddb931ce04b083269","3c69a83bde847af6fc3a53e1bb6b13cd06d38a27a142814b8dacc374f3b93284","5b46f7113f54565e7ffc83f2b474f557a1f54c7e5946769d5be220454656be73","fb58035d39c5759283cb73cfb3548aefe370aa3ad4e81fdb4e46f0979eb7669f","1311c325948b2d5576cebc70b1bf968d3446b4630802bef54120daf04ce1f625","d0b3609e8e7afed0fd0570152255458407e67249b94f6603afdfd68599423f21","17f4c5a1d6eaa87ea27eadcdff9085af3190533d98f799dda79a3af6f9a630ea","3e6f734ddf40e2e99ff7fff9568b7d9720663af9a0632c26a352c8d3270a3f0e","ec13f78303abcf550c5569dfae1446b8ceb89050f68ce04491481e72e8122ae2","a3fc57dbaa7f1efb010399ad4ef4fd9b462aa4e93bf74a9a34b099b97ffcc9cb","ffddd7ec6a450b0cb6f2f73f80de1df963ead312d7c81a8440268f34146ecb87","5d6a36ca0087fd6876df654d1b4192f0e402adfde994ad47e5c065da33692f9c","eb157a09c5f543d98644e2a99a785f9e0e91f054f9fecbf1c3e15831ff5d63a7","edd5530e2b1ccdf65093296e40a8634fcb11ecda3c164c31383a8c34cb04bc9d","9dfaf96d090fe8d96143465d85b4837661ae535143eea9ef99cd20df2e66338e","209d45c27e03c1417c42985252de6c25a2ec23abdc199d88e6139c88b93abd11","0ee5cdba58cfde3012bb9ff2e9edcc4e35a651373a2aa2c83ff9eb7df635419a","540f4dca27ea5a232828b6d91e1b2fce2720bdabaa4c1f3fbf59b672cc58bd8a","ba086b99d545ec6c9ff356989f076b5652ea1b09bcc65b87dfc43a5195a2efcc","c85d9776b36166b928ab1488d9224ebf970d41b0a35f09a3ee0b9bee3e698061","683196f606c5dab1c8c4a24a66d26e00f16f2d4b2a5abe25ebedd37d2954f930","9c3a1b01cba1238fb723ce06b6c163ef6c53be755394406782564d5c42c636b2","6e795e6270d39e918c7a0e62ac73793cda06fcf4b3692ee46583e15f5bf57ab8","0e821ef1eb67fa6144ea4de4277d913f5b1982d7407afd5f93754a8239d41554","5c09195ef359ffa9c6bbdb4fefb101d87ede4b9e9c28213faf5b45d102e4c609","80b4d93a4dcc90a12f6f4bb7c6851a8182ae29e556716d0d80b5c012a5ef554a","2556ef9d1820e0b6bbca6dd65a50ea64f525c4d8247ab50dff44c3f0d14a5643","cbd1c836db190d6e3add07165afc228f04e1f6170e1fe3aa5e6fc24a7e9573a3","9b13881feb958237232586d888a10a39d47cdffe3ee34688ed41888fa7baad94","122fe82cf5af80f0b26832b258b537b7dfe3ec28449c301b259ab10204b50d45","c467dada8fea6d60dff8a8be2675f737cacc76e14e50b72daa0f0710376df84b","9cb80bba611c2dd155a446ce424fe4bb1df2129751bc9416b7e42c055d1ddbff","44f41abb29bf3f4c52270d8119a96c935131e42a9186da15216a76b35e793b4e","043783bebe87efb440183c9ebc8c4fdc1bb92060a5a0f7ce847e30dee7013ac3","e3dc0a97a59dea936b4fb7b1f6f4117b4aac9c86d0cd08b69bab2d0532a8a5e3","5d897601f8a4fe913057019d8211b99b06e3138f625a0cfb601d074f4278271d","cfde5d194dd858ad68f910defaed5b0d28730f8bf38359a9265a93ab29bc7bef","16b21bbe6ad41019c071677877b8fc5dbc8d39a8b0406f020261c5f6f3894be3","f20aae41b169cddcbf3fde8ac380443182c8d7225194e788c404d9e11e6dc75d","87fd9a98cb1e689320ab89adc65e85d140a61260b4f66d12c777f4bd7cae2060","c48566cb13403fca44192b4528e3f2ac993869d39526bd42cd2f2167c0285add","efae20e0c581240c7522e04829da4f0453ca263068596554d4b0e27878c7dfac","3af68ef927788cda7daab34be513fa4508229fdc6e5130d564a0a1ccb3fefafe","bbbd2cbb15a37d5f4dd54ad8c7c537d3df8352117523030fcec7dcbe62a05a58","b50d24ebc117f8805332e7e260e9587f572bb7b2ff0ca1ff6cfafb38015781f3","5cc8b8e18fe7fefab4b3c53a39467b5a0deb4200abae7f063ff0624b9e856c51","8e990781eb0107c25429b1274a31a4f3866a9a46290cce40f354b2a6e71c6c21","8616706e4bd72987bd86c1b4afafa90fa2d4ef2f71708de03a823ab4e9b48e60","b9ce4613536386a98897f1e3d8f61a851ce6cb34dc3c9db4f2ef5f55f007e9e1","77fe56751d7615743937268c72d797fba28309f13ec9079c018b232040fca86a","31b5f53e3d57470830e87f9e03c02d4569ac81d4a758fdda75092f9a3f58beba","d765fbab22fd7003a65ed670100362ec1c90d55a772e6773a774135594e7ea41","1bf86149ef215f258d479695aa35ac89a3d34a6356a6df04e1b5db869289e563","58f4da9e99a4bdbd2f54eeb9303d5b5634b25423d729d44abb3fc55c925495b3","f75cd30f162c2af5e5aca39c01c1a521bfa034fae523793de872815a3468bc08","0cf1123db73dabd86466a462375a6addae52f58d23030c6033f8aadc23539a36","e29cef4158591ed213b1c2cba8988237b1ff369f7a6ecd8cb8ac0302bad1fba8","5307876e4d0021ea01235eb2f7c24671f3d8b37590f4b446cd132a4e1dc9a335","92550acd737790dc60c4c130e6aac78656dd48a8334a4882f40e7f86bdf7a590","3df821880914f8bb3c8107b1107be75c8ddbe2120a2cefabbaf9b65936b5f4dd","20626e4260b7d621745b2e78e883d9de7cc94ec346ef13344dd96eb479813870","078b7043bea0968860374bf4671ed74dd9f6be4e28ab659517d81f74be463c51","68b139ebb9a7f3ee4ded6286d74f978a47968727665120f3bfc560476ce33c4d","56d02c29b2fd39b1b1a1265df291f3f98e6ec3e6119aff9f4cfa44fe888efaa7","2d01884891da6495cb4a2f060e4898209a507e711464c4c1480df85264e863ed","620eb3b3aafe33839ee0f50e2cb237450f066fd88c8367cd15d75d02f7c9146f","6a5a3a7ae4e448668f8986632d2b6adfeebfdc06b0f9256f35c10ec148fa01f0","080b1aa93227952b4dd74b9d2c6e4f6002eb8403533749116a1c53bb9961c02d","874087eec1d457f6e3baf5ac46c42ea200e55040b394fac667aa3a64c49f5f6c","6e8a5b04a18abb192abc89d7219b9c6f633cb3136777ec808673a65f111ca749","6db505486e882a6688c5525cb65f6f06d3c5f16f03f329fbdec01dd379c97f96","d74d2a92b54f95e47d2b76bd5ee516aab7ae93afb79cd34c6681dd29eb09e72a","747e6326a724bc54f799a466a5b5c4978a601a04a063a5bdabe150af2f25b9e2","b57e22e53b56cca7a57bfcfb234aa6a66f9b9e4c07159d7388f94f17a3eaee2c","e47709ec4d1618ef429648cd8ef967aef2005526b34fcbfac33037add347dc71","b81abb3e47fbbb3af41fa75bada89bbcfa4b0feed9a0d6d4b19ed1ce1033b53c","15b330546e9784461058e5fd6e2346bf272140fa6f0cda34e193ae501d8b17b1","4d8ce72fd080bf9a46bdcc274bcbacccedd66d84e203966b197ac25a96932183","73327e6ae34e3f6591877fb75b451cf620cbbd76ee2b678213a9f793633cd0d3","3f1ba2f69944fa346789db7f60d53c9bec00032de0d797967978dea42e77b941","3f5df31539fee4816b97d4e45b4344fbdaf3ca59f6df941f8d780ee441e92cc1","50aaf44eb4d0e086af13729b3471a0a7dce95ea35ebd21c762ba26e203134b2e","3857c1773b8503c3ca45b7bc09ac89c3930c85ce93021054503f73d5d9101b5c","72702bd07fd6fb3ef64aadbcb909103aadfe71ee76e9fdeb11e0c92693cff6cb","f0dd6f7c9783637655478db7d7caf6becd41a79d54482aa59578ce88ab38e9bf",{"version":"cd756ccdabf433dd02b84d755383e489f14b3c1aede0477783aa04830fd5d695","affectsGlobalScope":true},"a4c88dbecdf8ee0c79f5b7c2bf31cd77e593f5d78384e2b674f67d754a549a9e","9cbdff04326da794ba008c0fc977ab062d1fe3fa2e9759654c72ffbe54b64a7c","aa60f8d20d36116fe05edaab24adee3c275209f71b65e272692cf99daf9489e1","150855f967a6490161d5aeed4cc4adf31fcb8f5dbe54b75799c12b8687fc9cc2","cf08b7139adc21b94204e3d4b3daf9946e3462a9e3fdc3e94c87e767e7936e20","47ddb601df40bfa01cebdd06ee8b87d0b72aa1259a4ceba3ad3b5cf68130112a","6b6392704ddb3f50e647dbbb716782bdd0cf8ea9cc134aae256a26223e632b47","afc3ad2a50f7f4de908e26fcf467e09ab8528c0e90f91e602b4865d953839228","df90b0c6b1d81851364c4d97fa23b91a993482bcf4a7bed7c7a24aa41632d494","03c0bc80f67c6f75b02341fbeb9f6ee92c66b90597729377f478885e6ad15a88","11ee9ab699b4619d217c640d917ca198f58066a86bd58c2917197d62aa6601e0","cf9d589d9e73bf32c8e7a6cae6b4a1cf9bef39e5594072533fdce985581a6ddc","959544feb1ca2df29eec6c500f27ea10f4885df245ebd8418fb4b87914614383","6548ab4b57eb9d092471a04513091673345f2fd95d5b876f600402ea8d603ee0","2793e8c6a023d26f78d6777a6d7f20fae3a9a8169863d46d8d54c73071851232","d0f11e830aa1350a31d9c00a0197243e9711e4882947aef53a96c629f405cb10","6610b9f45f1f71d2b1fb67df49cbcabe3f9e668a1ccb7d8328a51407b259ffb3","abbcc437e0792ab2fe08797ceca1ec85a95ec413c51612313b18ab8e75f690f6","e29d76ef1183ac0edf94b4712b6e51730c447c7e773e75ceb44a720b0c9a9fd9","4ee6dc3424998eede9a2a9b114acaaf7969cdda67baf82ba2c9cf88a8eec0ab1","26958d6f77e6db2425ca65df0fbfaba439396ef7f4457f5643fc32e4b62568a6","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","89b040dec8fcfc1de98827e1f4d4977e6ff5d3302c6790e9f10b54b916e1c742","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","e0de9f50e80fed1cc161b50e8e68dc056e38df75a4ef667a06b1922e372de169","6a8b31be08b212d1fc96de0ddd1ea49f32382ba712fea24c70bb56447f643f82","19ac6d624e4c18de4584db4bbdbc55387dbe3d19b3c134e50346bdf165658a17","54e3798c2801e8f3bc7a825d3d26c6a80ce763e19e6cb0b714594c430ef72332","70b8333214aadaccda8d38435911d3e3a686e503837dfda6b8c3f8c83e05729b","f3815045e126ec1b9d224782805a915ae01876a1c7d1eb9b3e320ffadbd63535","d07557f21b2ad690bfe37864aa28090bd7d01c7152b77938d92d97c8419c7144","b843ea5227a9873512aa1226b546a7e52ea5e922b89461f8b202a2f2a3f0b013","64b4d440f905da272e0568224ef8d62c5cd730755c6d453043f2e606e060ec5a","d6b58d955981bc1742501b792f1ab9f4cba0c4611f28dcf1c99376c1c33c9f9c","f0b9f6d5db82c3d1679f71b187c4451dbc2875ba734ce416a4804ad47390970a","a5c38939c3e22954a7166d80ab931ac6757283737b000f1e6dc924c6f4402b88","31a863da9da2a3edec16665695bdbc3134e853195f82dafec58e98c8e1bb3119","b382a659f417df3606f2fbd2d39a02f0aa81d846cd361e79656e135a7896b779","af21e37363b40696508be1e0f1189664d17bc215ac5e64c05f7eb086a6f2ea72","df470b1c65fc51db9486ced8ff89d19c5fa2cfc5c6b3eb32d6cbab354499801e",{"version":"a6703b8328a763c5148eddf07c5801c4b67de507bc25459532d0c0c6622d11c2","signature":"68260f4ebe8f11c39b1d43d6ea75d76da4e81e2965414db9b3bd5ef2a21cdf0e"},{"version":"17ff6bb89c80df67da92a4c4d32ddccef85ce1fc2c56147161d29f638e2a1a87","signature":"acf1e448964971d594529ec272a231c39326bafde595da38cd0797266d3b446f"},"40d81f5f052d5954b51f4f5ec258a2231cdba79232e823ba93dc6dce2af4a7ff","4489c6a9fde8934733aa7df6f7911461ee6e9e4ad092736bd416f6b2cc20b2c6","2c8e55457aaf4902941dfdba4061935922e8ee6e120539c9801cd7b400fae050","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","9d38964b57191567a14b396422c87488cecd48f405c642daa734159875ee81d9","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","8c95f96ccd4be0674944077aec1e4f2cccd515ca06d4327562dd017250e7d3fc",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","bc3cba7b0af2d52e7425299aee518db479d44004eff6fbbd206d1ee7e5ec3fb5","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","035a5df183489c2e22f3cf59fc1ed2b043d27f357eecc0eb8d8e840059d44245","a4809f4d92317535e6b22b01019437030077a76fec1d93b9881c9ed4738fcc54","5f53fa0bd22096d2a78533f94e02c899143b8f0f9891a46965294ee8b91a9434","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","b8442e9db28157344d1bc5d8a5a256f1692de213f0c0ddeb84359834015a008c","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","da2b6356b84a40111aaecb18304ea4e4fcb43d70efb1c13ca7d7a906445ee0d3","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","6f294731b495c65ecf46a5694f0082954b961cf05463bea823f8014098eaffa0","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","68a0d0c508e1b6d8d23a519a8a0a3303dc5baa4849ca049f21e5bad41945e3fc","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","b03afe4bec768ae333582915146f48b161e567a81b5ebc31c4d78af089770ac9","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9","4f6a12044ee6f458db11964153830abbc499e73d065c51c329ec97407f4b13dd","8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","916be7d770b0ae0406be9486ac12eb9825f21514961dd050594c4b250617d5a8","254d9fb8c872d73d34594be8a200fd7311dbfa10a4116bfc465fba408052f2b3","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","2ccea88888048bbfcacbc9531a5596ea48a3e7dcd0a25f531a81bb717903ba4f","d8f7109e14f20eb735225a62fd3f8366da1a8349e90331cdad57f4b04caf6c5a","cf3d384d082b933d987c4e2fe7bfb8710adfd9dc8155190056ed6695a25a559e","9871b7ee672bc16c78833bdab3052615834b08375cb144e4d2cba74473f4a589","c863198dae89420f3c552b5a03da6ed6d0acfa3807a64772b895db624b0de707","8b03a5e327d7db67112ebbc93b4f744133eda2c1743dbb0a990c61a8007823ef","86c73f2ee1752bac8eeeece234fd05dfcf0637a4fbd8032e4f5f43102faa8eec","42fad1f540271e35ca37cecda12c4ce2eef27f0f5cf0f8dd761d723c744d3159","ff3743a5de32bee10906aff63d1de726f6a7fd6ee2da4b8229054dfa69de2c34","83acd370f7f84f203e71ebba33ba61b7f1291ca027d7f9a662c6307d74e4ac22","1445cec898f90bdd18b2949b9590b3c012f5b7e1804e6e329fb0fe053946d5ec","0e5318ec2275d8da858b541920d9306650ae6ac8012f0e872fe66eb50321a669","cf530297c3fb3a92ec9591dd4fa229d58b5981e45fe6702a0bd2bea53a5e59be","c1f6f7d08d42148ddfe164d36d7aba91f467dbcb3caa715966ff95f55048b3a4","f4e9bf9103191ef3b3612d3ec0044ca4044ca5be27711fe648ada06fad4bcc85","0c1ee27b8f6a00097c2d6d91a21ee4d096ab52c1e28350f6362542b55380059a","7677d5b0db9e020d3017720f853ba18f415219fb3a9597343b1b1012cfd699f7","bc1c6bc119c1784b1a2be6d9c47addec0d83ef0d52c8fbe1f14a51b4dfffc675","52cf2ce99c2a23de70225e252e9822a22b4e0adb82643ab0b710858810e00bf1","770625067bb27a20b9826255a8d47b6b5b0a2d3dfcbd21f89904c731f671ba77","d1ed6765f4d7906a05968fb5cd6d1db8afa14dbe512a4884e8ea5c0f5e142c80","799c0f1b07c092626cf1efd71d459997635911bb5f7fc1196efe449bba87e965","2a184e4462b9914a30b1b5c41cf80c6d3428f17b20d3afb711fff3f0644001fd","9eabde32a3aa5d80de34af2c2206cdc3ee094c6504a8d0c2d6d20c7c179503cc","397c8051b6cfcb48aa22656f0faca2553c5f56187262135162ee79d2b2f6c966","a8ead142e0c87dcd5dc130eba1f8eeed506b08952d905c47621dc2f583b1bff9","a02f10ea5f73130efca046429254a4e3c06b5475baecc8f7b99a0014731be8b3","c2576a4083232b0e2d9bd06875dd43d371dee2e090325a9eac0133fd5650c1cb","4c9a0564bb317349de6a24eb4efea8bb79898fa72ad63a1809165f5bd42970dd","f40ac11d8859092d20f953aae14ba967282c3bb056431a37fced1866ec7a2681","cc11e9e79d4746cc59e0e17473a59d6f104692fd0eeea1bdb2e206eabed83b03","b444a410d34fb5e98aa5ee2b381362044f4884652e8bc8a11c8fe14bbd85518e","c35808c1f5e16d2c571aa65067e3cb95afeff843b259ecfa2fc107a9519b5392","14d5dc055143e941c8743c6a21fa459f961cbc3deedf1bfe47b11587ca4b3ef5","a3ad4e1fc542751005267d50a6298e6765928c0c3a8dce1572f2ba6ca518661c","f237e7c97a3a89f4591afd49ecb3bd8d14f51a1c4adc8fcae3430febedff5eb6","3ffdfbec93b7aed71082af62b8c3e0cc71261cc68d796665faa1e91604fbae8f","662201f943ed45b1ad600d03a90dffe20841e725203ced8b708c91fcd7f9379a","c9ef74c64ed051ea5b958621e7fb853fe3b56e8787c1587aefc6ea988b3c7e79","2462ccfac5f3375794b861abaa81da380f1bbd9401de59ffa43119a0b644253d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","7d8ddf0f021c53099e34ee831a06c394d50371816caa98684812f089b4c6b3d4","7d2a0ba1297be385a89b5515b88cd31b4a1eeef5236f710166dc1b36b1741e1b","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","ab82804a14454734010dcdcd43f564ff7b0389bee4c5692eec76ff5b30d4cf66","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e","bae8d023ef6b23df7da26f51cea44321f95817c190342a36882e93b80d07a960","ae271d475b632ce7b03fea6d9cf6da72439e57a109672671cbc79f54e1386938"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[434],[72,108,109,110,125],[109,110,126,127],[108,109],[108,125,128,131],[108,128,131,132],[129,130,131,133,134],[108,131],[108,125,128,129,130,133],[108,116],[108],[72,108],[60,108],[112,113,114,115,116,117,118,119,120,121,122,123,124],[108,114,115],[108,114,116],[108,135,172,173],[172],[173,174],[108,167],[167,168,169,170,171],[108,139,146,147],[108,139,146,147,167],[108,139,146,147,151],[108,139,146,147,148,150,151],[108,139,146,147,149],[108,139,146,147,152,153,155,156],[145,167],[137,146,147,152,153],[139,145,146],[108,139,146,147,152],[108,139,146,147,150],[108,139,146,147,163],[108,139,146,147,164],[111,136,139,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166],[137],[137,138],[72,108,172,175,184],[175,185],[185,186],[242,243,244,245],[108,125],[247],[135,219,246],[300,309,310,313],[300,309,312],[300,309,311,313],[301,304,305,307],[301,302,303],[304,305,307,308],[301,302,306],[312,318],[300,309,312,318],[309,312,318],[309,312,314,315,316,317],[300,309,310,311,312,313,318],[300,309],[301],[196,219,252],[219,266,268],[219,268,269,290],[196,219,249,251,252,265],[196,219],[250],[253],[196,252],[254,255,264],[263],[251,252,257,263,265,266,267,269,291,292,402],[196,219,403],[219,257,265],[258],[256,259,260,261,262],[196,219,250,252,255],[219,267,401],[190,196],[179],[178],[108,177,179],[179,180,181,182,183],[108,177],[178,219,281,282],[283],[108,177,219,283,285],[108,177,178,219,281,283],[285],[282,283,284,285,286,287,288,289],[108,177,284,287],[282,287],[219,281],[219,270],[270,271],[270,271,272,273],[219],[108,219,293],[219,398],[367],[219,293,367,395,398,399,400],[108,219,274,293],[294,295,296,297,396,397],[190,196,395],[190,196,296],[328],[328,340],[328,329,342,344],[328,340,343],[328,334],[328,333,335],[333,334,335,336],[338,339],[329,330,331,332,337,340,341,342,343,344,345,346],[328,347,348,349,350],[328,347,398],[382],[395],[385,386,387,388,389,390,391,392,393],[219,320],[367,391,398],[320,395,398],[196],[320,321,368,371,381,382,383,394],[196,351,367],[395,398],[319,321],[321],[398],[368],[219,371],[298,299,322,323,324,325,326,327,369,370,372,373,374,375,376,377,378,379,380],[219,373],[298,299,322,323,324,325,326,327,369,370,372,373,374,375,376,377,378,379,398],[219,319,320],[290,381],[219,321],[365],[196,352],[353,354,355,356,357,358,359,360,361,362,363,364],[352,365,366],[199],[196,199],[197,198,199,200,201,202,203,204,205,206,207,208,211,212,213,214,215,216,217,218],[190,196,197],[135,199,205,207],[210],[199,200],[196,214],[108,142],[140,141,144],[140,143],[108,140],[410,411],[434,435,436,437,438],[434,436],[209],[441,442,443],[73,108],[446],[447],[458],[452,457],[461,463,464,465,466,467,468,469,470,471,472,473],[461,462,464,465,466,467,468,469,470,471,472,473],[462,463,464,465,466,467,468,469,470,471,472,473],[461,462,463,465,466,467,468,469,470,471,472,473],[461,462,463,464,466,467,468,469,470,471,472,473],[461,462,463,464,465,467,468,469,470,471,472,473],[461,462,463,464,465,466,468,469,470,471,472,473],[461,462,463,464,465,466,467,469,470,471,472,473],[461,462,463,464,465,466,467,468,470,471,472,473],[461,462,463,464,465,466,467,468,469,471,472,473],[461,462,463,464,465,466,467,468,469,470,472,473],[461,462,463,464,465,466,467,468,469,470,471,473],[461,462,463,464,465,466,467,468,469,470,471,472],[56],[59],[60,65,92],[61,72,73,80,89,100],[61,62,72,80],[63,101],[64,65,73,81],[65,89,97],[66,68,72,80],[67],[68,69],[72],[71,72],[59,72],[72,73,74,89,100],[72,73,74,89],[72,75,80,89,100],[72,73,75,76,80,89,97,100],[75,77,89,97,100],[56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107],[72,78],[79,100,105],[68,72,80,89],[81],[82],[59,83],[84,99,105],[85],[86],[72,87],[87,88,101,103],[60,72,89,90,91],[60,89,91],[89,90],[92],[93],[72,95,96],[95,96],[65,80,89,97],[98],[80,99],[60,75,86,100],[65,101],[89,102],[103],[104],[60,65,72,74,83,89,100,103,105],[89,106],[108,176],[480,519],[480,504,519],[519],[480],[480,505,519],[480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518],[505,519],[520],[524],[420],[422],[420,421,422,423,424,425,426],[420,422],[108,430],[108,428,429],[430],[231],[231,232,233,234,235],[220,221,222,223,224,225,226,227,228,229,230],[450,453],[450,453,454,455],[452],[449,456],[451],[189,191,192,193,194,195],[189,190],[191],[190,191],[189,191],[219,236,237,238],[237],[238],[188,237,238,239],[405],[405,406,409,413],[412],[219,407,408],[178,219,274],[219,275],[219,275,278],[275,276,277,278,279,280],[48,49,125,135,187,219,236,240,241,246,403,419,427,430,431],[432],[72,108,219,240],[404],[404,414],[404,415,416,417,418],[135,187,219,236,240,241,403,419]],"referencedMap":[[436,1],[126,2],[128,3],[110,4],[132,5],[133,6],[129,6],[135,7],[130,6],[134,8],[131,9],[117,10],[114,11],[121,12],[115,10],[112,13],[125,14],[119,11],[116,15],[118,16],[174,17],[173,18],[175,19],[170,20],[168,20],[169,20],[172,21],[154,22],[159,23],[148,22],[153,24],[152,25],[150,26],[157,27],[158,22],[160,28],[155,29],[147,30],[161,31],[163,32],[164,33],[165,34],[167,35],[138,36],[139,37],[185,38],[186,39],[187,40],[246,41],[242,42],[243,11],[245,42],[248,43],[247,44],[311,45],[313,46],[312,47],[308,48],[304,49],[305,49],[309,50],[307,51],[314,52],[315,53],[316,54],[318,55],[317,52],[319,56],[310,57],[303,58],[306,49],[268,59],[269,60],[291,61],[266,62],[249,63],[251,64],[250,63],[254,65],[253,66],[265,67],[255,63],[264,68],[403,69],[256,70],[258,71],[259,72],[260,63],[263,73],[262,74],[292,60],[402,75],[252,76],[181,77],[182,77],[179,78],[180,79],[184,80],[183,81],[283,82],[284,83],[287,84],[285,85],[286,86],[290,87],[288,88],[289,89],[282,90],[271,91],[272,92],[274,93],[270,94],[178,12],[399,95],[293,96],[400,97],[401,98],[294,99],[295,94],[296,94],[398,100],[396,101],[297,102],[328,94],[329,103],[330,103],[331,103],[332,103],[341,103],[342,103],[343,104],[345,105],[346,103],[344,106],[333,103],[335,107],[336,108],[334,103],[337,109],[338,103],[339,103],[340,110],[347,111],[351,112],[349,103],[348,103],[350,113],[383,114],[385,94],[386,115],[394,116],[387,94],[388,94],[389,117],[390,94],[392,118],[391,119],[393,120],[395,121],[368,122],[298,115],[299,123],[322,124],[323,125],[324,124],[326,94],[327,126],[369,127],[372,128],[381,129],[374,130],[373,94],[375,94],[376,96],[380,131],[377,126],[378,128],[379,115],[321,132],[382,133],[371,134],[366,135],[353,136],[362,136],[354,136],[355,136],[364,136],[356,136],[357,136],[365,137],[363,136],[358,136],[361,136],[359,136],[360,136],[367,138],[352,120],[197,120],[198,120],[200,139],[201,120],[202,120],[203,140],[199,120],[219,141],[207,142],[208,143],[211,144],[217,145],[218,146],[143,147],[142,11],[145,148],[140,11],[144,149],[141,150],[412,151],[439,152],[435,1],[437,153],[438,1],[408,11],[210,154],[444,155],[445,156],[447,157],[448,158],[459,159],[458,160],[462,161],[463,162],[461,163],[464,164],[465,165],[466,166],[467,167],[468,168],[469,169],[470,170],[471,171],[472,172],[473,173],[56,174],[57,174],[59,175],[60,176],[61,177],[62,178],[63,179],[64,180],[65,181],[66,182],[67,183],[68,184],[69,184],[70,185],[71,186],[72,187],[73,188],[74,189],[75,190],[76,191],[77,192],[108,193],[78,194],[79,195],[80,196],[81,197],[82,198],[83,199],[84,200],[85,201],[86,202],[87,203],[88,204],[89,205],[91,206],[90,207],[92,208],[93,209],[95,210],[96,211],[97,212],[98,213],[99,214],[100,215],[101,216],[102,217],[103,218],[104,219],[105,220],[106,221],[476,11],[177,222],[479,11],[504,223],[505,224],[480,225],[483,225],[502,223],[503,223],[493,223],[492,226],[490,223],[485,223],[498,223],[496,223],[500,223],[484,223],[497,223],[501,223],[486,223],[487,223],[499,223],[481,223],[488,223],[489,223],[491,223],[495,223],[506,227],[494,223],[482,223],[519,228],[513,227],[515,229],[514,227],[507,227],[508,227],[510,227],[512,227],[516,229],[517,229],[509,229],[511,229],[521,230],[525,231],[421,232],[423,233],[427,234],[425,235],[424,235],[428,236],[430,237],[429,238],[227,239],[229,239],[228,239],[226,239],[236,240],[231,241],[222,239],[223,239],[224,239],[225,239],[454,242],[456,243],[455,242],[453,244],[457,245],[452,246],[196,247],[191,248],[192,249],[193,249],[194,250],[195,250],[190,251],[239,252],[238,253],[237,254],[240,255],[406,256],[414,257],[413,258],[409,259],[275,260],[276,261],[277,261],[279,262],[281,263],[280,261],[432,264],[433,265],[404,266],[418,267],[417,267],[415,268],[416,267],[419,269]],"exportedModulesMap":[[436,1],[126,2],[128,3],[110,4],[132,5],[133,6],[129,6],[135,7],[130,6],[134,8],[131,9],[117,10],[114,11],[121,12],[115,10],[112,13],[125,14],[119,11],[116,15],[118,16],[174,17],[173,18],[175,19],[170,20],[168,20],[169,20],[172,21],[154,22],[159,23],[148,22],[153,24],[152,25],[150,26],[157,27],[158,22],[160,28],[155,29],[147,30],[161,31],[163,32],[164,33],[165,34],[167,35],[138,36],[139,37],[185,38],[186,39],[187,40],[246,41],[242,42],[243,11],[245,42],[248,43],[247,44],[311,45],[313,46],[312,47],[308,48],[304,49],[305,49],[309,50],[307,51],[314,52],[315,53],[316,54],[318,55],[317,52],[319,56],[310,57],[303,58],[306,49],[268,59],[269,60],[291,61],[266,62],[249,63],[251,64],[250,63],[254,65],[253,66],[265,67],[255,63],[264,68],[403,69],[256,70],[258,71],[259,72],[260,63],[263,73],[262,74],[292,60],[402,75],[252,76],[181,77],[182,77],[179,78],[180,79],[184,80],[183,81],[283,82],[284,83],[287,84],[285,85],[286,86],[290,87],[288,88],[289,89],[282,90],[271,91],[272,92],[274,93],[270,94],[178,12],[399,95],[293,96],[400,97],[401,98],[294,99],[295,94],[296,94],[398,100],[396,101],[297,102],[328,94],[329,103],[330,103],[331,103],[332,103],[341,103],[342,103],[343,104],[345,105],[346,103],[344,106],[333,103],[335,107],[336,108],[334,103],[337,109],[338,103],[339,103],[340,110],[347,111],[351,112],[349,103],[348,103],[350,113],[383,114],[385,94],[386,115],[394,116],[387,94],[388,94],[389,117],[390,94],[392,118],[391,119],[393,120],[395,121],[368,122],[298,115],[299,123],[322,124],[323,125],[324,124],[326,94],[327,126],[369,127],[372,128],[381,129],[374,130],[373,94],[375,94],[376,96],[380,131],[377,126],[378,128],[379,115],[321,132],[382,133],[371,134],[366,135],[353,136],[362,136],[354,136],[355,136],[364,136],[356,136],[357,136],[365,137],[363,136],[358,136],[361,136],[359,136],[360,136],[367,138],[352,120],[197,120],[198,120],[200,139],[201,120],[202,120],[203,140],[199,120],[219,141],[207,142],[208,143],[211,144],[217,145],[218,146],[143,147],[142,11],[145,148],[140,11],[144,149],[141,150],[412,151],[439,152],[435,1],[437,153],[438,1],[408,11],[210,154],[444,155],[445,156],[447,157],[448,158],[459,159],[458,160],[462,161],[463,162],[461,163],[464,164],[465,165],[466,166],[467,167],[468,168],[469,169],[470,170],[471,171],[472,172],[473,173],[56,174],[57,174],[59,175],[60,176],[61,177],[62,178],[63,179],[64,180],[65,181],[66,182],[67,183],[68,184],[69,184],[70,185],[71,186],[72,187],[73,188],[74,189],[75,190],[76,191],[77,192],[108,193],[78,194],[79,195],[80,196],[81,197],[82,198],[83,199],[84,200],[85,201],[86,202],[87,203],[88,204],[89,205],[91,206],[90,207],[92,208],[93,209],[95,210],[96,211],[97,212],[98,213],[99,214],[100,215],[101,216],[102,217],[103,218],[104,219],[105,220],[106,221],[476,11],[177,222],[479,11],[504,223],[505,224],[480,225],[483,225],[502,223],[503,223],[493,223],[492,226],[490,223],[485,223],[498,223],[496,223],[500,223],[484,223],[497,223],[501,223],[486,223],[487,223],[499,223],[481,223],[488,223],[489,223],[491,223],[495,223],[506,227],[494,223],[482,223],[519,228],[513,227],[515,229],[514,227],[507,227],[508,227],[510,227],[512,227],[516,229],[517,229],[509,229],[511,229],[521,230],[525,231],[421,232],[423,233],[427,234],[425,235],[424,235],[428,236],[430,237],[429,238],[227,239],[229,239],[228,239],[226,239],[236,240],[231,241],[222,239],[223,239],[224,239],[225,239],[454,242],[456,243],[455,242],[453,244],[457,245],[452,246],[196,247],[191,248],[192,249],[193,249],[194,250],[195,250],[190,251],[239,252],[238,253],[237,254],[240,255],[406,256],[414,257],[413,258],[409,259],[275,260],[276,261],[277,261],[279,262],[281,263],[280,261],[432,270],[433,265],[404,266],[418,267],[417,267],[415,268],[416,267],[419,269]],"semanticDiagnosticsPerFile":[436,434,126,109,128,110,127,132,133,129,135,130,134,131,117,114,121,115,112,120,125,122,123,124,119,116,113,118,174,173,175,170,168,169,172,171,154,159,148,153,152,150,157,158,160,155,149,147,146,156,162,161,163,164,165,167,137,138,139,136,151,166,185,186,187,241,407,244,246,242,243,245,248,247,311,313,312,300,308,304,305,309,307,314,315,316,318,317,319,310,303,301,302,306,268,269,291,266,249,251,250,257,254,253,265,255,264,267,403,256,258,259,260,263,261,262,292,402,252,181,182,179,180,184,183,283,284,287,285,286,290,288,289,282,271,273,272,274,270,178,399,293,400,401,294,295,296,398,396,297,397,328,329,330,331,332,341,342,343,345,346,344,333,335,336,334,337,338,339,340,347,351,349,348,350,320,383,385,386,394,387,388,389,390,392,391,393,384,395,368,298,299,322,323,324,325,326,327,369,370,372,381,374,373,375,376,380,377,378,379,321,382,371,366,353,362,354,355,364,356,357,365,363,358,361,359,360,367,352,197,198,200,201,202,203,204,205,206,199,219,207,208,211,212,213,214,215,216,217,218,143,142,145,140,144,141,410,412,411,439,435,437,438,408,210,440,441,444,442,445,446,447,448,459,458,443,460,462,463,461,464,465,466,467,468,469,470,471,472,473,474,209,56,57,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,58,107,75,76,77,108,78,79,80,81,82,83,84,85,86,87,88,89,91,90,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,475,476,477,478,177,176,479,504,505,480,483,502,503,493,492,490,485,498,496,500,484,497,501,486,487,499,481,488,489,491,495,506,494,482,519,518,513,515,514,507,508,510,512,516,517,509,511,521,520,522,523,524,525,421,420,423,422,426,427,425,424,111,449,428,430,429,230,227,229,228,226,236,231,235,232,234,233,222,223,224,220,221,225,450,454,456,455,453,457,452,451,189,196,191,192,193,194,195,190,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,188,239,238,237,240,406,414,413,405,409,275,276,277,278,279,281,280,432,431,433,404,418,417,415,416,419,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -diff --git a/dist/types/KeyringController.d.ts.map b/dist/types/KeyringController.d.ts.map -index 310d2853a09b2ce1c4aa6c457ba4450103127868..3715da4a8b1826fed478f8b7c8c3de80dafa9987 100644 ---- a/dist/types/KeyringController.d.ts.map -+++ b/dist/types/KeyringController.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"KeyringController.d.ts","sourceRoot":"","sources":["../../src/KeyringController.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -+{"version":3,"file":"KeyringController.d.ts","sourceRoot":"","sources":["../../src/KeyringController.ts"],"names":[],"mappings":"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"} -\ No newline at end of file diff --git a/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch b/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch new file mode 100644 index 000000000000..439c1ddf49ae --- /dev/null +++ b/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch @@ -0,0 +1,13 @@ +diff --git a/package.json b/package.json +index 5a6217eaed16fdfe7f1ad693871f85320bd6b421..69bdf1a9155497e37fc58db7bbc74597fd543535 100644 +--- a/package.json ++++ b/package.json +@@ -18,7 +18,7 @@ + "sideEffects": false, + "exports": { + ".": { +- "import": "./dist/index.mjs", ++ "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + }, diff --git a/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch b/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch deleted file mode 100644 index 91cbc903b5c6..000000000000 --- a/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch +++ /dev/null @@ -1,24 +0,0 @@ -diff --git a/dist/chunk-4ZD3DTQ7.js b/dist/chunk-4ZD3DTQ7.js -index e172d15715b3cb4f26c42e51f8c7ff7394075bfe..2c32427d0e005c8db2e4aa524609972fd57cba60 100644 ---- a/dist/chunk-4ZD3DTQ7.js -+++ b/dist/chunk-4ZD3DTQ7.js -@@ -341,7 +341,6 @@ var NetworkController = class extends _basecontroller.BaseController { - async initializeProvider() { - _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _ensureAutoManagedNetworkClientRegistryPopulated, ensureAutoManagedNetworkClientRegistryPopulated_fn).call(this); - _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _applyNetworkSelection, applyNetworkSelection_fn).call(this); -- await this.lookupNetwork(); - } - /** - * Refreshes the network meta with EIP-1559 support and the network status -diff --git a/dist/chunk-UG2NYGJD.mjs b/dist/chunk-UG2NYGJD.mjs -index c39eb49a4a1d2b4ddb78aadb4fb03446b1705528..73f4df206f489036ed52831ac685219074bb768b 100644 ---- a/dist/chunk-UG2NYGJD.mjs -+++ b/dist/chunk-UG2NYGJD.mjs -@@ -341,7 +341,6 @@ var NetworkController = class extends BaseController { - async initializeProvider() { - __privateMethod(this, _ensureAutoManagedNetworkClientRegistryPopulated, ensureAutoManagedNetworkClientRegistryPopulated_fn).call(this); - __privateMethod(this, _applyNetworkSelection, applyNetworkSelection_fn).call(this); -- await this.lookupNetwork(); - } - /** - * Refreshes the network meta with EIP-1559 support and the network status diff --git a/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch b/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch new file mode 100644 index 000000000000..63fff98c47e6 --- /dev/null +++ b/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch @@ -0,0 +1,33 @@ +diff --git a/PATCH.txt b/PATCH.txt +new file mode 100644 +index 0000000000000000000000000000000000000000..78b9156dc2b0bf7c33dadf325cb3ec0bfae71ccb +--- /dev/null ++++ b/PATCH.txt +@@ -0,0 +1,3 @@ ++We remove `lookupNetwork` from `initializeProvider` in the network controller to prevent network requests before user onboarding is completed. ++The network lookup is done after onboarding is completed, and when the extension reloads if onboarding has been completed. ++This patch is part of a temporary fix that will be reverted soon to make way for a more permanent solution. https://github.com/MetaMask/metamask-extension/pull/23005 +diff --git a/dist/chunk-BEL2VMHN.js b/dist/chunk-BEL2VMHN.js +index fcf6c5ad51d0db75cf0e3219a569e17437a55486..751447609c924e626c0f442931eb77687b160e42 100644 +--- a/dist/chunk-BEL2VMHN.js ++++ b/dist/chunk-BEL2VMHN.js +@@ -315,7 +315,6 @@ var NetworkController = class extends _basecontroller.BaseController { + */ + async initializeProvider() { + _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _applyNetworkSelection, applyNetworkSelection_fn).call(this, this.state.selectedNetworkClientId); +- await this.lookupNetwork(); + } + /** + * Refreshes the network meta with EIP-1559 support and the network status +diff --git a/dist/chunk-RTMQACMX.mjs b/dist/chunk-RTMQACMX.mjs +index fc6ae58a396aaa062e8d9a8de2cddd5ef073a5a4..2a6f811c10a0ed3fc943f4672b21a5d1c195c7cd 100644 +--- a/dist/chunk-RTMQACMX.mjs ++++ b/dist/chunk-RTMQACMX.mjs +@@ -315,7 +315,6 @@ var NetworkController = class extends BaseController { + */ + async initializeProvider() { + __privateMethod(this, _applyNetworkSelection, applyNetworkSelection_fn).call(this, this.state.selectedNetworkClientId); +- await this.lookupNetwork(); + } + /** + * Refreshes the network meta with EIP-1559 support and the network status diff --git a/.yarn/patches/@metamask-ppom-validator-npm-0.32.0-f677deea54.patch b/.yarn/patches/@metamask-ppom-validator-npm-0.32.0-f677deea54.patch new file mode 100644 index 000000000000..d996c0bdd022 --- /dev/null +++ b/.yarn/patches/@metamask-ppom-validator-npm-0.32.0-f677deea54.patch @@ -0,0 +1,15 @@ +diff --git a/dist/ppom-controller.js b/dist/ppom-controller.js +index 9cf1502efabec00b25ad381bf2001200ccc9f34f..bfe55b6e68989f794deab069e8b80fc8d719ec25 100644 +--- a/dist/ppom-controller.js ++++ b/dist/ppom-controller.js +@@ -203,7 +203,9 @@ async function _PPOMController_initialisePPOM() { + console.error(`Error in deleting files: ${error.message}`); + }); + }, _PPOMController_onNetworkChange = function _PPOMController_onNetworkChange(networkControllerState) { +- const id = (0, util_1.addHexPrefix)(networkControllerState.providerConfig.chainId); ++ const selectedNetworkClient = this.messagingSystem.call('NetworkController:getNetworkClientById', networkControllerState.selectedNetworkClientId); ++ const { chainId } = selectedNetworkClient.configuration; ++ const id = (0, util_1.addHexPrefix)(chainId); + if (id === __classPrivateFieldGet(this, _PPOMController_chainId, "f")) { + return; + } diff --git a/.yarn/patches/@metamask-selected-network-controller-npm-13.0.0-b0f6db473a.patch b/.yarn/patches/@metamask-selected-network-controller-npm-13.0.0-b0f6db473a.patch deleted file mode 100644 index 4fddeda9cc57..000000000000 --- a/.yarn/patches/@metamask-selected-network-controller-npm-13.0.0-b0f6db473a.patch +++ /dev/null @@ -1,1469 +0,0 @@ -diff --git a/dist/SelectedNetworkController.js b/dist/SelectedNetworkController.js -index ca967cd382ba810dadd7ffa914d5a8aceb6f156a..4b4103f6dd6c13e72956daa9557d623ec2c832b6 100644 ---- a/dist/SelectedNetworkController.js -+++ b/dist/SelectedNetworkController.js -@@ -4,12 +4,12 @@ - - - --var _chunkOGUVGN6Rjs = require('./chunk-OGUVGN6R.js'); -+var _chunkCECZWJ42js = require('./chunk-CECZWJ42.js'); - - - - - - --exports.METAMASK_DOMAIN = _chunkOGUVGN6Rjs.METAMASK_DOMAIN; exports.SelectedNetworkController = _chunkOGUVGN6Rjs.SelectedNetworkController; exports.SelectedNetworkControllerActionTypes = _chunkOGUVGN6Rjs.SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = _chunkOGUVGN6Rjs.SelectedNetworkControllerEventTypes; exports.controllerName = _chunkOGUVGN6Rjs.controllerName; -+exports.METAMASK_DOMAIN = _chunkCECZWJ42js.METAMASK_DOMAIN; exports.SelectedNetworkController = _chunkCECZWJ42js.SelectedNetworkController; exports.SelectedNetworkControllerActionTypes = _chunkCECZWJ42js.SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = _chunkCECZWJ42js.SelectedNetworkControllerEventTypes; exports.controllerName = _chunkCECZWJ42js.controllerName; - //# sourceMappingURL=SelectedNetworkController.js.map -\ No newline at end of file -diff --git a/dist/SelectedNetworkController.mjs b/dist/SelectedNetworkController.mjs -index 5228bbe7acb3da6abb826f00670a1034c51a0514..d4b1f0cf8d8e6123df56be0b766e285968f8587c 100644 ---- a/dist/SelectedNetworkController.mjs -+++ b/dist/SelectedNetworkController.mjs -@@ -4,7 +4,7 @@ import { - SelectedNetworkControllerActionTypes, - SelectedNetworkControllerEventTypes, - controllerName --} from "./chunk-S4D42VCM.mjs"; -+} from "./chunk-7DSTEJNI.mjs"; - export { - METAMASK_DOMAIN, - SelectedNetworkController, -diff --git a/dist/SelectedNetworkMiddleware.js b/dist/SelectedNetworkMiddleware.js -index 919f26ef57e8c6af5c63d1e0c8e85c43ec241e4a..0b9e434874a1ead6b596fa933597145ee40362d2 100644 ---- a/dist/SelectedNetworkMiddleware.js -+++ b/dist/SelectedNetworkMiddleware.js -@@ -1,8 +1,8 @@ - "use strict";Object.defineProperty(exports, "__esModule", {value: true}); - --var _chunk6W2ETVOHjs = require('./chunk-6W2ETVOH.js'); --require('./chunk-OGUVGN6R.js'); -+var _chunkANSSZMDIjs = require('./chunk-ANSSZMDI.js'); -+require('./chunk-CECZWJ42.js'); - - --exports.createSelectedNetworkMiddleware = _chunk6W2ETVOHjs.createSelectedNetworkMiddleware; -+exports.createSelectedNetworkMiddleware = _chunkANSSZMDIjs.createSelectedNetworkMiddleware; - //# sourceMappingURL=SelectedNetworkMiddleware.js.map -\ No newline at end of file -diff --git a/dist/SelectedNetworkMiddleware.mjs b/dist/SelectedNetworkMiddleware.mjs -index a9031b4aa2589009c2f23d03f1d23a01044c4178..c3302c19f429473963d663e87340b33d9b2caf98 100644 ---- a/dist/SelectedNetworkMiddleware.mjs -+++ b/dist/SelectedNetworkMiddleware.mjs -@@ -1,7 +1,7 @@ - import { - createSelectedNetworkMiddleware --} from "./chunk-ZY7ETPVE.mjs"; --import "./chunk-S4D42VCM.mjs"; -+} from "./chunk-HFN7TKJS.mjs"; -+import "./chunk-7DSTEJNI.mjs"; - export { - createSelectedNetworkMiddleware - }; -diff --git a/dist/chunk-6W2ETVOH.js b/dist/chunk-6W2ETVOH.js -deleted file mode 100644 -index 9714addd232767e296dd378a5cc0d57cafc9b882..0000000000000000000000000000000000000000 ---- a/dist/chunk-6W2ETVOH.js -+++ /dev/null -@@ -1,23 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true}); -- --var _chunkOGUVGN6Rjs = require('./chunk-OGUVGN6R.js'); -- --// src/SelectedNetworkMiddleware.ts --var createSelectedNetworkMiddleware = (messenger) => { -- const getNetworkClientIdForDomain = (origin) => messenger.call( -- _chunkOGUVGN6Rjs.SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -- origin -- ); -- return (req, _, next) => { -- if (!req.origin) { -- throw new Error("Request object is lacking an 'origin'"); -- } -- req.networkClientId = getNetworkClientIdForDomain(req.origin); -- return next(); -- }; --}; -- -- -- --exports.createSelectedNetworkMiddleware = createSelectedNetworkMiddleware; --//# sourceMappingURL=chunk-6W2ETVOH.js.map -\ No newline at end of file -diff --git a/dist/chunk-6W2ETVOH.js.map b/dist/chunk-6W2ETVOH.js.map -deleted file mode 100644 -index 9fe4c1fd6e9b12bfd9091833618334f2c931098f..0000000000000000000000000000000000000000 ---- a/dist/chunk-6W2ETVOH.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/SelectedNetworkMiddleware.ts"],"names":[],"mappings":";;;;;AAYO,IAAM,kCAAkC,CAC7C,cAC2C;AAC3C,QAAM,8BAA8B,CAAC,WACnC,UAAU;AAAA,IACR,qCAAqC;AAAA,IACrC;AAAA,EACF;AAEF,SAAO,CAAC,KAA8C,GAAG,SAAS;AAChE,QAAI,CAAC,IAAI,QAAQ;AACf,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAEA,QAAI,kBAAkB,4BAA4B,IAAI,MAAM;AAC5D,WAAO,KAAK;AAAA,EACd;AACF","sourcesContent":["import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';\nimport type { NetworkClientId } from '@metamask/network-controller';\nimport type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';\n\nimport type { SelectedNetworkControllerMessenger } from './SelectedNetworkController';\nimport { SelectedNetworkControllerActionTypes } from './SelectedNetworkController';\n\nexport type SelectedNetworkMiddlewareJsonRpcRequest = JsonRpcRequest & {\n networkClientId?: NetworkClientId;\n origin?: string;\n};\n\nexport const createSelectedNetworkMiddleware = (\n messenger: SelectedNetworkControllerMessenger,\n): JsonRpcMiddleware => {\n const getNetworkClientIdForDomain = (origin: string) =>\n messenger.call(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n origin,\n );\n\n return (req: SelectedNetworkMiddlewareJsonRpcRequest, _, next) => {\n if (!req.origin) {\n throw new Error(\"Request object is lacking an 'origin'\");\n }\n\n req.networkClientId = getNetworkClientIdForDomain(req.origin);\n return next();\n };\n};\n"]} -\ No newline at end of file -diff --git a/dist/chunk-7DSTEJNI.mjs b/dist/chunk-7DSTEJNI.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..1902ab28e3c37d563704840e31fdbc885db5354c ---- /dev/null -+++ b/dist/chunk-7DSTEJNI.mjs -@@ -0,0 +1,284 @@ -+var __accessCheck = (obj, member, msg) => { -+ if (!member.has(obj)) -+ throw TypeError("Cannot " + msg); -+}; -+var __privateGet = (obj, member, getter) => { -+ __accessCheck(obj, member, "read from private field"); -+ return getter ? getter.call(obj) : member.get(obj); -+}; -+var __privateAdd = (obj, member, value) => { -+ if (member.has(obj)) -+ throw TypeError("Cannot add the same private member more than once"); -+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -+}; -+var __privateSet = (obj, member, value, setter) => { -+ __accessCheck(obj, member, "write to private field"); -+ setter ? setter.call(obj, value) : member.set(obj, value); -+ return value; -+}; -+var __privateMethod = (obj, member, method) => { -+ __accessCheck(obj, member, "access private method"); -+ return method; -+}; -+ -+// src/SelectedNetworkController.ts -+import { BaseController } from "@metamask/base-controller"; -+import { createEventEmitterProxy } from "@metamask/swappable-obj-proxy"; -+var controllerName = "SelectedNetworkController"; -+var stateMetadata = { -+ domains: { persist: true, anonymous: false } -+}; -+var getDefaultState = () => ({ domains: {} }); -+var snapsPrefixes = ["npm:", "local:"]; -+var METAMASK_DOMAIN = "metamask"; -+var SelectedNetworkControllerActionTypes = { -+ getState: `${controllerName}:getState`, -+ getNetworkClientIdForDomain: `${controllerName}:getNetworkClientIdForDomain`, -+ setNetworkClientIdForDomain: `${controllerName}:setNetworkClientIdForDomain` -+}; -+var SelectedNetworkControllerEventTypes = { -+ stateChange: `${controllerName}:stateChange` -+}; -+var _domainProxyMap, _useRequestQueuePreference, _registerMessageHandlers, registerMessageHandlers_fn, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn, _domainHasPermissions, domainHasPermissions_fn, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn; -+var SelectedNetworkController = class extends BaseController { -+ /** -+ * Construct a SelectedNetworkController controller. -+ * -+ * @param options - The controller options. -+ * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. -+ * @param options.state - The controllers initial state. -+ * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference. -+ * @param options.onPreferencesStateChange - A callback that is called when the preference state changes. -+ * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use. -+ */ -+ constructor({ -+ messenger, -+ state = getDefaultState(), -+ useRequestQueuePreference, -+ onPreferencesStateChange, -+ domainProxyMap -+ }) { -+ super({ -+ name: controllerName, -+ metadata: stateMetadata, -+ messenger, -+ state -+ }); -+ __privateAdd(this, _registerMessageHandlers); -+ __privateAdd(this, _setNetworkClientIdForDomain); -+ /** -+ * This method is used when a domain is removed from the PermissionsController. -+ * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy. -+ * -+ * @param domain - The domain for which to unset the network client ID. -+ */ -+ __privateAdd(this, _unsetNetworkClientIdForDomain); -+ __privateAdd(this, _domainHasPermissions); -+ // Loop through all domains and for those with permissions it points that domain's proxy -+ // to an unproxied instance of the globally selected network client. -+ // NOT the NetworkController's proxy of the globally selected networkClient -+ __privateAdd(this, _resetAllPermissionedDomains); -+ __privateAdd(this, _domainProxyMap, void 0); -+ __privateAdd(this, _useRequestQueuePreference, void 0); -+ __privateSet(this, _useRequestQueuePreference, useRequestQueuePreference); -+ __privateSet(this, _domainProxyMap, domainProxyMap); -+ __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -+ this.messagingSystem.call("PermissionController:getSubjectNames").filter((domain) => this.state.domains[domain] === void 0).forEach( -+ (domain) => this.setNetworkClientIdForDomain( -+ domain, -+ this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -+ ) -+ ); -+ this.messagingSystem.subscribe( -+ "PermissionController:stateChange", -+ (_, patches) => { -+ patches.forEach(({ op, path }) => { -+ const isChangingSubject = path[0] === "subjects" && path[1] !== void 0; -+ if (isChangingSubject && typeof path[1] === "string") { -+ const domain = path[1]; -+ if (op === "add" && this.state.domains[domain] === void 0) { -+ this.setNetworkClientIdForDomain( -+ domain, -+ this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -+ ); -+ } else if (op === "remove" && this.state.domains[domain] !== void 0) { -+ __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -+ } -+ } -+ }); -+ } -+ ); -+ this.messagingSystem.subscribe( -+ "NetworkController:stateChange", -+ ({ selectedNetworkClientId }, patches) => { -+ patches.forEach(({ op, path }) => { -+ if (op === "remove" && path[0] === "networkConfigurations") { -+ const removedNetworkClientId = path[1]; -+ Object.entries(this.state.domains).forEach( -+ ([domain, networkClientIdForDomain]) => { -+ if (networkClientIdForDomain === removedNetworkClientId) { -+ this.setNetworkClientIdForDomain( -+ domain, -+ selectedNetworkClientId -+ ); -+ } -+ } -+ ); -+ } -+ }); -+ } -+ ); -+ onPreferencesStateChange(({ useRequestQueue }) => { -+ if (__privateGet(this, _useRequestQueuePreference) !== useRequestQueue) { -+ if (!useRequestQueue) { -+ Object.keys(this.state.domains).forEach((domain) => { -+ __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -+ }); -+ } else { -+ __privateMethod(this, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn).call(this); -+ } -+ __privateSet(this, _useRequestQueuePreference, useRequestQueue); -+ } -+ }); -+ } -+ setNetworkClientIdForDomain(domain, networkClientId) { -+ if (!__privateGet(this, _useRequestQueuePreference)) { -+ return; -+ } -+ if (domain === METAMASK_DOMAIN) { -+ throw new Error( -+ `NetworkClientId for domain "${METAMASK_DOMAIN}" cannot be set on the SelectedNetworkController` -+ ); -+ } -+ if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -+ return; -+ } -+ if (!__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ throw new Error( -+ "NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions" -+ ); -+ } -+ __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, networkClientId); -+ } -+ getNetworkClientIdForDomain(domain) { -+ const { selectedNetworkClientId: metamaskSelectedNetworkClientId } = this.messagingSystem.call("NetworkController:getState"); -+ if (!__privateGet(this, _useRequestQueuePreference)) { -+ return metamaskSelectedNetworkClientId; -+ } -+ return this.state.domains[domain] ?? metamaskSelectedNetworkClientId; -+ } -+ /** -+ * Accesses the provider and block tracker for the currently selected network. -+ * -+ * @param domain - the domain for the provider -+ * @returns The proxy and block tracker proxies. -+ */ -+ getProviderAndBlockTracker(domain) { -+ if (domain === METAMASK_DOMAIN || snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ if (networkClient === void 0) { -+ throw new Error("Selected network not initialized"); -+ } -+ return networkClient; -+ } -+ let networkProxy = __privateGet(this, _domainProxyMap).get(domain); -+ if (networkProxy === void 0) { -+ let networkClient; -+ if (__privateGet(this, _useRequestQueuePreference) && __privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ const networkClientId = this.getNetworkClientIdForDomain(domain); -+ networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ } else { -+ networkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ if (networkClient === void 0) { -+ throw new Error("Selected network not initialized"); -+ } -+ } -+ networkProxy = { -+ provider: createEventEmitterProxy(networkClient.provider), -+ blockTracker: createEventEmitterProxy(networkClient.blockTracker, { -+ eventFilter: "skipInternal" -+ }) -+ }; -+ __privateGet(this, _domainProxyMap).set(domain, networkProxy); -+ } -+ return networkProxy; -+ } -+}; -+_domainProxyMap = new WeakMap(); -+_useRequestQueuePreference = new WeakMap(); -+_registerMessageHandlers = new WeakSet(); -+registerMessageHandlers_fn = function() { -+ this.messagingSystem.registerActionHandler( -+ SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -+ this.getNetworkClientIdForDomain.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain, -+ this.setNetworkClientIdForDomain.bind(this) -+ ); -+}; -+_setNetworkClientIdForDomain = new WeakSet(); -+setNetworkClientIdForDomain_fn = function(domain, networkClientId) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ const networkProxy = this.getProviderAndBlockTracker(domain); -+ networkProxy.provider.setTarget(networkClient.provider); -+ networkProxy.blockTracker.setTarget(networkClient.blockTracker); -+ this.update((state) => { -+ state.domains[domain] = networkClientId; -+ }); -+}; -+_unsetNetworkClientIdForDomain = new WeakSet(); -+unsetNetworkClientIdForDomain_fn = function(domain) { -+ const globallySelectedNetworkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ const networkProxy = __privateGet(this, _domainProxyMap).get(domain); -+ if (networkProxy && globallySelectedNetworkClient) { -+ networkProxy.provider.setTarget(globallySelectedNetworkClient.provider); -+ networkProxy.blockTracker.setTarget( -+ globallySelectedNetworkClient.blockTracker -+ ); -+ } else if (networkProxy) { -+ __privateGet(this, _domainProxyMap).delete(domain); -+ } -+ this.update((state) => { -+ delete state.domains[domain]; -+ }); -+}; -+_domainHasPermissions = new WeakSet(); -+domainHasPermissions_fn = function(domain) { -+ return this.messagingSystem.call( -+ "PermissionController:hasPermissions", -+ domain -+ ); -+}; -+_resetAllPermissionedDomains = new WeakSet(); -+resetAllPermissionedDomains_fn = function() { -+ __privateGet(this, _domainProxyMap).forEach((_, domain) => { -+ const { selectedNetworkClientId } = this.messagingSystem.call( -+ "NetworkController:getState" -+ ); -+ if (__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, selectedNetworkClientId); -+ } -+ }); -+}; -+ -+export { -+ controllerName, -+ METAMASK_DOMAIN, -+ SelectedNetworkControllerActionTypes, -+ SelectedNetworkControllerEventTypes, -+ SelectedNetworkController -+}; -+//# sourceMappingURL=chunk-7DSTEJNI.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-7DSTEJNI.mjs.map b/dist/chunk-7DSTEJNI.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..33fde9b65d7c89592b3754333e6c1f62a0d90f63 ---- /dev/null -+++ b/dist/chunk-7DSTEJNI.mjs.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/SelectedNetworkController.ts"],"sourcesContent":["import type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n BlockTrackerProxy,\n NetworkClientId,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetSelectedNetworkClientAction,\n NetworkControllerGetStateAction,\n NetworkControllerStateChangeEvent,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport type {\n PermissionControllerStateChange,\n GetSubjects as PermissionControllerGetSubjectsAction,\n HasPermissions as PermissionControllerHasPermissions,\n} from '@metamask/permission-controller';\nimport { createEventEmitterProxy } from '@metamask/swappable-obj-proxy';\nimport type { Patch } from 'immer';\n\nexport const controllerName = 'SelectedNetworkController';\n\nconst stateMetadata = {\n domains: { persist: true, anonymous: false },\n};\n\nconst getDefaultState = () => ({ domains: {} });\n\n// npm and local are currently the only valid prefixes for snap domains\n// TODO: eventually we maybe want to pull this in from snaps-utils to ensure it stays in sync\n// For now it seems like overkill to add a dependency for this one constant\n// https://github.com/MetaMask/snaps/blob/2beee7803bfe9e540788a3558b546b9f55dc3cb4/packages/snaps-utils/src/types.ts#L120\nconst snapsPrefixes = ['npm:', 'local:'] as const;\n\nexport type Domain = string;\n\nexport const METAMASK_DOMAIN = 'metamask' as const;\n\nexport const SelectedNetworkControllerActionTypes = {\n getState: `${controllerName}:getState` as const,\n getNetworkClientIdForDomain:\n `${controllerName}:getNetworkClientIdForDomain` as const,\n setNetworkClientIdForDomain:\n `${controllerName}:setNetworkClientIdForDomain` as const,\n};\n\nexport const SelectedNetworkControllerEventTypes = {\n stateChange: `${controllerName}:stateChange` as const,\n};\n\nexport type SelectedNetworkControllerState = {\n domains: Record;\n};\n\nexport type SelectedNetworkControllerStateChangeEvent = {\n type: typeof SelectedNetworkControllerEventTypes.stateChange;\n payload: [SelectedNetworkControllerState, Patch[]];\n};\n\nexport type SelectedNetworkControllerGetSelectedNetworkStateAction = {\n type: typeof SelectedNetworkControllerActionTypes.getState;\n handler: () => SelectedNetworkControllerState;\n};\n\nexport type SelectedNetworkControllerGetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain;\n handler: SelectedNetworkController['getNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerSetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain;\n handler: SelectedNetworkController['setNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerActions =\n | SelectedNetworkControllerGetSelectedNetworkStateAction\n | SelectedNetworkControllerGetNetworkClientIdForDomainAction\n | SelectedNetworkControllerSetNetworkClientIdForDomainAction;\n\nexport type AllowedActions =\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetSelectedNetworkClientAction\n | NetworkControllerGetStateAction\n | PermissionControllerHasPermissions\n | PermissionControllerGetSubjectsAction;\n\nexport type SelectedNetworkControllerEvents =\n SelectedNetworkControllerStateChangeEvent;\n\nexport type AllowedEvents =\n | NetworkControllerStateChangeEvent\n | PermissionControllerStateChange;\n\nexport type SelectedNetworkControllerMessenger = RestrictedControllerMessenger<\n typeof controllerName,\n SelectedNetworkControllerActions | AllowedActions,\n SelectedNetworkControllerEvents | AllowedEvents,\n AllowedActions['type'],\n AllowedEvents['type']\n>;\n\nexport type SelectedNetworkControllerOptions = {\n state?: SelectedNetworkControllerState;\n messenger: SelectedNetworkControllerMessenger;\n useRequestQueuePreference: boolean;\n onPreferencesStateChange: (\n listener: (preferencesState: { useRequestQueue: boolean }) => void,\n ) => void;\n domainProxyMap: Map;\n};\n\nexport type NetworkProxy = {\n provider: ProviderProxy;\n blockTracker: BlockTrackerProxy;\n};\n\n/**\n * Controller for getting and setting the network for a particular domain.\n */\nexport class SelectedNetworkController extends BaseController<\n typeof controllerName,\n SelectedNetworkControllerState,\n SelectedNetworkControllerMessenger\n> {\n #domainProxyMap: Map;\n\n #useRequestQueuePreference: boolean;\n\n /**\n * Construct a SelectedNetworkController controller.\n *\n * @param options - The controller options.\n * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller.\n * @param options.state - The controllers initial state.\n * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference.\n * @param options.onPreferencesStateChange - A callback that is called when the preference state changes.\n * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use.\n */\n constructor({\n messenger,\n state = getDefaultState(),\n useRequestQueuePreference,\n onPreferencesStateChange,\n domainProxyMap,\n }: SelectedNetworkControllerOptions) {\n super({\n name: controllerName,\n metadata: stateMetadata,\n messenger,\n state,\n });\n this.#useRequestQueuePreference = useRequestQueuePreference;\n this.#domainProxyMap = domainProxyMap;\n this.#registerMessageHandlers();\n\n // this is fetching all the dapp permissions from the PermissionsController and looking for any domains that are not in domains state in this controller. Then we take any missing domains and add them to state here, setting it with the globally selected networkClientId (fetched from the NetworkController)\n this.messagingSystem\n .call('PermissionController:getSubjectNames')\n .filter((domain) => this.state.domains[domain] === undefined)\n .forEach((domain) =>\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n ),\n );\n\n this.messagingSystem.subscribe(\n 'PermissionController:stateChange',\n (_, patches) => {\n patches.forEach(({ op, path }) => {\n const isChangingSubject =\n path[0] === 'subjects' && path[1] !== undefined;\n if (isChangingSubject && typeof path[1] === 'string') {\n const domain = path[1];\n if (op === 'add' && this.state.domains[domain] === undefined) {\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n );\n } else if (\n op === 'remove' &&\n this.state.domains[domain] !== undefined\n ) {\n this.#unsetNetworkClientIdForDomain(domain);\n }\n }\n });\n },\n );\n\n this.messagingSystem.subscribe(\n 'NetworkController:stateChange',\n ({ selectedNetworkClientId }, patches) => {\n patches.forEach(({ op, path }) => {\n // if a network is removed, update the networkClientId for all domains that were using it to the selected network\n if (op === 'remove' && path[0] === 'networkConfigurations') {\n const removedNetworkClientId = path[1] as NetworkClientId;\n Object.entries(this.state.domains).forEach(\n ([domain, networkClientIdForDomain]) => {\n if (networkClientIdForDomain === removedNetworkClientId) {\n this.setNetworkClientIdForDomain(\n domain,\n selectedNetworkClientId,\n );\n }\n },\n );\n }\n });\n },\n );\n\n onPreferencesStateChange(({ useRequestQueue }) => {\n if (this.#useRequestQueuePreference !== useRequestQueue) {\n if (!useRequestQueue) {\n // Loop through all domains and points each domain's proxy\n // to the NetworkController's own proxy of the globally selected networkClient\n Object.keys(this.state.domains).forEach((domain) => {\n this.#unsetNetworkClientIdForDomain(domain);\n });\n } else {\n this.#resetAllPermissionedDomains();\n }\n this.#useRequestQueuePreference = useRequestQueue;\n }\n });\n }\n\n #registerMessageHandlers(): void {\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n this.getNetworkClientIdForDomain.bind(this),\n );\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain,\n this.setNetworkClientIdForDomain.bind(this),\n );\n }\n\n #setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n const networkProxy = this.getProviderAndBlockTracker(domain);\n networkProxy.provider.setTarget(networkClient.provider);\n networkProxy.blockTracker.setTarget(networkClient.blockTracker);\n\n this.update((state) => {\n state.domains[domain] = networkClientId;\n });\n }\n\n /**\n * This method is used when a domain is removed from the PermissionsController.\n * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy.\n *\n * @param domain - The domain for which to unset the network client ID.\n */\n #unsetNetworkClientIdForDomain(domain: Domain) {\n const globallySelectedNetworkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n const networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy && globallySelectedNetworkClient) {\n networkProxy.provider.setTarget(globallySelectedNetworkClient.provider);\n networkProxy.blockTracker.setTarget(\n globallySelectedNetworkClient.blockTracker,\n );\n } else if (networkProxy) {\n this.#domainProxyMap.delete(domain);\n }\n this.update((state) => {\n delete state.domains[domain];\n });\n }\n\n #domainHasPermissions(domain: Domain): boolean {\n return this.messagingSystem.call(\n 'PermissionController:hasPermissions',\n domain,\n );\n }\n\n // Loop through all domains and for those with permissions it points that domain's proxy\n // to an unproxied instance of the globally selected network client.\n // NOT the NetworkController's proxy of the globally selected networkClient\n #resetAllPermissionedDomains() {\n this.#domainProxyMap.forEach((_: NetworkProxy, domain: string) => {\n const { selectedNetworkClientId } = this.messagingSystem.call(\n 'NetworkController:getState',\n );\n // can't use public setNetworkClientIdForDomain because it will throw an error\n // rather than simply skip if the domain doesn't have permissions which can happen\n // in this case since proxies are added for each site the user visits\n if (this.#domainHasPermissions(domain)) {\n this.#setNetworkClientIdForDomain(domain, selectedNetworkClientId);\n }\n });\n }\n\n setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n // Core PR: https://github.com/MetaMask/core/pull/4388\n // Patch Branch: patch-selected-network-controller-13.0.0-setNetworkClient-guard\n if (!this.#useRequestQueuePreference) {\n return;\n }\n if (domain === METAMASK_DOMAIN) {\n throw new Error(\n `NetworkClientId for domain \"${METAMASK_DOMAIN}\" cannot be set on the SelectedNetworkController`,\n );\n }\n\n if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) {\n return;\n }\n\n if (!this.#domainHasPermissions(domain)) {\n throw new Error(\n 'NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions',\n );\n }\n\n this.#setNetworkClientIdForDomain(domain, networkClientId);\n }\n\n getNetworkClientIdForDomain(domain: Domain): NetworkClientId {\n const { selectedNetworkClientId: metamaskSelectedNetworkClientId } =\n this.messagingSystem.call('NetworkController:getState');\n if (!this.#useRequestQueuePreference) {\n return metamaskSelectedNetworkClientId;\n }\n return this.state.domains[domain] ?? metamaskSelectedNetworkClientId;\n }\n\n /**\n * Accesses the provider and block tracker for the currently selected network.\n *\n * @param domain - the domain for the provider\n * @returns The proxy and block tracker proxies.\n */\n getProviderAndBlockTracker(domain: Domain): NetworkProxy {\n // If the domain is 'metamask' or a snap, return the NetworkController's globally selected network client proxy\n if (\n domain === METAMASK_DOMAIN ||\n snapsPrefixes.some((prefix) => domain.startsWith(prefix))\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n return networkClient;\n }\n\n let networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy === undefined) {\n let networkClient;\n if (\n this.#useRequestQueuePreference &&\n this.#domainHasPermissions(domain)\n ) {\n const networkClientId = this.getNetworkClientIdForDomain(domain);\n networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n } else {\n networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n }\n networkProxy = {\n provider: createEventEmitterProxy(networkClient.provider),\n blockTracker: createEventEmitterProxy(networkClient.blockTracker, {\n eventFilter: 'skipInternal',\n }),\n };\n this.#domainProxyMap.set(domain, networkProxy);\n }\n return networkProxy;\n }\n}\n"],"mappings":"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","names":[]} -\ No newline at end of file -diff --git a/dist/chunk-ANSSZMDI.js b/dist/chunk-ANSSZMDI.js -new file mode 100644 -index 0000000000000000000000000000000000000000..46fa6599a0d673a526cc2d054a47733fc0570e64 ---- /dev/null -+++ b/dist/chunk-ANSSZMDI.js -@@ -0,0 +1,23 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); -+ -+var _chunkCECZWJ42js = require('./chunk-CECZWJ42.js'); -+ -+// src/SelectedNetworkMiddleware.ts -+var createSelectedNetworkMiddleware = (messenger) => { -+ const getNetworkClientIdForDomain = (origin) => messenger.call( -+ _chunkCECZWJ42js.SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -+ origin -+ ); -+ return (req, _, next) => { -+ if (!req.origin) { -+ throw new Error("Request object is lacking an 'origin'"); -+ } -+ req.networkClientId = getNetworkClientIdForDomain(req.origin); -+ return next(); -+ }; -+}; -+ -+ -+ -+exports.createSelectedNetworkMiddleware = createSelectedNetworkMiddleware; -+//# sourceMappingURL=chunk-ANSSZMDI.js.map -\ No newline at end of file -diff --git a/dist/chunk-ANSSZMDI.js.map b/dist/chunk-ANSSZMDI.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..9fe4c1fd6e9b12bfd9091833618334f2c931098f ---- /dev/null -+++ b/dist/chunk-ANSSZMDI.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/SelectedNetworkMiddleware.ts"],"names":[],"mappings":";;;;;AAYO,IAAM,kCAAkC,CAC7C,cAC2C;AAC3C,QAAM,8BAA8B,CAAC,WACnC,UAAU;AAAA,IACR,qCAAqC;AAAA,IACrC;AAAA,EACF;AAEF,SAAO,CAAC,KAA8C,GAAG,SAAS;AAChE,QAAI,CAAC,IAAI,QAAQ;AACf,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAEA,QAAI,kBAAkB,4BAA4B,IAAI,MAAM;AAC5D,WAAO,KAAK;AAAA,EACd;AACF","sourcesContent":["import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';\nimport type { NetworkClientId } from '@metamask/network-controller';\nimport type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';\n\nimport type { SelectedNetworkControllerMessenger } from './SelectedNetworkController';\nimport { SelectedNetworkControllerActionTypes } from './SelectedNetworkController';\n\nexport type SelectedNetworkMiddlewareJsonRpcRequest = JsonRpcRequest & {\n networkClientId?: NetworkClientId;\n origin?: string;\n};\n\nexport const createSelectedNetworkMiddleware = (\n messenger: SelectedNetworkControllerMessenger,\n): JsonRpcMiddleware => {\n const getNetworkClientIdForDomain = (origin: string) =>\n messenger.call(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n origin,\n );\n\n return (req: SelectedNetworkMiddlewareJsonRpcRequest, _, next) => {\n if (!req.origin) {\n throw new Error(\"Request object is lacking an 'origin'\");\n }\n\n req.networkClientId = getNetworkClientIdForDomain(req.origin);\n return next();\n };\n};\n"]} -\ No newline at end of file -diff --git a/dist/chunk-CECZWJ42.js b/dist/chunk-CECZWJ42.js -new file mode 100644 -index 0000000000000000000000000000000000000000..3471ce2b3b927414a25b81b864ddad0cdaeeb26a ---- /dev/null -+++ b/dist/chunk-CECZWJ42.js -@@ -0,0 +1,284 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true});var __accessCheck = (obj, member, msg) => { -+ if (!member.has(obj)) -+ throw TypeError("Cannot " + msg); -+}; -+var __privateGet = (obj, member, getter) => { -+ __accessCheck(obj, member, "read from private field"); -+ return getter ? getter.call(obj) : member.get(obj); -+}; -+var __privateAdd = (obj, member, value) => { -+ if (member.has(obj)) -+ throw TypeError("Cannot add the same private member more than once"); -+ member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -+}; -+var __privateSet = (obj, member, value, setter) => { -+ __accessCheck(obj, member, "write to private field"); -+ setter ? setter.call(obj, value) : member.set(obj, value); -+ return value; -+}; -+var __privateMethod = (obj, member, method) => { -+ __accessCheck(obj, member, "access private method"); -+ return method; -+}; -+ -+// src/SelectedNetworkController.ts -+var _basecontroller = require('@metamask/base-controller'); -+var _swappableobjproxy = require('@metamask/swappable-obj-proxy'); -+var controllerName = "SelectedNetworkController"; -+var stateMetadata = { -+ domains: { persist: true, anonymous: false } -+}; -+var getDefaultState = () => ({ domains: {} }); -+var snapsPrefixes = ["npm:", "local:"]; -+var METAMASK_DOMAIN = "metamask"; -+var SelectedNetworkControllerActionTypes = { -+ getState: `${controllerName}:getState`, -+ getNetworkClientIdForDomain: `${controllerName}:getNetworkClientIdForDomain`, -+ setNetworkClientIdForDomain: `${controllerName}:setNetworkClientIdForDomain` -+}; -+var SelectedNetworkControllerEventTypes = { -+ stateChange: `${controllerName}:stateChange` -+}; -+var _domainProxyMap, _useRequestQueuePreference, _registerMessageHandlers, registerMessageHandlers_fn, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn, _domainHasPermissions, domainHasPermissions_fn, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn; -+var SelectedNetworkController = class extends _basecontroller.BaseController { -+ /** -+ * Construct a SelectedNetworkController controller. -+ * -+ * @param options - The controller options. -+ * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. -+ * @param options.state - The controllers initial state. -+ * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference. -+ * @param options.onPreferencesStateChange - A callback that is called when the preference state changes. -+ * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use. -+ */ -+ constructor({ -+ messenger, -+ state = getDefaultState(), -+ useRequestQueuePreference, -+ onPreferencesStateChange, -+ domainProxyMap -+ }) { -+ super({ -+ name: controllerName, -+ metadata: stateMetadata, -+ messenger, -+ state -+ }); -+ __privateAdd(this, _registerMessageHandlers); -+ __privateAdd(this, _setNetworkClientIdForDomain); -+ /** -+ * This method is used when a domain is removed from the PermissionsController. -+ * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy. -+ * -+ * @param domain - The domain for which to unset the network client ID. -+ */ -+ __privateAdd(this, _unsetNetworkClientIdForDomain); -+ __privateAdd(this, _domainHasPermissions); -+ // Loop through all domains and for those with permissions it points that domain's proxy -+ // to an unproxied instance of the globally selected network client. -+ // NOT the NetworkController's proxy of the globally selected networkClient -+ __privateAdd(this, _resetAllPermissionedDomains); -+ __privateAdd(this, _domainProxyMap, void 0); -+ __privateAdd(this, _useRequestQueuePreference, void 0); -+ __privateSet(this, _useRequestQueuePreference, useRequestQueuePreference); -+ __privateSet(this, _domainProxyMap, domainProxyMap); -+ __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -+ this.messagingSystem.call("PermissionController:getSubjectNames").filter((domain) => this.state.domains[domain] === void 0).forEach( -+ (domain) => this.setNetworkClientIdForDomain( -+ domain, -+ this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -+ ) -+ ); -+ this.messagingSystem.subscribe( -+ "PermissionController:stateChange", -+ (_, patches) => { -+ patches.forEach(({ op, path }) => { -+ const isChangingSubject = path[0] === "subjects" && path[1] !== void 0; -+ if (isChangingSubject && typeof path[1] === "string") { -+ const domain = path[1]; -+ if (op === "add" && this.state.domains[domain] === void 0) { -+ this.setNetworkClientIdForDomain( -+ domain, -+ this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -+ ); -+ } else if (op === "remove" && this.state.domains[domain] !== void 0) { -+ __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -+ } -+ } -+ }); -+ } -+ ); -+ this.messagingSystem.subscribe( -+ "NetworkController:stateChange", -+ ({ selectedNetworkClientId }, patches) => { -+ patches.forEach(({ op, path }) => { -+ if (op === "remove" && path[0] === "networkConfigurations") { -+ const removedNetworkClientId = path[1]; -+ Object.entries(this.state.domains).forEach( -+ ([domain, networkClientIdForDomain]) => { -+ if (networkClientIdForDomain === removedNetworkClientId) { -+ this.setNetworkClientIdForDomain( -+ domain, -+ selectedNetworkClientId -+ ); -+ } -+ } -+ ); -+ } -+ }); -+ } -+ ); -+ onPreferencesStateChange(({ useRequestQueue }) => { -+ if (__privateGet(this, _useRequestQueuePreference) !== useRequestQueue) { -+ if (!useRequestQueue) { -+ Object.keys(this.state.domains).forEach((domain) => { -+ __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -+ }); -+ } else { -+ __privateMethod(this, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn).call(this); -+ } -+ __privateSet(this, _useRequestQueuePreference, useRequestQueue); -+ } -+ }); -+ } -+ setNetworkClientIdForDomain(domain, networkClientId) { -+ if (!__privateGet(this, _useRequestQueuePreference)) { -+ return; -+ } -+ if (domain === METAMASK_DOMAIN) { -+ throw new Error( -+ `NetworkClientId for domain "${METAMASK_DOMAIN}" cannot be set on the SelectedNetworkController` -+ ); -+ } -+ if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -+ return; -+ } -+ if (!__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ throw new Error( -+ "NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions" -+ ); -+ } -+ __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, networkClientId); -+ } -+ getNetworkClientIdForDomain(domain) { -+ const { selectedNetworkClientId: metamaskSelectedNetworkClientId } = this.messagingSystem.call("NetworkController:getState"); -+ if (!__privateGet(this, _useRequestQueuePreference)) { -+ return metamaskSelectedNetworkClientId; -+ } -+ return this.state.domains[domain] ?? metamaskSelectedNetworkClientId; -+ } -+ /** -+ * Accesses the provider and block tracker for the currently selected network. -+ * -+ * @param domain - the domain for the provider -+ * @returns The proxy and block tracker proxies. -+ */ -+ getProviderAndBlockTracker(domain) { -+ if (domain === METAMASK_DOMAIN || snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ if (networkClient === void 0) { -+ throw new Error("Selected network not initialized"); -+ } -+ return networkClient; -+ } -+ let networkProxy = __privateGet(this, _domainProxyMap).get(domain); -+ if (networkProxy === void 0) { -+ let networkClient; -+ if (__privateGet(this, _useRequestQueuePreference) && __privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ const networkClientId = this.getNetworkClientIdForDomain(domain); -+ networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ } else { -+ networkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ if (networkClient === void 0) { -+ throw new Error("Selected network not initialized"); -+ } -+ } -+ networkProxy = { -+ provider: _swappableobjproxy.createEventEmitterProxy.call(void 0, networkClient.provider), -+ blockTracker: _swappableobjproxy.createEventEmitterProxy.call(void 0, networkClient.blockTracker, { -+ eventFilter: "skipInternal" -+ }) -+ }; -+ __privateGet(this, _domainProxyMap).set(domain, networkProxy); -+ } -+ return networkProxy; -+ } -+}; -+_domainProxyMap = new WeakMap(); -+_useRequestQueuePreference = new WeakMap(); -+_registerMessageHandlers = new WeakSet(); -+registerMessageHandlers_fn = function() { -+ this.messagingSystem.registerActionHandler( -+ SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -+ this.getNetworkClientIdForDomain.bind(this) -+ ); -+ this.messagingSystem.registerActionHandler( -+ SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain, -+ this.setNetworkClientIdForDomain.bind(this) -+ ); -+}; -+_setNetworkClientIdForDomain = new WeakSet(); -+setNetworkClientIdForDomain_fn = function(domain, networkClientId) { -+ const networkClient = this.messagingSystem.call( -+ "NetworkController:getNetworkClientById", -+ networkClientId -+ ); -+ const networkProxy = this.getProviderAndBlockTracker(domain); -+ networkProxy.provider.setTarget(networkClient.provider); -+ networkProxy.blockTracker.setTarget(networkClient.blockTracker); -+ this.update((state) => { -+ state.domains[domain] = networkClientId; -+ }); -+}; -+_unsetNetworkClientIdForDomain = new WeakSet(); -+unsetNetworkClientIdForDomain_fn = function(domain) { -+ const globallySelectedNetworkClient = this.messagingSystem.call( -+ "NetworkController:getSelectedNetworkClient" -+ ); -+ const networkProxy = __privateGet(this, _domainProxyMap).get(domain); -+ if (networkProxy && globallySelectedNetworkClient) { -+ networkProxy.provider.setTarget(globallySelectedNetworkClient.provider); -+ networkProxy.blockTracker.setTarget( -+ globallySelectedNetworkClient.blockTracker -+ ); -+ } else if (networkProxy) { -+ __privateGet(this, _domainProxyMap).delete(domain); -+ } -+ this.update((state) => { -+ delete state.domains[domain]; -+ }); -+}; -+_domainHasPermissions = new WeakSet(); -+domainHasPermissions_fn = function(domain) { -+ return this.messagingSystem.call( -+ "PermissionController:hasPermissions", -+ domain -+ ); -+}; -+_resetAllPermissionedDomains = new WeakSet(); -+resetAllPermissionedDomains_fn = function() { -+ __privateGet(this, _domainProxyMap).forEach((_, domain) => { -+ const { selectedNetworkClientId } = this.messagingSystem.call( -+ "NetworkController:getState" -+ ); -+ if (__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -+ __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, selectedNetworkClientId); -+ } -+ }); -+}; -+ -+ -+ -+ -+ -+ -+ -+exports.controllerName = controllerName; exports.METAMASK_DOMAIN = METAMASK_DOMAIN; exports.SelectedNetworkControllerActionTypes = SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = SelectedNetworkControllerEventTypes; exports.SelectedNetworkController = SelectedNetworkController; -+//# sourceMappingURL=chunk-CECZWJ42.js.map -\ No newline at end of file -diff --git a/dist/chunk-CECZWJ42.js.map b/dist/chunk-CECZWJ42.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..a2eab942638757819a9cb850057450500d22f9ed ---- /dev/null -+++ b/dist/chunk-CECZWJ42.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/SelectedNetworkController.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n BlockTrackerProxy,\n NetworkClientId,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetSelectedNetworkClientAction,\n NetworkControllerGetStateAction,\n NetworkControllerStateChangeEvent,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport type {\n PermissionControllerStateChange,\n GetSubjects as PermissionControllerGetSubjectsAction,\n HasPermissions as PermissionControllerHasPermissions,\n} from '@metamask/permission-controller';\nimport { createEventEmitterProxy } from '@metamask/swappable-obj-proxy';\nimport type { Patch } from 'immer';\n\nexport const controllerName = 'SelectedNetworkController';\n\nconst stateMetadata = {\n domains: { persist: true, anonymous: false },\n};\n\nconst getDefaultState = () => ({ domains: {} });\n\n// npm and local are currently the only valid prefixes for snap domains\n// TODO: eventually we maybe want to pull this in from snaps-utils to ensure it stays in sync\n// For now it seems like overkill to add a dependency for this one constant\n// https://github.com/MetaMask/snaps/blob/2beee7803bfe9e540788a3558b546b9f55dc3cb4/packages/snaps-utils/src/types.ts#L120\nconst snapsPrefixes = ['npm:', 'local:'] as const;\n\nexport type Domain = string;\n\nexport const METAMASK_DOMAIN = 'metamask' as const;\n\nexport const SelectedNetworkControllerActionTypes = {\n getState: `${controllerName}:getState` as const,\n getNetworkClientIdForDomain:\n `${controllerName}:getNetworkClientIdForDomain` as const,\n setNetworkClientIdForDomain:\n `${controllerName}:setNetworkClientIdForDomain` as const,\n};\n\nexport const SelectedNetworkControllerEventTypes = {\n stateChange: `${controllerName}:stateChange` as const,\n};\n\nexport type SelectedNetworkControllerState = {\n domains: Record;\n};\n\nexport type SelectedNetworkControllerStateChangeEvent = {\n type: typeof SelectedNetworkControllerEventTypes.stateChange;\n payload: [SelectedNetworkControllerState, Patch[]];\n};\n\nexport type SelectedNetworkControllerGetSelectedNetworkStateAction = {\n type: typeof SelectedNetworkControllerActionTypes.getState;\n handler: () => SelectedNetworkControllerState;\n};\n\nexport type SelectedNetworkControllerGetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain;\n handler: SelectedNetworkController['getNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerSetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain;\n handler: SelectedNetworkController['setNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerActions =\n | SelectedNetworkControllerGetSelectedNetworkStateAction\n | SelectedNetworkControllerGetNetworkClientIdForDomainAction\n | SelectedNetworkControllerSetNetworkClientIdForDomainAction;\n\nexport type AllowedActions =\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetSelectedNetworkClientAction\n | NetworkControllerGetStateAction\n | PermissionControllerHasPermissions\n | PermissionControllerGetSubjectsAction;\n\nexport type SelectedNetworkControllerEvents =\n SelectedNetworkControllerStateChangeEvent;\n\nexport type AllowedEvents =\n | NetworkControllerStateChangeEvent\n | PermissionControllerStateChange;\n\nexport type SelectedNetworkControllerMessenger = RestrictedControllerMessenger<\n typeof controllerName,\n SelectedNetworkControllerActions | AllowedActions,\n SelectedNetworkControllerEvents | AllowedEvents,\n AllowedActions['type'],\n AllowedEvents['type']\n>;\n\nexport type SelectedNetworkControllerOptions = {\n state?: SelectedNetworkControllerState;\n messenger: SelectedNetworkControllerMessenger;\n useRequestQueuePreference: boolean;\n onPreferencesStateChange: (\n listener: (preferencesState: { useRequestQueue: boolean }) => void,\n ) => void;\n domainProxyMap: Map;\n};\n\nexport type NetworkProxy = {\n provider: ProviderProxy;\n blockTracker: BlockTrackerProxy;\n};\n\n/**\n * Controller for getting and setting the network for a particular domain.\n */\nexport class SelectedNetworkController extends BaseController<\n typeof controllerName,\n SelectedNetworkControllerState,\n SelectedNetworkControllerMessenger\n> {\n #domainProxyMap: Map;\n\n #useRequestQueuePreference: boolean;\n\n /**\n * Construct a SelectedNetworkController controller.\n *\n * @param options - The controller options.\n * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller.\n * @param options.state - The controllers initial state.\n * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference.\n * @param options.onPreferencesStateChange - A callback that is called when the preference state changes.\n * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use.\n */\n constructor({\n messenger,\n state = getDefaultState(),\n useRequestQueuePreference,\n onPreferencesStateChange,\n domainProxyMap,\n }: SelectedNetworkControllerOptions) {\n super({\n name: controllerName,\n metadata: stateMetadata,\n messenger,\n state,\n });\n this.#useRequestQueuePreference = useRequestQueuePreference;\n this.#domainProxyMap = domainProxyMap;\n this.#registerMessageHandlers();\n\n // this is fetching all the dapp permissions from the PermissionsController and looking for any domains that are not in domains state in this controller. Then we take any missing domains and add them to state here, setting it with the globally selected networkClientId (fetched from the NetworkController)\n this.messagingSystem\n .call('PermissionController:getSubjectNames')\n .filter((domain) => this.state.domains[domain] === undefined)\n .forEach((domain) =>\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n ),\n );\n\n this.messagingSystem.subscribe(\n 'PermissionController:stateChange',\n (_, patches) => {\n patches.forEach(({ op, path }) => {\n const isChangingSubject =\n path[0] === 'subjects' && path[1] !== undefined;\n if (isChangingSubject && typeof path[1] === 'string') {\n const domain = path[1];\n if (op === 'add' && this.state.domains[domain] === undefined) {\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n );\n } else if (\n op === 'remove' &&\n this.state.domains[domain] !== undefined\n ) {\n this.#unsetNetworkClientIdForDomain(domain);\n }\n }\n });\n },\n );\n\n this.messagingSystem.subscribe(\n 'NetworkController:stateChange',\n ({ selectedNetworkClientId }, patches) => {\n patches.forEach(({ op, path }) => {\n // if a network is removed, update the networkClientId for all domains that were using it to the selected network\n if (op === 'remove' && path[0] === 'networkConfigurations') {\n const removedNetworkClientId = path[1] as NetworkClientId;\n Object.entries(this.state.domains).forEach(\n ([domain, networkClientIdForDomain]) => {\n if (networkClientIdForDomain === removedNetworkClientId) {\n this.setNetworkClientIdForDomain(\n domain,\n selectedNetworkClientId,\n );\n }\n },\n );\n }\n });\n },\n );\n\n onPreferencesStateChange(({ useRequestQueue }) => {\n if (this.#useRequestQueuePreference !== useRequestQueue) {\n if (!useRequestQueue) {\n // Loop through all domains and points each domain's proxy\n // to the NetworkController's own proxy of the globally selected networkClient\n Object.keys(this.state.domains).forEach((domain) => {\n this.#unsetNetworkClientIdForDomain(domain);\n });\n } else {\n this.#resetAllPermissionedDomains();\n }\n this.#useRequestQueuePreference = useRequestQueue;\n }\n });\n }\n\n #registerMessageHandlers(): void {\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n this.getNetworkClientIdForDomain.bind(this),\n );\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain,\n this.setNetworkClientIdForDomain.bind(this),\n );\n }\n\n #setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n const networkProxy = this.getProviderAndBlockTracker(domain);\n networkProxy.provider.setTarget(networkClient.provider);\n networkProxy.blockTracker.setTarget(networkClient.blockTracker);\n\n this.update((state) => {\n state.domains[domain] = networkClientId;\n });\n }\n\n /**\n * This method is used when a domain is removed from the PermissionsController.\n * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy.\n *\n * @param domain - The domain for which to unset the network client ID.\n */\n #unsetNetworkClientIdForDomain(domain: Domain) {\n const globallySelectedNetworkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n const networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy && globallySelectedNetworkClient) {\n networkProxy.provider.setTarget(globallySelectedNetworkClient.provider);\n networkProxy.blockTracker.setTarget(\n globallySelectedNetworkClient.blockTracker,\n );\n } else if (networkProxy) {\n this.#domainProxyMap.delete(domain);\n }\n this.update((state) => {\n delete state.domains[domain];\n });\n }\n\n #domainHasPermissions(domain: Domain): boolean {\n return this.messagingSystem.call(\n 'PermissionController:hasPermissions',\n domain,\n );\n }\n\n // Loop through all domains and for those with permissions it points that domain's proxy\n // to an unproxied instance of the globally selected network client.\n // NOT the NetworkController's proxy of the globally selected networkClient\n #resetAllPermissionedDomains() {\n this.#domainProxyMap.forEach((_: NetworkProxy, domain: string) => {\n const { selectedNetworkClientId } = this.messagingSystem.call(\n 'NetworkController:getState',\n );\n // can't use public setNetworkClientIdForDomain because it will throw an error\n // rather than simply skip if the domain doesn't have permissions which can happen\n // in this case since proxies are added for each site the user visits\n if (this.#domainHasPermissions(domain)) {\n this.#setNetworkClientIdForDomain(domain, selectedNetworkClientId);\n }\n });\n }\n\n setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n // Core PR: https://github.com/MetaMask/core/pull/4388\n // Patch Branch: patch-selected-network-controller-13.0.0-setNetworkClient-guard\n if (!this.#useRequestQueuePreference) {\n return;\n }\n if (domain === METAMASK_DOMAIN) {\n throw new Error(\n `NetworkClientId for domain \"${METAMASK_DOMAIN}\" cannot be set on the SelectedNetworkController`,\n );\n }\n\n if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) {\n return;\n }\n\n if (!this.#domainHasPermissions(domain)) {\n throw new Error(\n 'NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions',\n );\n }\n\n this.#setNetworkClientIdForDomain(domain, networkClientId);\n }\n\n getNetworkClientIdForDomain(domain: Domain): NetworkClientId {\n const { selectedNetworkClientId: metamaskSelectedNetworkClientId } =\n this.messagingSystem.call('NetworkController:getState');\n if (!this.#useRequestQueuePreference) {\n return metamaskSelectedNetworkClientId;\n }\n return this.state.domains[domain] ?? metamaskSelectedNetworkClientId;\n }\n\n /**\n * Accesses the provider and block tracker for the currently selected network.\n *\n * @param domain - the domain for the provider\n * @returns The proxy and block tracker proxies.\n */\n getProviderAndBlockTracker(domain: Domain): NetworkProxy {\n // If the domain is 'metamask' or a snap, return the NetworkController's globally selected network client proxy\n if (\n domain === METAMASK_DOMAIN ||\n snapsPrefixes.some((prefix) => domain.startsWith(prefix))\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n return networkClient;\n }\n\n let networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy === undefined) {\n let networkClient;\n if (\n this.#useRequestQueuePreference &&\n this.#domainHasPermissions(domain)\n ) {\n const networkClientId = this.getNetworkClientIdForDomain(domain);\n networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n } else {\n networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n }\n networkProxy = {\n provider: createEventEmitterProxy(networkClient.provider),\n blockTracker: createEventEmitterProxy(networkClient.blockTracker, {\n eventFilter: 'skipInternal',\n }),\n };\n this.#domainProxyMap.set(domain, networkProxy);\n }\n return networkProxy;\n }\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-HFN7TKJS.mjs b/dist/chunk-HFN7TKJS.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..6f4a4ec6b18571939d2b6c10ddb8ab4e5345389c ---- /dev/null -+++ b/dist/chunk-HFN7TKJS.mjs -@@ -0,0 +1,23 @@ -+import { -+ SelectedNetworkControllerActionTypes -+} from "./chunk-7DSTEJNI.mjs"; -+ -+// src/SelectedNetworkMiddleware.ts -+var createSelectedNetworkMiddleware = (messenger) => { -+ const getNetworkClientIdForDomain = (origin) => messenger.call( -+ SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -+ origin -+ ); -+ return (req, _, next) => { -+ if (!req.origin) { -+ throw new Error("Request object is lacking an 'origin'"); -+ } -+ req.networkClientId = getNetworkClientIdForDomain(req.origin); -+ return next(); -+ }; -+}; -+ -+export { -+ createSelectedNetworkMiddleware -+}; -+//# sourceMappingURL=chunk-HFN7TKJS.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-HFN7TKJS.mjs.map b/dist/chunk-HFN7TKJS.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..fbcfd73d57f291b4f129caa47ef0c9614987cc30 ---- /dev/null -+++ b/dist/chunk-HFN7TKJS.mjs.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/SelectedNetworkMiddleware.ts"],"sourcesContent":["import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';\nimport type { NetworkClientId } from '@metamask/network-controller';\nimport type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';\n\nimport type { SelectedNetworkControllerMessenger } from './SelectedNetworkController';\nimport { SelectedNetworkControllerActionTypes } from './SelectedNetworkController';\n\nexport type SelectedNetworkMiddlewareJsonRpcRequest = JsonRpcRequest & {\n networkClientId?: NetworkClientId;\n origin?: string;\n};\n\nexport const createSelectedNetworkMiddleware = (\n messenger: SelectedNetworkControllerMessenger,\n): JsonRpcMiddleware => {\n const getNetworkClientIdForDomain = (origin: string) =>\n messenger.call(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n origin,\n );\n\n return (req: SelectedNetworkMiddlewareJsonRpcRequest, _, next) => {\n if (!req.origin) {\n throw new Error(\"Request object is lacking an 'origin'\");\n }\n\n req.networkClientId = getNetworkClientIdForDomain(req.origin);\n return next();\n };\n};\n"],"mappings":";;;;;AAYO,IAAM,kCAAkC,CAC7C,cAC2C;AAC3C,QAAM,8BAA8B,CAAC,WACnC,UAAU;AAAA,IACR,qCAAqC;AAAA,IACrC;AAAA,EACF;AAEF,SAAO,CAAC,KAA8C,GAAG,SAAS;AAChE,QAAI,CAAC,IAAI,QAAQ;AACf,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAEA,QAAI,kBAAkB,4BAA4B,IAAI,MAAM;AAC5D,WAAO,KAAK;AAAA,EACd;AACF;","names":[]} -\ No newline at end of file -diff --git a/dist/chunk-OGUVGN6R.js b/dist/chunk-OGUVGN6R.js -deleted file mode 100644 -index 1fb2670b25d9230aa3b0d1d496223ccee8f48263..0000000000000000000000000000000000000000 ---- a/dist/chunk-OGUVGN6R.js -+++ /dev/null -@@ -1,281 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true});var __accessCheck = (obj, member, msg) => { -- if (!member.has(obj)) -- throw TypeError("Cannot " + msg); --}; --var __privateGet = (obj, member, getter) => { -- __accessCheck(obj, member, "read from private field"); -- return getter ? getter.call(obj) : member.get(obj); --}; --var __privateAdd = (obj, member, value) => { -- if (member.has(obj)) -- throw TypeError("Cannot add the same private member more than once"); -- member instanceof WeakSet ? member.add(obj) : member.set(obj, value); --}; --var __privateSet = (obj, member, value, setter) => { -- __accessCheck(obj, member, "write to private field"); -- setter ? setter.call(obj, value) : member.set(obj, value); -- return value; --}; --var __privateMethod = (obj, member, method) => { -- __accessCheck(obj, member, "access private method"); -- return method; --}; -- --// src/SelectedNetworkController.ts --var _basecontroller = require('@metamask/base-controller'); --var _swappableobjproxy = require('@metamask/swappable-obj-proxy'); --var controllerName = "SelectedNetworkController"; --var stateMetadata = { -- domains: { persist: true, anonymous: false } --}; --var getDefaultState = () => ({ domains: {} }); --var snapsPrefixes = ["npm:", "local:"]; --var METAMASK_DOMAIN = "metamask"; --var SelectedNetworkControllerActionTypes = { -- getState: `${controllerName}:getState`, -- getNetworkClientIdForDomain: `${controllerName}:getNetworkClientIdForDomain`, -- setNetworkClientIdForDomain: `${controllerName}:setNetworkClientIdForDomain` --}; --var SelectedNetworkControllerEventTypes = { -- stateChange: `${controllerName}:stateChange` --}; --var _domainProxyMap, _useRequestQueuePreference, _registerMessageHandlers, registerMessageHandlers_fn, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn, _domainHasPermissions, domainHasPermissions_fn, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn; --var SelectedNetworkController = class extends _basecontroller.BaseController { -- /** -- * Construct a SelectedNetworkController controller. -- * -- * @param options - The controller options. -- * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. -- * @param options.state - The controllers initial state. -- * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference. -- * @param options.onPreferencesStateChange - A callback that is called when the preference state changes. -- * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use. -- */ -- constructor({ -- messenger, -- state = getDefaultState(), -- useRequestQueuePreference, -- onPreferencesStateChange, -- domainProxyMap -- }) { -- super({ -- name: controllerName, -- metadata: stateMetadata, -- messenger, -- state -- }); -- __privateAdd(this, _registerMessageHandlers); -- __privateAdd(this, _setNetworkClientIdForDomain); -- /** -- * This method is used when a domain is removed from the PermissionsController. -- * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy. -- * -- * @param domain - The domain for which to unset the network client ID. -- */ -- __privateAdd(this, _unsetNetworkClientIdForDomain); -- __privateAdd(this, _domainHasPermissions); -- // Loop through all domains and for those with permissions it points that domain's proxy -- // to an unproxied instance of the globally selected network client. -- // NOT the NetworkController's proxy of the globally selected networkClient -- __privateAdd(this, _resetAllPermissionedDomains); -- __privateAdd(this, _domainProxyMap, void 0); -- __privateAdd(this, _useRequestQueuePreference, void 0); -- __privateSet(this, _useRequestQueuePreference, useRequestQueuePreference); -- __privateSet(this, _domainProxyMap, domainProxyMap); -- __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -- this.messagingSystem.call("PermissionController:getSubjectNames").filter((domain) => this.state.domains[domain] === void 0).forEach( -- (domain) => this.setNetworkClientIdForDomain( -- domain, -- this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -- ) -- ); -- this.messagingSystem.subscribe( -- "PermissionController:stateChange", -- (_, patches) => { -- patches.forEach(({ op, path }) => { -- const isChangingSubject = path[0] === "subjects" && path[1] !== void 0; -- if (isChangingSubject && typeof path[1] === "string") { -- const domain = path[1]; -- if (op === "add" && this.state.domains[domain] === void 0) { -- this.setNetworkClientIdForDomain( -- domain, -- this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -- ); -- } else if (op === "remove" && this.state.domains[domain] !== void 0) { -- __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -- } -- } -- }); -- } -- ); -- this.messagingSystem.subscribe( -- "NetworkController:stateChange", -- ({ selectedNetworkClientId }, patches) => { -- patches.forEach(({ op, path }) => { -- if (op === "remove" && path[0] === "networkConfigurations") { -- const removedNetworkClientId = path[1]; -- Object.entries(this.state.domains).forEach( -- ([domain, networkClientIdForDomain]) => { -- if (networkClientIdForDomain === removedNetworkClientId) { -- this.setNetworkClientIdForDomain( -- domain, -- selectedNetworkClientId -- ); -- } -- } -- ); -- } -- }); -- } -- ); -- onPreferencesStateChange(({ useRequestQueue }) => { -- if (__privateGet(this, _useRequestQueuePreference) !== useRequestQueue) { -- if (!useRequestQueue) { -- Object.keys(this.state.domains).forEach((domain) => { -- __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -- }); -- } else { -- __privateMethod(this, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn).call(this); -- } -- __privateSet(this, _useRequestQueuePreference, useRequestQueue); -- } -- }); -- } -- setNetworkClientIdForDomain(domain, networkClientId) { -- if (domain === METAMASK_DOMAIN) { -- throw new Error( -- `NetworkClientId for domain "${METAMASK_DOMAIN}" cannot be set on the SelectedNetworkController` -- ); -- } -- if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -- return; -- } -- if (!__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- throw new Error( -- "NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions" -- ); -- } -- __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, networkClientId); -- } -- getNetworkClientIdForDomain(domain) { -- const { selectedNetworkClientId: metamaskSelectedNetworkClientId } = this.messagingSystem.call("NetworkController:getState"); -- if (!__privateGet(this, _useRequestQueuePreference)) { -- return metamaskSelectedNetworkClientId; -- } -- return this.state.domains[domain] ?? metamaskSelectedNetworkClientId; -- } -- /** -- * Accesses the provider and block tracker for the currently selected network. -- * -- * @param domain - the domain for the provider -- * @returns The proxy and block tracker proxies. -- */ -- getProviderAndBlockTracker(domain) { -- if (domain === METAMASK_DOMAIN || snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- if (networkClient === void 0) { -- throw new Error("Selected network not initialized"); -- } -- return networkClient; -- } -- let networkProxy = __privateGet(this, _domainProxyMap).get(domain); -- if (networkProxy === void 0) { -- let networkClient; -- if (__privateGet(this, _useRequestQueuePreference) && __privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- const networkClientId = this.getNetworkClientIdForDomain(domain); -- networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- } else { -- networkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- if (networkClient === void 0) { -- throw new Error("Selected network not initialized"); -- } -- } -- networkProxy = { -- provider: _swappableobjproxy.createEventEmitterProxy.call(void 0, networkClient.provider), -- blockTracker: _swappableobjproxy.createEventEmitterProxy.call(void 0, networkClient.blockTracker, { -- eventFilter: "skipInternal" -- }) -- }; -- __privateGet(this, _domainProxyMap).set(domain, networkProxy); -- } -- return networkProxy; -- } --}; --_domainProxyMap = new WeakMap(); --_useRequestQueuePreference = new WeakMap(); --_registerMessageHandlers = new WeakSet(); --registerMessageHandlers_fn = function() { -- this.messagingSystem.registerActionHandler( -- SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -- this.getNetworkClientIdForDomain.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain, -- this.setNetworkClientIdForDomain.bind(this) -- ); --}; --_setNetworkClientIdForDomain = new WeakSet(); --setNetworkClientIdForDomain_fn = function(domain, networkClientId) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- const networkProxy = this.getProviderAndBlockTracker(domain); -- networkProxy.provider.setTarget(networkClient.provider); -- networkProxy.blockTracker.setTarget(networkClient.blockTracker); -- this.update((state) => { -- state.domains[domain] = networkClientId; -- }); --}; --_unsetNetworkClientIdForDomain = new WeakSet(); --unsetNetworkClientIdForDomain_fn = function(domain) { -- const globallySelectedNetworkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- const networkProxy = __privateGet(this, _domainProxyMap).get(domain); -- if (networkProxy && globallySelectedNetworkClient) { -- networkProxy.provider.setTarget(globallySelectedNetworkClient.provider); -- networkProxy.blockTracker.setTarget( -- globallySelectedNetworkClient.blockTracker -- ); -- } else if (networkProxy) { -- __privateGet(this, _domainProxyMap).delete(domain); -- } -- this.update((state) => { -- delete state.domains[domain]; -- }); --}; --_domainHasPermissions = new WeakSet(); --domainHasPermissions_fn = function(domain) { -- return this.messagingSystem.call( -- "PermissionController:hasPermissions", -- domain -- ); --}; --_resetAllPermissionedDomains = new WeakSet(); --resetAllPermissionedDomains_fn = function() { -- __privateGet(this, _domainProxyMap).forEach((_, domain) => { -- const { selectedNetworkClientId } = this.messagingSystem.call( -- "NetworkController:getState" -- ); -- if (__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, selectedNetworkClientId); -- } -- }); --}; -- -- -- -- -- -- -- --exports.controllerName = controllerName; exports.METAMASK_DOMAIN = METAMASK_DOMAIN; exports.SelectedNetworkControllerActionTypes = SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = SelectedNetworkControllerEventTypes; exports.SelectedNetworkController = SelectedNetworkController; --//# sourceMappingURL=chunk-OGUVGN6R.js.map -\ No newline at end of file -diff --git a/dist/chunk-OGUVGN6R.js.map b/dist/chunk-OGUVGN6R.js.map -deleted file mode 100644 -index 4e38e02bd55874905ac6ea9c5874f01f102a7ff7..0000000000000000000000000000000000000000 ---- a/dist/chunk-OGUVGN6R.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/SelectedNetworkController.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n BlockTrackerProxy,\n NetworkClientId,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetSelectedNetworkClientAction,\n NetworkControllerGetStateAction,\n NetworkControllerStateChangeEvent,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport type {\n PermissionControllerStateChange,\n GetSubjects as PermissionControllerGetSubjectsAction,\n HasPermissions as PermissionControllerHasPermissions,\n} from '@metamask/permission-controller';\nimport { createEventEmitterProxy } from '@metamask/swappable-obj-proxy';\nimport type { Patch } from 'immer';\n\nexport const controllerName = 'SelectedNetworkController';\n\nconst stateMetadata = {\n domains: { persist: true, anonymous: false },\n};\n\nconst getDefaultState = () => ({ domains: {} });\n\n// npm and local are currently the only valid prefixes for snap domains\n// TODO: eventually we maybe want to pull this in from snaps-utils to ensure it stays in sync\n// For now it seems like overkill to add a dependency for this one constant\n// https://github.com/MetaMask/snaps/blob/2beee7803bfe9e540788a3558b546b9f55dc3cb4/packages/snaps-utils/src/types.ts#L120\nconst snapsPrefixes = ['npm:', 'local:'] as const;\n\nexport type Domain = string;\n\nexport const METAMASK_DOMAIN = 'metamask' as const;\n\nexport const SelectedNetworkControllerActionTypes = {\n getState: `${controllerName}:getState` as const,\n getNetworkClientIdForDomain:\n `${controllerName}:getNetworkClientIdForDomain` as const,\n setNetworkClientIdForDomain:\n `${controllerName}:setNetworkClientIdForDomain` as const,\n};\n\nexport const SelectedNetworkControllerEventTypes = {\n stateChange: `${controllerName}:stateChange` as const,\n};\n\nexport type SelectedNetworkControllerState = {\n domains: Record;\n};\n\nexport type SelectedNetworkControllerStateChangeEvent = {\n type: typeof SelectedNetworkControllerEventTypes.stateChange;\n payload: [SelectedNetworkControllerState, Patch[]];\n};\n\nexport type SelectedNetworkControllerGetSelectedNetworkStateAction = {\n type: typeof SelectedNetworkControllerActionTypes.getState;\n handler: () => SelectedNetworkControllerState;\n};\n\nexport type SelectedNetworkControllerGetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain;\n handler: SelectedNetworkController['getNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerSetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain;\n handler: SelectedNetworkController['setNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerActions =\n | SelectedNetworkControllerGetSelectedNetworkStateAction\n | SelectedNetworkControllerGetNetworkClientIdForDomainAction\n | SelectedNetworkControllerSetNetworkClientIdForDomainAction;\n\nexport type AllowedActions =\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetSelectedNetworkClientAction\n | NetworkControllerGetStateAction\n | PermissionControllerHasPermissions\n | PermissionControllerGetSubjectsAction;\n\nexport type SelectedNetworkControllerEvents =\n SelectedNetworkControllerStateChangeEvent;\n\nexport type AllowedEvents =\n | NetworkControllerStateChangeEvent\n | PermissionControllerStateChange;\n\nexport type SelectedNetworkControllerMessenger = RestrictedControllerMessenger<\n typeof controllerName,\n SelectedNetworkControllerActions | AllowedActions,\n SelectedNetworkControllerEvents | AllowedEvents,\n AllowedActions['type'],\n AllowedEvents['type']\n>;\n\nexport type SelectedNetworkControllerOptions = {\n state?: SelectedNetworkControllerState;\n messenger: SelectedNetworkControllerMessenger;\n useRequestQueuePreference: boolean;\n onPreferencesStateChange: (\n listener: (preferencesState: { useRequestQueue: boolean }) => void,\n ) => void;\n domainProxyMap: Map;\n};\n\nexport type NetworkProxy = {\n provider: ProviderProxy;\n blockTracker: BlockTrackerProxy;\n};\n\n/**\n * Controller for getting and setting the network for a particular domain.\n */\nexport class SelectedNetworkController extends BaseController<\n typeof controllerName,\n SelectedNetworkControllerState,\n SelectedNetworkControllerMessenger\n> {\n #domainProxyMap: Map;\n\n #useRequestQueuePreference: boolean;\n\n /**\n * Construct a SelectedNetworkController controller.\n *\n * @param options - The controller options.\n * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller.\n * @param options.state - The controllers initial state.\n * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference.\n * @param options.onPreferencesStateChange - A callback that is called when the preference state changes.\n * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use.\n */\n constructor({\n messenger,\n state = getDefaultState(),\n useRequestQueuePreference,\n onPreferencesStateChange,\n domainProxyMap,\n }: SelectedNetworkControllerOptions) {\n super({\n name: controllerName,\n metadata: stateMetadata,\n messenger,\n state,\n });\n this.#useRequestQueuePreference = useRequestQueuePreference;\n this.#domainProxyMap = domainProxyMap;\n this.#registerMessageHandlers();\n\n // this is fetching all the dapp permissions from the PermissionsController and looking for any domains that are not in domains state in this controller. Then we take any missing domains and add them to state here, setting it with the globally selected networkClientId (fetched from the NetworkController)\n this.messagingSystem\n .call('PermissionController:getSubjectNames')\n .filter((domain) => this.state.domains[domain] === undefined)\n .forEach((domain) =>\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n ),\n );\n\n this.messagingSystem.subscribe(\n 'PermissionController:stateChange',\n (_, patches) => {\n patches.forEach(({ op, path }) => {\n const isChangingSubject =\n path[0] === 'subjects' && path[1] !== undefined;\n if (isChangingSubject && typeof path[1] === 'string') {\n const domain = path[1];\n if (op === 'add' && this.state.domains[domain] === undefined) {\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n );\n } else if (\n op === 'remove' &&\n this.state.domains[domain] !== undefined\n ) {\n this.#unsetNetworkClientIdForDomain(domain);\n }\n }\n });\n },\n );\n\n this.messagingSystem.subscribe(\n 'NetworkController:stateChange',\n ({ selectedNetworkClientId }, patches) => {\n patches.forEach(({ op, path }) => {\n // if a network is removed, update the networkClientId for all domains that were using it to the selected network\n if (op === 'remove' && path[0] === 'networkConfigurations') {\n const removedNetworkClientId = path[1] as NetworkClientId;\n Object.entries(this.state.domains).forEach(\n ([domain, networkClientIdForDomain]) => {\n if (networkClientIdForDomain === removedNetworkClientId) {\n this.setNetworkClientIdForDomain(\n domain,\n selectedNetworkClientId,\n );\n }\n },\n );\n }\n });\n },\n );\n\n onPreferencesStateChange(({ useRequestQueue }) => {\n if (this.#useRequestQueuePreference !== useRequestQueue) {\n if (!useRequestQueue) {\n // Loop through all domains and points each domain's proxy\n // to the NetworkController's own proxy of the globally selected networkClient\n Object.keys(this.state.domains).forEach((domain) => {\n this.#unsetNetworkClientIdForDomain(domain);\n });\n } else {\n this.#resetAllPermissionedDomains();\n }\n this.#useRequestQueuePreference = useRequestQueue;\n }\n });\n }\n\n #registerMessageHandlers(): void {\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n this.getNetworkClientIdForDomain.bind(this),\n );\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain,\n this.setNetworkClientIdForDomain.bind(this),\n );\n }\n\n #setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n const networkProxy = this.getProviderAndBlockTracker(domain);\n networkProxy.provider.setTarget(networkClient.provider);\n networkProxy.blockTracker.setTarget(networkClient.blockTracker);\n\n this.update((state) => {\n state.domains[domain] = networkClientId;\n });\n }\n\n /**\n * This method is used when a domain is removed from the PermissionsController.\n * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy.\n *\n * @param domain - The domain for which to unset the network client ID.\n */\n #unsetNetworkClientIdForDomain(domain: Domain) {\n const globallySelectedNetworkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n const networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy && globallySelectedNetworkClient) {\n networkProxy.provider.setTarget(globallySelectedNetworkClient.provider);\n networkProxy.blockTracker.setTarget(\n globallySelectedNetworkClient.blockTracker,\n );\n } else if (networkProxy) {\n this.#domainProxyMap.delete(domain);\n }\n this.update((state) => {\n delete state.domains[domain];\n });\n }\n\n #domainHasPermissions(domain: Domain): boolean {\n return this.messagingSystem.call(\n 'PermissionController:hasPermissions',\n domain,\n );\n }\n\n // Loop through all domains and for those with permissions it points that domain's proxy\n // to an unproxied instance of the globally selected network client.\n // NOT the NetworkController's proxy of the globally selected networkClient\n #resetAllPermissionedDomains() {\n this.#domainProxyMap.forEach((_: NetworkProxy, domain: string) => {\n const { selectedNetworkClientId } = this.messagingSystem.call(\n 'NetworkController:getState',\n );\n // can't use public setNetworkClientIdForDomain because it will throw an error\n // rather than simply skip if the domain doesn't have permissions which can happen\n // in this case since proxies are added for each site the user visits\n if (this.#domainHasPermissions(domain)) {\n this.#setNetworkClientIdForDomain(domain, selectedNetworkClientId);\n }\n });\n }\n\n setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n if (domain === METAMASK_DOMAIN) {\n throw new Error(\n `NetworkClientId for domain \"${METAMASK_DOMAIN}\" cannot be set on the SelectedNetworkController`,\n );\n }\n\n if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) {\n return;\n }\n\n if (!this.#domainHasPermissions(domain)) {\n throw new Error(\n 'NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions',\n );\n }\n\n this.#setNetworkClientIdForDomain(domain, networkClientId);\n }\n\n getNetworkClientIdForDomain(domain: Domain): NetworkClientId {\n const { selectedNetworkClientId: metamaskSelectedNetworkClientId } =\n this.messagingSystem.call('NetworkController:getState');\n if (!this.#useRequestQueuePreference) {\n return metamaskSelectedNetworkClientId;\n }\n return this.state.domains[domain] ?? metamaskSelectedNetworkClientId;\n }\n\n /**\n * Accesses the provider and block tracker for the currently selected network.\n *\n * @param domain - the domain for the provider\n * @returns The proxy and block tracker proxies.\n */\n getProviderAndBlockTracker(domain: Domain): NetworkProxy {\n // If the domain is 'metamask' or a snap, return the NetworkController's globally selected network client proxy\n if (\n domain === METAMASK_DOMAIN ||\n snapsPrefixes.some((prefix) => domain.startsWith(prefix))\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n return networkClient;\n }\n\n let networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy === undefined) {\n let networkClient;\n if (\n this.#useRequestQueuePreference &&\n this.#domainHasPermissions(domain)\n ) {\n const networkClientId = this.getNetworkClientIdForDomain(domain);\n networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n } else {\n networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n }\n networkProxy = {\n provider: createEventEmitterProxy(networkClient.provider),\n blockTracker: createEventEmitterProxy(networkClient.blockTracker, {\n eventFilter: 'skipInternal',\n }),\n };\n this.#domainProxyMap.set(domain, networkProxy);\n }\n return networkProxy;\n }\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-S4D42VCM.mjs b/dist/chunk-S4D42VCM.mjs -deleted file mode 100644 -index a2a93432c8799383bef3bbe2cf3adf1ac3bda043..0000000000000000000000000000000000000000 ---- a/dist/chunk-S4D42VCM.mjs -+++ /dev/null -@@ -1,281 +0,0 @@ --var __accessCheck = (obj, member, msg) => { -- if (!member.has(obj)) -- throw TypeError("Cannot " + msg); --}; --var __privateGet = (obj, member, getter) => { -- __accessCheck(obj, member, "read from private field"); -- return getter ? getter.call(obj) : member.get(obj); --}; --var __privateAdd = (obj, member, value) => { -- if (member.has(obj)) -- throw TypeError("Cannot add the same private member more than once"); -- member instanceof WeakSet ? member.add(obj) : member.set(obj, value); --}; --var __privateSet = (obj, member, value, setter) => { -- __accessCheck(obj, member, "write to private field"); -- setter ? setter.call(obj, value) : member.set(obj, value); -- return value; --}; --var __privateMethod = (obj, member, method) => { -- __accessCheck(obj, member, "access private method"); -- return method; --}; -- --// src/SelectedNetworkController.ts --import { BaseController } from "@metamask/base-controller"; --import { createEventEmitterProxy } from "@metamask/swappable-obj-proxy"; --var controllerName = "SelectedNetworkController"; --var stateMetadata = { -- domains: { persist: true, anonymous: false } --}; --var getDefaultState = () => ({ domains: {} }); --var snapsPrefixes = ["npm:", "local:"]; --var METAMASK_DOMAIN = "metamask"; --var SelectedNetworkControllerActionTypes = { -- getState: `${controllerName}:getState`, -- getNetworkClientIdForDomain: `${controllerName}:getNetworkClientIdForDomain`, -- setNetworkClientIdForDomain: `${controllerName}:setNetworkClientIdForDomain` --}; --var SelectedNetworkControllerEventTypes = { -- stateChange: `${controllerName}:stateChange` --}; --var _domainProxyMap, _useRequestQueuePreference, _registerMessageHandlers, registerMessageHandlers_fn, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn, _domainHasPermissions, domainHasPermissions_fn, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn; --var SelectedNetworkController = class extends BaseController { -- /** -- * Construct a SelectedNetworkController controller. -- * -- * @param options - The controller options. -- * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller. -- * @param options.state - The controllers initial state. -- * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference. -- * @param options.onPreferencesStateChange - A callback that is called when the preference state changes. -- * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use. -- */ -- constructor({ -- messenger, -- state = getDefaultState(), -- useRequestQueuePreference, -- onPreferencesStateChange, -- domainProxyMap -- }) { -- super({ -- name: controllerName, -- metadata: stateMetadata, -- messenger, -- state -- }); -- __privateAdd(this, _registerMessageHandlers); -- __privateAdd(this, _setNetworkClientIdForDomain); -- /** -- * This method is used when a domain is removed from the PermissionsController. -- * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy. -- * -- * @param domain - The domain for which to unset the network client ID. -- */ -- __privateAdd(this, _unsetNetworkClientIdForDomain); -- __privateAdd(this, _domainHasPermissions); -- // Loop through all domains and for those with permissions it points that domain's proxy -- // to an unproxied instance of the globally selected network client. -- // NOT the NetworkController's proxy of the globally selected networkClient -- __privateAdd(this, _resetAllPermissionedDomains); -- __privateAdd(this, _domainProxyMap, void 0); -- __privateAdd(this, _useRequestQueuePreference, void 0); -- __privateSet(this, _useRequestQueuePreference, useRequestQueuePreference); -- __privateSet(this, _domainProxyMap, domainProxyMap); -- __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); -- this.messagingSystem.call("PermissionController:getSubjectNames").filter((domain) => this.state.domains[domain] === void 0).forEach( -- (domain) => this.setNetworkClientIdForDomain( -- domain, -- this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -- ) -- ); -- this.messagingSystem.subscribe( -- "PermissionController:stateChange", -- (_, patches) => { -- patches.forEach(({ op, path }) => { -- const isChangingSubject = path[0] === "subjects" && path[1] !== void 0; -- if (isChangingSubject && typeof path[1] === "string") { -- const domain = path[1]; -- if (op === "add" && this.state.domains[domain] === void 0) { -- this.setNetworkClientIdForDomain( -- domain, -- this.messagingSystem.call("NetworkController:getState").selectedNetworkClientId -- ); -- } else if (op === "remove" && this.state.domains[domain] !== void 0) { -- __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -- } -- } -- }); -- } -- ); -- this.messagingSystem.subscribe( -- "NetworkController:stateChange", -- ({ selectedNetworkClientId }, patches) => { -- patches.forEach(({ op, path }) => { -- if (op === "remove" && path[0] === "networkConfigurations") { -- const removedNetworkClientId = path[1]; -- Object.entries(this.state.domains).forEach( -- ([domain, networkClientIdForDomain]) => { -- if (networkClientIdForDomain === removedNetworkClientId) { -- this.setNetworkClientIdForDomain( -- domain, -- selectedNetworkClientId -- ); -- } -- } -- ); -- } -- }); -- } -- ); -- onPreferencesStateChange(({ useRequestQueue }) => { -- if (__privateGet(this, _useRequestQueuePreference) !== useRequestQueue) { -- if (!useRequestQueue) { -- Object.keys(this.state.domains).forEach((domain) => { -- __privateMethod(this, _unsetNetworkClientIdForDomain, unsetNetworkClientIdForDomain_fn).call(this, domain); -- }); -- } else { -- __privateMethod(this, _resetAllPermissionedDomains, resetAllPermissionedDomains_fn).call(this); -- } -- __privateSet(this, _useRequestQueuePreference, useRequestQueue); -- } -- }); -- } -- setNetworkClientIdForDomain(domain, networkClientId) { -- if (domain === METAMASK_DOMAIN) { -- throw new Error( -- `NetworkClientId for domain "${METAMASK_DOMAIN}" cannot be set on the SelectedNetworkController` -- ); -- } -- if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -- return; -- } -- if (!__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- throw new Error( -- "NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions" -- ); -- } -- __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, networkClientId); -- } -- getNetworkClientIdForDomain(domain) { -- const { selectedNetworkClientId: metamaskSelectedNetworkClientId } = this.messagingSystem.call("NetworkController:getState"); -- if (!__privateGet(this, _useRequestQueuePreference)) { -- return metamaskSelectedNetworkClientId; -- } -- return this.state.domains[domain] ?? metamaskSelectedNetworkClientId; -- } -- /** -- * Accesses the provider and block tracker for the currently selected network. -- * -- * @param domain - the domain for the provider -- * @returns The proxy and block tracker proxies. -- */ -- getProviderAndBlockTracker(domain) { -- if (domain === METAMASK_DOMAIN || snapsPrefixes.some((prefix) => domain.startsWith(prefix))) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- if (networkClient === void 0) { -- throw new Error("Selected network not initialized"); -- } -- return networkClient; -- } -- let networkProxy = __privateGet(this, _domainProxyMap).get(domain); -- if (networkProxy === void 0) { -- let networkClient; -- if (__privateGet(this, _useRequestQueuePreference) && __privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- const networkClientId = this.getNetworkClientIdForDomain(domain); -- networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- } else { -- networkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- if (networkClient === void 0) { -- throw new Error("Selected network not initialized"); -- } -- } -- networkProxy = { -- provider: createEventEmitterProxy(networkClient.provider), -- blockTracker: createEventEmitterProxy(networkClient.blockTracker, { -- eventFilter: "skipInternal" -- }) -- }; -- __privateGet(this, _domainProxyMap).set(domain, networkProxy); -- } -- return networkProxy; -- } --}; --_domainProxyMap = new WeakMap(); --_useRequestQueuePreference = new WeakMap(); --_registerMessageHandlers = new WeakSet(); --registerMessageHandlers_fn = function() { -- this.messagingSystem.registerActionHandler( -- SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -- this.getNetworkClientIdForDomain.bind(this) -- ); -- this.messagingSystem.registerActionHandler( -- SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain, -- this.setNetworkClientIdForDomain.bind(this) -- ); --}; --_setNetworkClientIdForDomain = new WeakSet(); --setNetworkClientIdForDomain_fn = function(domain, networkClientId) { -- const networkClient = this.messagingSystem.call( -- "NetworkController:getNetworkClientById", -- networkClientId -- ); -- const networkProxy = this.getProviderAndBlockTracker(domain); -- networkProxy.provider.setTarget(networkClient.provider); -- networkProxy.blockTracker.setTarget(networkClient.blockTracker); -- this.update((state) => { -- state.domains[domain] = networkClientId; -- }); --}; --_unsetNetworkClientIdForDomain = new WeakSet(); --unsetNetworkClientIdForDomain_fn = function(domain) { -- const globallySelectedNetworkClient = this.messagingSystem.call( -- "NetworkController:getSelectedNetworkClient" -- ); -- const networkProxy = __privateGet(this, _domainProxyMap).get(domain); -- if (networkProxy && globallySelectedNetworkClient) { -- networkProxy.provider.setTarget(globallySelectedNetworkClient.provider); -- networkProxy.blockTracker.setTarget( -- globallySelectedNetworkClient.blockTracker -- ); -- } else if (networkProxy) { -- __privateGet(this, _domainProxyMap).delete(domain); -- } -- this.update((state) => { -- delete state.domains[domain]; -- }); --}; --_domainHasPermissions = new WeakSet(); --domainHasPermissions_fn = function(domain) { -- return this.messagingSystem.call( -- "PermissionController:hasPermissions", -- domain -- ); --}; --_resetAllPermissionedDomains = new WeakSet(); --resetAllPermissionedDomains_fn = function() { -- __privateGet(this, _domainProxyMap).forEach((_, domain) => { -- const { selectedNetworkClientId } = this.messagingSystem.call( -- "NetworkController:getState" -- ); -- if (__privateMethod(this, _domainHasPermissions, domainHasPermissions_fn).call(this, domain)) { -- __privateMethod(this, _setNetworkClientIdForDomain, setNetworkClientIdForDomain_fn).call(this, domain, selectedNetworkClientId); -- } -- }); --}; -- --export { -- controllerName, -- METAMASK_DOMAIN, -- SelectedNetworkControllerActionTypes, -- SelectedNetworkControllerEventTypes, -- SelectedNetworkController --}; --//# sourceMappingURL=chunk-S4D42VCM.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-S4D42VCM.mjs.map b/dist/chunk-S4D42VCM.mjs.map -deleted file mode 100644 -index b05eba0cbdeb5af4729d92e7b8de695c2d2a75e3..0000000000000000000000000000000000000000 ---- a/dist/chunk-S4D42VCM.mjs.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/SelectedNetworkController.ts"],"sourcesContent":["import type { RestrictedControllerMessenger } from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n BlockTrackerProxy,\n NetworkClientId,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetSelectedNetworkClientAction,\n NetworkControllerGetStateAction,\n NetworkControllerStateChangeEvent,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport type {\n PermissionControllerStateChange,\n GetSubjects as PermissionControllerGetSubjectsAction,\n HasPermissions as PermissionControllerHasPermissions,\n} from '@metamask/permission-controller';\nimport { createEventEmitterProxy } from '@metamask/swappable-obj-proxy';\nimport type { Patch } from 'immer';\n\nexport const controllerName = 'SelectedNetworkController';\n\nconst stateMetadata = {\n domains: { persist: true, anonymous: false },\n};\n\nconst getDefaultState = () => ({ domains: {} });\n\n// npm and local are currently the only valid prefixes for snap domains\n// TODO: eventually we maybe want to pull this in from snaps-utils to ensure it stays in sync\n// For now it seems like overkill to add a dependency for this one constant\n// https://github.com/MetaMask/snaps/blob/2beee7803bfe9e540788a3558b546b9f55dc3cb4/packages/snaps-utils/src/types.ts#L120\nconst snapsPrefixes = ['npm:', 'local:'] as const;\n\nexport type Domain = string;\n\nexport const METAMASK_DOMAIN = 'metamask' as const;\n\nexport const SelectedNetworkControllerActionTypes = {\n getState: `${controllerName}:getState` as const,\n getNetworkClientIdForDomain:\n `${controllerName}:getNetworkClientIdForDomain` as const,\n setNetworkClientIdForDomain:\n `${controllerName}:setNetworkClientIdForDomain` as const,\n};\n\nexport const SelectedNetworkControllerEventTypes = {\n stateChange: `${controllerName}:stateChange` as const,\n};\n\nexport type SelectedNetworkControllerState = {\n domains: Record;\n};\n\nexport type SelectedNetworkControllerStateChangeEvent = {\n type: typeof SelectedNetworkControllerEventTypes.stateChange;\n payload: [SelectedNetworkControllerState, Patch[]];\n};\n\nexport type SelectedNetworkControllerGetSelectedNetworkStateAction = {\n type: typeof SelectedNetworkControllerActionTypes.getState;\n handler: () => SelectedNetworkControllerState;\n};\n\nexport type SelectedNetworkControllerGetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain;\n handler: SelectedNetworkController['getNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerSetNetworkClientIdForDomainAction = {\n type: typeof SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain;\n handler: SelectedNetworkController['setNetworkClientIdForDomain'];\n};\n\nexport type SelectedNetworkControllerActions =\n | SelectedNetworkControllerGetSelectedNetworkStateAction\n | SelectedNetworkControllerGetNetworkClientIdForDomainAction\n | SelectedNetworkControllerSetNetworkClientIdForDomainAction;\n\nexport type AllowedActions =\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetSelectedNetworkClientAction\n | NetworkControllerGetStateAction\n | PermissionControllerHasPermissions\n | PermissionControllerGetSubjectsAction;\n\nexport type SelectedNetworkControllerEvents =\n SelectedNetworkControllerStateChangeEvent;\n\nexport type AllowedEvents =\n | NetworkControllerStateChangeEvent\n | PermissionControllerStateChange;\n\nexport type SelectedNetworkControllerMessenger = RestrictedControllerMessenger<\n typeof controllerName,\n SelectedNetworkControllerActions | AllowedActions,\n SelectedNetworkControllerEvents | AllowedEvents,\n AllowedActions['type'],\n AllowedEvents['type']\n>;\n\nexport type SelectedNetworkControllerOptions = {\n state?: SelectedNetworkControllerState;\n messenger: SelectedNetworkControllerMessenger;\n useRequestQueuePreference: boolean;\n onPreferencesStateChange: (\n listener: (preferencesState: { useRequestQueue: boolean }) => void,\n ) => void;\n domainProxyMap: Map;\n};\n\nexport type NetworkProxy = {\n provider: ProviderProxy;\n blockTracker: BlockTrackerProxy;\n};\n\n/**\n * Controller for getting and setting the network for a particular domain.\n */\nexport class SelectedNetworkController extends BaseController<\n typeof controllerName,\n SelectedNetworkControllerState,\n SelectedNetworkControllerMessenger\n> {\n #domainProxyMap: Map;\n\n #useRequestQueuePreference: boolean;\n\n /**\n * Construct a SelectedNetworkController controller.\n *\n * @param options - The controller options.\n * @param options.messenger - The restricted controller messenger for the EncryptionPublicKey controller.\n * @param options.state - The controllers initial state.\n * @param options.useRequestQueuePreference - A boolean indicating whether to use the request queue preference.\n * @param options.onPreferencesStateChange - A callback that is called when the preference state changes.\n * @param options.domainProxyMap - A map for storing domain-specific proxies that are held in memory only during use.\n */\n constructor({\n messenger,\n state = getDefaultState(),\n useRequestQueuePreference,\n onPreferencesStateChange,\n domainProxyMap,\n }: SelectedNetworkControllerOptions) {\n super({\n name: controllerName,\n metadata: stateMetadata,\n messenger,\n state,\n });\n this.#useRequestQueuePreference = useRequestQueuePreference;\n this.#domainProxyMap = domainProxyMap;\n this.#registerMessageHandlers();\n\n // this is fetching all the dapp permissions from the PermissionsController and looking for any domains that are not in domains state in this controller. Then we take any missing domains and add them to state here, setting it with the globally selected networkClientId (fetched from the NetworkController)\n this.messagingSystem\n .call('PermissionController:getSubjectNames')\n .filter((domain) => this.state.domains[domain] === undefined)\n .forEach((domain) =>\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n ),\n );\n\n this.messagingSystem.subscribe(\n 'PermissionController:stateChange',\n (_, patches) => {\n patches.forEach(({ op, path }) => {\n const isChangingSubject =\n path[0] === 'subjects' && path[1] !== undefined;\n if (isChangingSubject && typeof path[1] === 'string') {\n const domain = path[1];\n if (op === 'add' && this.state.domains[domain] === undefined) {\n this.setNetworkClientIdForDomain(\n domain,\n this.messagingSystem.call('NetworkController:getState')\n .selectedNetworkClientId,\n );\n } else if (\n op === 'remove' &&\n this.state.domains[domain] !== undefined\n ) {\n this.#unsetNetworkClientIdForDomain(domain);\n }\n }\n });\n },\n );\n\n this.messagingSystem.subscribe(\n 'NetworkController:stateChange',\n ({ selectedNetworkClientId }, patches) => {\n patches.forEach(({ op, path }) => {\n // if a network is removed, update the networkClientId for all domains that were using it to the selected network\n if (op === 'remove' && path[0] === 'networkConfigurations') {\n const removedNetworkClientId = path[1] as NetworkClientId;\n Object.entries(this.state.domains).forEach(\n ([domain, networkClientIdForDomain]) => {\n if (networkClientIdForDomain === removedNetworkClientId) {\n this.setNetworkClientIdForDomain(\n domain,\n selectedNetworkClientId,\n );\n }\n },\n );\n }\n });\n },\n );\n\n onPreferencesStateChange(({ useRequestQueue }) => {\n if (this.#useRequestQueuePreference !== useRequestQueue) {\n if (!useRequestQueue) {\n // Loop through all domains and points each domain's proxy\n // to the NetworkController's own proxy of the globally selected networkClient\n Object.keys(this.state.domains).forEach((domain) => {\n this.#unsetNetworkClientIdForDomain(domain);\n });\n } else {\n this.#resetAllPermissionedDomains();\n }\n this.#useRequestQueuePreference = useRequestQueue;\n }\n });\n }\n\n #registerMessageHandlers(): void {\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n this.getNetworkClientIdForDomain.bind(this),\n );\n this.messagingSystem.registerActionHandler(\n SelectedNetworkControllerActionTypes.setNetworkClientIdForDomain,\n this.setNetworkClientIdForDomain.bind(this),\n );\n }\n\n #setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n const networkProxy = this.getProviderAndBlockTracker(domain);\n networkProxy.provider.setTarget(networkClient.provider);\n networkProxy.blockTracker.setTarget(networkClient.blockTracker);\n\n this.update((state) => {\n state.domains[domain] = networkClientId;\n });\n }\n\n /**\n * This method is used when a domain is removed from the PermissionsController.\n * It will remove re-point the network proxy to the globally selected network in the domainProxyMap or, if no globally selected network client is available, delete the proxy.\n *\n * @param domain - The domain for which to unset the network client ID.\n */\n #unsetNetworkClientIdForDomain(domain: Domain) {\n const globallySelectedNetworkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n const networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy && globallySelectedNetworkClient) {\n networkProxy.provider.setTarget(globallySelectedNetworkClient.provider);\n networkProxy.blockTracker.setTarget(\n globallySelectedNetworkClient.blockTracker,\n );\n } else if (networkProxy) {\n this.#domainProxyMap.delete(domain);\n }\n this.update((state) => {\n delete state.domains[domain];\n });\n }\n\n #domainHasPermissions(domain: Domain): boolean {\n return this.messagingSystem.call(\n 'PermissionController:hasPermissions',\n domain,\n );\n }\n\n // Loop through all domains and for those with permissions it points that domain's proxy\n // to an unproxied instance of the globally selected network client.\n // NOT the NetworkController's proxy of the globally selected networkClient\n #resetAllPermissionedDomains() {\n this.#domainProxyMap.forEach((_: NetworkProxy, domain: string) => {\n const { selectedNetworkClientId } = this.messagingSystem.call(\n 'NetworkController:getState',\n );\n // can't use public setNetworkClientIdForDomain because it will throw an error\n // rather than simply skip if the domain doesn't have permissions which can happen\n // in this case since proxies are added for each site the user visits\n if (this.#domainHasPermissions(domain)) {\n this.#setNetworkClientIdForDomain(domain, selectedNetworkClientId);\n }\n });\n }\n\n setNetworkClientIdForDomain(\n domain: Domain,\n networkClientId: NetworkClientId,\n ) {\n if (domain === METAMASK_DOMAIN) {\n throw new Error(\n `NetworkClientId for domain \"${METAMASK_DOMAIN}\" cannot be set on the SelectedNetworkController`,\n );\n }\n\n if (snapsPrefixes.some((prefix) => domain.startsWith(prefix))) {\n return;\n }\n\n if (!this.#domainHasPermissions(domain)) {\n throw new Error(\n 'NetworkClientId for domain cannot be called with a domain that has not yet been granted permissions',\n );\n }\n\n this.#setNetworkClientIdForDomain(domain, networkClientId);\n }\n\n getNetworkClientIdForDomain(domain: Domain): NetworkClientId {\n const { selectedNetworkClientId: metamaskSelectedNetworkClientId } =\n this.messagingSystem.call('NetworkController:getState');\n if (!this.#useRequestQueuePreference) {\n return metamaskSelectedNetworkClientId;\n }\n return this.state.domains[domain] ?? metamaskSelectedNetworkClientId;\n }\n\n /**\n * Accesses the provider and block tracker for the currently selected network.\n *\n * @param domain - the domain for the provider\n * @returns The proxy and block tracker proxies.\n */\n getProviderAndBlockTracker(domain: Domain): NetworkProxy {\n // If the domain is 'metamask' or a snap, return the NetworkController's globally selected network client proxy\n if (\n domain === METAMASK_DOMAIN ||\n snapsPrefixes.some((prefix) => domain.startsWith(prefix))\n ) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n return networkClient;\n }\n\n let networkProxy = this.#domainProxyMap.get(domain);\n if (networkProxy === undefined) {\n let networkClient;\n if (\n this.#useRequestQueuePreference &&\n this.#domainHasPermissions(domain)\n ) {\n const networkClientId = this.getNetworkClientIdForDomain(domain);\n networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n } else {\n networkClient = this.messagingSystem.call(\n 'NetworkController:getSelectedNetworkClient',\n );\n if (networkClient === undefined) {\n throw new Error('Selected network not initialized');\n }\n }\n networkProxy = {\n provider: createEventEmitterProxy(networkClient.provider),\n blockTracker: createEventEmitterProxy(networkClient.blockTracker, {\n eventFilter: 'skipInternal',\n }),\n };\n this.#domainProxyMap.set(domain, networkProxy);\n }\n return networkProxy;\n }\n}\n"],"mappings":"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","names":[]} -\ No newline at end of file -diff --git a/dist/chunk-ZY7ETPVE.mjs b/dist/chunk-ZY7ETPVE.mjs -deleted file mode 100644 -index 50a21cfa6988a3e32a4c1a20f9113eaec7bf99ac..0000000000000000000000000000000000000000 ---- a/dist/chunk-ZY7ETPVE.mjs -+++ /dev/null -@@ -1,23 +0,0 @@ --import { -- SelectedNetworkControllerActionTypes --} from "./chunk-S4D42VCM.mjs"; -- --// src/SelectedNetworkMiddleware.ts --var createSelectedNetworkMiddleware = (messenger) => { -- const getNetworkClientIdForDomain = (origin) => messenger.call( -- SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain, -- origin -- ); -- return (req, _, next) => { -- if (!req.origin) { -- throw new Error("Request object is lacking an 'origin'"); -- } -- req.networkClientId = getNetworkClientIdForDomain(req.origin); -- return next(); -- }; --}; -- --export { -- createSelectedNetworkMiddleware --}; --//# sourceMappingURL=chunk-ZY7ETPVE.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-ZY7ETPVE.mjs.map b/dist/chunk-ZY7ETPVE.mjs.map -deleted file mode 100644 -index fbcfd73d57f291b4f129caa47ef0c9614987cc30..0000000000000000000000000000000000000000 ---- a/dist/chunk-ZY7ETPVE.mjs.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/SelectedNetworkMiddleware.ts"],"sourcesContent":["import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';\nimport type { NetworkClientId } from '@metamask/network-controller';\nimport type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';\n\nimport type { SelectedNetworkControllerMessenger } from './SelectedNetworkController';\nimport { SelectedNetworkControllerActionTypes } from './SelectedNetworkController';\n\nexport type SelectedNetworkMiddlewareJsonRpcRequest = JsonRpcRequest & {\n networkClientId?: NetworkClientId;\n origin?: string;\n};\n\nexport const createSelectedNetworkMiddleware = (\n messenger: SelectedNetworkControllerMessenger,\n): JsonRpcMiddleware => {\n const getNetworkClientIdForDomain = (origin: string) =>\n messenger.call(\n SelectedNetworkControllerActionTypes.getNetworkClientIdForDomain,\n origin,\n );\n\n return (req: SelectedNetworkMiddlewareJsonRpcRequest, _, next) => {\n if (!req.origin) {\n throw new Error(\"Request object is lacking an 'origin'\");\n }\n\n req.networkClientId = getNetworkClientIdForDomain(req.origin);\n return next();\n };\n};\n"],"mappings":";;;;;AAYO,IAAM,kCAAkC,CAC7C,cAC2C;AAC3C,QAAM,8BAA8B,CAAC,WACnC,UAAU;AAAA,IACR,qCAAqC;AAAA,IACrC;AAAA,EACF;AAEF,SAAO,CAAC,KAA8C,GAAG,SAAS;AAChE,QAAI,CAAC,IAAI,QAAQ;AACf,YAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAEA,QAAI,kBAAkB,4BAA4B,IAAI,MAAM;AAC5D,WAAO,KAAK;AAAA,EACd;AACF;","names":[]} -\ No newline at end of file -diff --git a/dist/index.js b/dist/index.js -index 4c9b1798d88466e11abbcf1c0616f2617d73ab5a..cd71df24b3f6424f640c62ce8a7938fcf15ae480 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -1,17 +1,17 @@ - "use strict";Object.defineProperty(exports, "__esModule", {value: true}); - --var _chunk6W2ETVOHjs = require('./chunk-6W2ETVOH.js'); -+var _chunkANSSZMDIjs = require('./chunk-ANSSZMDI.js'); - - - - - --var _chunkOGUVGN6Rjs = require('./chunk-OGUVGN6R.js'); -+var _chunkCECZWJ42js = require('./chunk-CECZWJ42.js'); - - - - - - --exports.METAMASK_DOMAIN = _chunkOGUVGN6Rjs.METAMASK_DOMAIN; exports.SelectedNetworkController = _chunkOGUVGN6Rjs.SelectedNetworkController; exports.SelectedNetworkControllerActionTypes = _chunkOGUVGN6Rjs.SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = _chunkOGUVGN6Rjs.SelectedNetworkControllerEventTypes; exports.createSelectedNetworkMiddleware = _chunk6W2ETVOHjs.createSelectedNetworkMiddleware; -+exports.METAMASK_DOMAIN = _chunkCECZWJ42js.METAMASK_DOMAIN; exports.SelectedNetworkController = _chunkCECZWJ42js.SelectedNetworkController; exports.SelectedNetworkControllerActionTypes = _chunkCECZWJ42js.SelectedNetworkControllerActionTypes; exports.SelectedNetworkControllerEventTypes = _chunkCECZWJ42js.SelectedNetworkControllerEventTypes; exports.createSelectedNetworkMiddleware = _chunkANSSZMDIjs.createSelectedNetworkMiddleware; - //# sourceMappingURL=index.js.map -\ No newline at end of file -diff --git a/dist/index.mjs b/dist/index.mjs -index 8780cadd8a535b745960ebde8d7aa49989451ef6..0c7b103ca9cb6a604bf5f831bccef3002abd7c5f 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -1,12 +1,12 @@ - import { - createSelectedNetworkMiddleware --} from "./chunk-ZY7ETPVE.mjs"; -+} from "./chunk-HFN7TKJS.mjs"; - import { - METAMASK_DOMAIN, - SelectedNetworkController, - SelectedNetworkControllerActionTypes, - SelectedNetworkControllerEventTypes --} from "./chunk-S4D42VCM.mjs"; -+} from "./chunk-7DSTEJNI.mjs"; - export { - METAMASK_DOMAIN, - SelectedNetworkController, -diff --git a/dist/tsconfig.build.tsbuildinfo b/dist/tsconfig.build.tsbuildinfo -index 0f3a7a7e18a90ebf82da9962818a7e552c892d9c..8a2c9ef72e1d3a4302d4bda2c3e634414225fd17 100644 ---- a/dist/tsconfig.build.tsbuildinfo -+++ b/dist/tsconfig.build.tsbuildinfo -@@ -1 +1 @@ --{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createprovider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../base-controller/dist/types/basecontrollerv1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asynceventemitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacytransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/basetransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionfactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerclass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/restrictedcontrollermessenger.d.ts","../../base-controller/dist/types/controllermessenger.d.ts","../../base-controller/dist/types/basecontrollerv2.d.ts","../../base-controller/dist/types/index.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/types.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createeventemitterproxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createswappableproxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/index.d.ts","../../network-controller/dist/types/constants.d.ts","../../../node_modules/@metamask/safe-event-emitter/index.d.ts","../../json-rpc-engine/dist/types/jsonrpcengine.d.ts","../../json-rpc-engine/dist/types/createasyncmiddleware.d.ts","../../json-rpc-engine/dist/types/createscaffoldmiddleware.d.ts","../../json-rpc-engine/dist/types/getuniqueid.d.ts","../../json-rpc-engine/dist/types/idremapmiddleware.d.ts","../../json-rpc-engine/dist/types/mergemiddleware.d.ts","../../json-rpc-engine/dist/types/index.d.ts","../../eth-json-rpc-provider/dist/types/safe-event-emitter-provider.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-engine.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-middleware.d.ts","../../eth-json-rpc-provider/dist/types/index.d.ts","../../../node_modules/eth-block-tracker/dist/blocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/pollingblocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/subscribeblocktracker.d.ts","../../../node_modules/eth-block-tracker/dist/index.d.ts","../../network-controller/dist/types/types.d.ts","../../network-controller/dist/types/create-auto-managed-network-client.d.ts","../../network-controller/dist/types/networkcontroller.d.ts","../../network-controller/dist/types/create-network-client.d.ts","../../network-controller/dist/types/index.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/utils.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/classes.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/errors.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/error-constants.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/index.d.ts","../../approval-controller/dist/types/approvalcontroller.d.ts","../../approval-controller/dist/types/errors.d.ts","../../approval-controller/dist/types/index.d.ts","../../permission-controller/dist/types/permission-middleware.d.ts","../../permission-controller/dist/types/subjectmetadatacontroller.d.ts","../../permission-controller/dist/types/permissioncontroller.d.ts","../../permission-controller/dist/types/permission.d.ts","../../permission-controller/dist/types/caveat.d.ts","../../permission-controller/dist/types/errors.d.ts","../../permission-controller/dist/types/utils.d.ts","../../permission-controller/dist/types/rpc-methods/getpermissions.d.ts","../../permission-controller/dist/types/rpc-methods/requestpermissions.d.ts","../../permission-controller/dist/types/rpc-methods/revokepermissions.d.ts","../../permission-controller/dist/types/rpc-methods/index.d.ts","../../permission-controller/dist/types/index.d.ts","../src/selectednetworkcontroller.ts","../src/selectednetworkmiddleware.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupsemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/difflines.d.ts","../../../node_modules/jest-diff/build/printdiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","e475453e7140e95542332943d3052fe4c7430ad1efce42b3e9157f1fee8cbc5f","ebfdf904255ce746c9d30117c2edef355fb19bf7650478d2405f39f0e4f302e6","f3f63b48addb8e2ea9d20bb671c3c306413b3daa39996d0ae52f63d8e32158e1","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","b4000a0a525fa921e896cbdb32ae802c9684f0fd371b5fc69e7310f7918cc2c3","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","34693fb4a5e771e11668219221344dd1bd7d8b77ed005a1c1d965fb559be8406","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"e44ea2d6b7b853f6c81482416db43dafc11944561b810e469ae423085511ce7e","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"a7289d79eb84a59d2475b4d0136b4404be3cfdd17c3ea46b9194add1d645df01","affectsGlobalScope":true},"0bb26fa2a90ee890eed57ee812c71fa84d3d07850163ec4a204de86412cc57c1","132ca47da601c60141dd6f10bd08c70d0620177e5638439df2464ec3945b6d98",{"version":"55d2bbae076fed7269c3e16faeb32f988f558427b7a1c3bf04aa7551ab86ae90","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","cf83847c9264dcd592b6c89c1542925b899b277228687f3638614e3fa784cf76","3a41ebe7f089d50f447466b35b6cabb8b584c0994fc9809d0cd0a4ebc41e1239","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","0c42d6cba77d9ad1cf45256ccb8489aa502fe2dbee1ec9048a29d49f5d532e73","2cf89c17245db65d175d4ef699cd68187516f9b3ae5c572fc0b9ad60f35dc223","5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"a34d65f61ec5aac5b53502c8b0bd4e00d217bccb95bf94d449e2571baa11fb8c","affectsGlobalScope":true},"8d42e5af5fb0a96a77e135ce84cc60636c9bad39d9dba043a4efe9d1bdeb3cc3","56fcc451e9065eb121c9cc4c1b9994a816306f3b0b3b1fce7ad59f0ac97a9999","8a6f12b74d3e6c4f5e1b918cb8e64ae16bc6756cf0d48bcc28a28e1bf26ca0cd","c3759b5bc5cc40f5988d86a497741a80fa91258629ae50a2b3735e774cd377cc","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","45dd82fb5aea9b12b2a90b427b28f3a014e8b2ee9b74087a5ab882841cb5fbc5",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"48b2f9302651eb31acd5be69bb4e6b35797a7fcd6b77391d10a4ccadf7dc3609","0c8c917ef15498c827bd494a0ef365e9f76deb211f8acbb86932e20489310788","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"9cdc2c6144b03822c9842505d09945bcf813b86827fdb260dd7586b63abc19bf","affectsGlobalScope":true},{"version":"2923dee3c897f03e91b54a210cdbefea7290562f0ac4b948667d4c9ee844b79e","affectsGlobalScope":true},"79169698d09a2be54b14f3bcad2575b414bf3525063fde0a1e4fcd5d6efd380e","051d939bcf77caa3cef3282708ab3a6fdfb741a7366e1d74a9e7603b67417ec3","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","a234d62ae81d012ebf23898a45672edf3e5c93ecf5a438a42b96c08dd68cde43","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","09ed02a725db002693236b6dfc49b2c6eb5557be1421d7fbe4f07cfe38211d92","09d801ff4a303d4976d4b9cb94af3a9097c4a70345e662d176975872d2998e51","c8558b01389b5f7610ac293aa612ccea2ae64d83af43b49f8142f190be1f414c","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"b10b426c56e220b5093bf8a2446ee47af47263b7b1a03f4b18e42326b231b111","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b4635ef36bee17e1304337d591c3b6b461ecdbc1876d0effbe6a581e62201fe5","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"e4507242542bd499238f693d88b2d32e22177cc508854625f87bcc9bc3fa1256","affectsGlobalScope":true},{"version":"d942354e4966a98d3a92d1b1af0b4ac06f33af3f88116743e2c304c027ca26ef","affectsGlobalScope":true},"39f0808e5be3cb38674726c21fe2eb453c55e48a901679b4ce30fef85549b892","6afd66a7432ef100027ea110449e874196381e019e30eda7e7d8ca390366b7a8","befb8a9a78ac99d8fbc3ed392810489a7b90760c7a58934e8f1c8538f581cff3","e670bdf01540d35c170fae68edfd2f288eff909936780c379d6a9103b787b22c","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"88003d9ab15507806f41b120be6d407c1afe566c2f6689ebe3a034dd5ec0c8dc","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","6a9c5127096b35264eb7cd21b2417bfc1d42cceca9ba4ce2bb0c3410b7816042","93b7325b49dfbf613d940ed0e471216657b2d77459dac34f1b5b1678f08f884c","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","563fa27fdaec8f195b84f71a7af0ef48d30d5cc830575db86da86a63a470c8e6","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","d6e6620a30d582182acc3f0a992a0c311adc589f111096aea11ab83fc09a5ccc","6213b8f686f56beab22b59a0f468590fd3a4c5fa931236a017efeca91d7c9584","c451cec9a588b1f105a5ea2c6063d4fca112b9d70105cacdadda0e1ef67e9379","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","a12eaa942232703a8a8477a2f240ad5a2c26c595012ea8f128224e77984099c4","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","8129a34006218a6f3cdc81bbd438d5429eb18b08b4338a26977ac3b4df129d75","30d2170e1a718b5035611af55e3618b4ba8f42f0749bb52ee593da6082c4e2ce","98ef38666d88ec9699a722053e07ede65d3042f693fe7ff8c786e53dbb6fd43b","a3b8b6be7620897d1e481e8650c980a210a138fceb6e710eaf95fd9dd0dfe94a","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","9f9e5bae412fa5909fae636d6733aee27a108cc2ed5b13980611016336774d3c","662fe197bba64bd3f17ee118058cd2d0d2dbe33d7c0c865fd6365d90bfc44e1e","030519c351f800551cac2658038804969ca4584d2c0175a710602ac234ca1340","0278a6939ca83cd040b08ff8c5fc7838b6693ddc52f22526bf158e6b10e0246c","c2d6206e5ba4fd3063b01218c2b3b997afc1cfbeb49fcee991fa8595842ce53d","29c188a2c660f99f1b4835022e011c4268d7af989d4b7dda33c0a69ca1a777f8","1ed0bf138e87912d741e28333b58cbf814ae863783b3b404d2454cbabb9c5fc0","3452ee7d8ef0b1bbd47b2a56924a1dc3c79dc84a19d212e9dc496f92e4943aa0","99510e20e3d4816e283e59e8f0f31f603b2f026648240ffdb1ca9f24be678419","037d1fbeb96dc35600814be14d0fbf31acf35f1d7b443ea33514937de69c2bf2","64d1859b7dd9f419ba08e064c2b16b1a5edde0316d6c2bb1833c9381d4dffc3d","69b0f96ca137c0dba05f321a159141ad36f79cfba2fffcc29d131e280275e6f2","7bb64cf513a2b1cf7a94f81ca201f3d76a9b17af556f0cfc4e2707443e6caa66","a9add2a29da4cd0b617ae89f196b3f2172a031aeb086922cdf097236eef8b008","99afac3e6e683ee3111e499f9919953e9489cb39cad74363717aa3805e91db51","c49f92b83968f4ee0b6026396a9b6e2d6fee8b660d08a90efb03355ce3433a7a","4fdc6afe4d7ef6aeb32ac0818d47e99f98a31d0696abc4cb2af489c78ac1ba1d","d73d5a0e854037d43781b2d5d33f4b95ee509e0ddede677aade79fbee6a97cdc","35d14e1ae04be300828b1a1614316b9312a009cfd5e29fa56f94c2a9f60b12df","d160fe745f9c3b72d7b9036fdb2b6b500a520d43e36bb842c927b6fe59ea2c23",{"version":"4a415bf08be658b3f7ab2c2754a077e36a32e08aabb73aad26de47a45c0fa81e","signature":"3a4f9e7087c566703447928d15c234bd0bcc63a2a50b6ec39ed37c9bc0342310"},{"version":"506bcad28e45e13c23c2c25c9691e0dc42d8755a0e24b9a48586f51b5bebae8f","signature":"3a12771c76c5ed979438dc0e390224bd3d8661bcbea18114f77c4c6d9de5b8b2"},{"version":"6fbdb35bc3b9cfb225c48108b5bacb5c0d67bbcb677cf13b912e15a852551023","signature":"ee06131adf64c6cd201de36563174cff0e160b81438be186a7bd85e6c2b54fa7"},"a5aaeca001d2f69093d04aac4db321e4c338fd9b20cbc4f0b0af3dc6ae0f235b","cc957354aa3c94c9961ebf46282cfde1e81d107fc5785a61f62c67f1dd3ac2eb","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","93de1c6dab503f053efe8d304cb522bb3a89feab8c98f307a674a4fae04773e9","29a46d003ca3c721e6405f00dee7e3de91b14e09701eba5d887bf76fb2d47d38","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","9990f9e566bc3c2c6e38df81294fb756e7f5b7b0e5bb17ab75384e190548b4b6",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","df95e00612c1faa5e0e7ef0dba589b18665bbeb3221db2b6cee1fe4d0e61921f","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","8b06ac3faeacb8484d84ddb44571d8f410697f98d7bfa86c0fda60373a9f5215","7eb06594824ada538b1d8b48c3925a83e7db792f47a081a62cf3e5c4e23cf0ee","f5638f7c2f12a9a1a57b5c41b3c1ea7db3876c003bab68e6a57afd6bcc169af0","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","675e702f2032766a91eeadee64f51014c64688525da99dccd8178f0c599f13a8","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","19c816167e076e7c24f074389c6cf3ed87bdbb917d1ea439ca281f9d26db2439","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","98f9d826db9cd99d27a01a59ee5f22863df00ccf1aaf43e1d7db80ebf716f7c3","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","dcd91d3b697cb650b95db5471189b99815af5db2a1cd28760f91e0b12ede8ed5","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","3cf0d343c2276842a5b617f22ba82af6322c7cfe8bb52238ffc0c491a3c21019","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9",{"version":"f2eff8704452659641164876c1ef0df4174659ce7311b0665798ea3f556fa9ad","affectsGlobalScope":true},"8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","2b8264b2fefd7367e0f20e2c04eed5d3038831fe00f5efbc110ff0131aab899b","a73a445c1e0a6d0f8b48e8eb22dc9d647896783a7f8991cbbc31c0d94bf1f5a2","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","cd1d2f103b79002cd94b85a640a103f094227a2c4c53bc8af1fdbf4e13d9729e","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","3dce33e7eb25594863b8e615f14a45ab98190d85953436750644212d8a18c066","2b93035328f7778d200252681c1d86285d501ed424825a18f81e4c3028aa51d9","2ac9c8332c5f8510b8bdd571f8271e0f39b0577714d5e95c1e79a12b2616f069","42c21aa963e7b86fa00801d96e88b36803188018d5ad91db2a9101bccd40b3ff","d31eb848cdebb4c55b4893b335a7c0cca95ad66dee13cbb7d0893810c0a9c301","b9f96255e1048ed2ea33ec553122716f0e57fc1c3ad778e9aa15f5b46547bd23","7a9e0a564fee396cacf706523b5aeed96e04c6b871a8bebefad78499fbffc5bc","906c751ef5822ec0dadcea2f0e9db64a33fb4ee926cc9f7efa38afe5d5371b2a","5387c049e9702f2d2d7ece1a74836a14b47fbebe9bbeb19f94c580a37c855351","c68391fb9efad5d99ff332c65b1606248c4e4a9f1dd9a087204242b56c7126d6","e9cf02252d3a0ced987d24845dcb1f11c1be5541f17e5daa44c6de2d18138d0c","e8b02b879754d85f48489294f99147aeccc352c760d95a6fe2b6e49cd400b2fe","9f6908ab3d8a86c68b86e38578afc7095114e66b2fc36a2a96e9252aac3998e0","0eedb2344442b143ddcd788f87096961cd8572b64f10b4afc3356aa0460171c6","71405cc70f183d029cc5018375f6c35117ffdaf11846c35ebf85ee3956b1b2a6","c68baff4d8ba346130e9753cefe2e487a16731bf17e05fdacc81e8c9a26aae9d","2cd15528d8bb5d0453aa339b4b52e0696e8b07e790c153831c642c3dea5ac8af","479d622e66283ffa9883fbc33e441f7fc928b2277ff30aacbec7b7761b4e9579","ade307876dc5ca267ca308d09e737b611505e015c535863f22420a11fffc1c54","f8cdefa3e0dee639eccbe9794b46f90291e5fd3989fcba60d2f08fde56179fb9","86c5a62f99aac7053976e317dbe9acb2eaf903aaf3d2e5bb1cafe5c2df7b37a8","2b300954ce01a8343866f737656e13243e86e5baef51bd0631b21dcef1f6e954","a2d409a9ffd872d6b9d78ead00baa116bbc73cfa959fce9a2f29d3227876b2a1","b288936f560cd71f4a6002953290de9ff8dfbfbf37f5a9391be5c83322324898","61178a781ef82e0ff54f9430397e71e8f365fc1e3725e0e5346f2de7b0d50dfa","6a6ccb37feb3aad32d9be026a3337db195979cd5727a616fc0f557e974101a54","c649ea79205c029a02272ef55b7ab14ada0903db26144d2205021f24727ac7a3","38e2b02897c6357bbcff729ef84c736727b45cc152abe95a7567caccdfad2a1d","d6610ea7e0b1a7686dba062a1e5544dd7d34140f4545305b7c6afaebfb348341","3dee35db743bdba2c8d19aece7ac049bde6fa587e195d86547c882784e6ba34c","b15e55c5fa977c2f25ca0b1db52cfa2d1fd4bf0baf90a8b90d4a7678ca462ff1","f41d30972724714763a2698ae949fbc463afb203b5fa7c4ad7e4de0871129a17","843dd7b6a7c6269fd43827303f5cbe65c1fecabc30b4670a50d5a15d57daeeb9","f06d8b8567ee9fd799bf7f806efe93b67683ef24f4dea5b23ef12edff4434d9d","6017384f697ff38bc3ef6a546df5b230c3c31329db84cbfe686c83bec011e2b2","e1a5b30d9248549ca0c0bb1d653bafae20c64c4aa5928cc4cd3017b55c2177b0","a593632d5878f17295bd53e1c77f27bf4c15212822f764a2bfc1702f4b413fa0","a868a534ba1c2ca9060b8a13b0ffbbbf78b4be7b0ff80d8c75b02773f7192c29","da7545aba8f54a50fde23e2ede00158dc8112560d934cee58098dfb03aae9b9d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","a1a261624efb3a00ff346b13580f70f3463b8cdcc58b60f5793ff11785d52cab","f83b320cceccfc48457a818d18fc9a006ab18d0bdd727aa2c2e73dc1b4a45e98","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","b0d10e46cfe3f6c476b69af02eaa38e4ccc7430221ce3109ae84bb9fb8282298","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e","70e9a18da08294f75bf23e46c7d69e67634c0765d355887b9b41f0d959e1426e","ed44ba6b95f08b758748be7902e0cc54178b1337c56d0e2469c77b03f63ac73b"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[120,247],[120],[91,120,127,128,129,144],[120,128,129,145,146],[120,127,128],[120,127,144,147,150],[120,127,147,150,151],[120,148,149,150,152,153],[120,127,150],[120,127,144,147,148,149,152],[120,127,135],[120,127],[91,120,127],[80,120,127],[120,131,132,133,134,135,136,137,138,139,140,141,142,143],[120,127,133,134],[120,127,133,135],[120,166,224],[120,224,225],[120,224,225,226,227],[120,166],[120,198],[120,198,199,200],[64,120],[67,120],[64,67,120],[65,66,67,68,69,70,71,72,73,74,75,120,155,158,159,160,161,162,163,164,165],[58,64,65,120],[67,73,75,120,154],[120,157],[67,68,120],[64,120,161],[120,193,194],[120,247,248,249,250,251],[120,247,249],[120,156],[120,254,255,256],[92,120,127],[120,259],[120,260],[120,271],[120,265,270],[120,274,276,277,278,279,280,281,282,283,284,285,286],[120,274,275,277,278,279,280,281,282,283,284,285,286],[120,275,276,277,278,279,280,281,282,283,284,285,286],[120,274,275,276,278,279,280,281,282,283,284,285,286],[120,274,275,276,277,279,280,281,282,283,284,285,286],[120,274,275,276,277,278,280,281,282,283,284,285,286],[120,274,275,276,277,278,279,281,282,283,284,285,286],[120,274,275,276,277,278,279,280,282,283,284,285,286],[120,274,275,276,277,278,279,280,281,283,284,285,286],[120,274,275,276,277,278,279,280,281,282,284,285,286],[120,274,275,276,277,278,279,280,281,282,283,285,286],[120,274,275,276,277,278,279,280,281,282,283,284,286],[120,274,275,276,277,278,279,280,281,282,283,284,285],[76,120],[79,120],[80,85,111,120],[81,91,92,99,108,119,120],[81,82,91,99,120],[83,120],[84,85,92,100,120],[85,108,116,120],[86,88,91,99,120],[87,120],[88,89,120],[90,91,120],[91,120],[91,92,93,108,119,120],[91,92,93,108,120],[91,94,99,108,119,120],[91,92,94,95,99,108,116,119,120],[94,96,108,116,119,120],[76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126],[91,97,120],[98,119,120,124],[88,91,99,108,120],[100,120],[101,120],[79,102,120],[103,118,120,124],[104,120],[105,120],[91,106,120],[106,107,120,122],[80,91,108,109,110,120],[80,108,110,120],[108,109,120],[111,120],[112,120],[91,114,115,120],[114,115,120],[85,99,116,120],[117,120],[99,118,120],[80,94,105,119,120],[85,120],[108,120,121],[120,122],[120,123],[80,85,91,93,102,108,119,120,122,124],[108,120,125],[120,127,292],[120,295,334],[120,295,319,334],[120,334],[120,295],[120,295,320,334],[120,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333],[120,320,334],[120,335],[120,339],[120,203],[120,215,216,217],[120,203,214,215],[120,178],[120,178,179,180,181,182],[120,167,168,169,170,171,172,173,174,175,176,177],[120,263,266],[120,263,266,267,268],[120,265],[120,262,269],[120,264],[57,59,60,61,62,63,120],[57,58,120],[59,120],[58,59,120],[57,59,120],[120,166,187,228],[120,229,230],[120,166,183,184,185],[120,184],[56,120,184,185,186],[120,185],[120,188],[120,188,189,192,196],[120,195],[120,166,190,191],[120,211,212,213],[120,210,211],[120,166,210,211],[120,166,203,210],[120,166,204],[120,204,205,206,207,208,209],[120,166,203],[120,219],[120,202,219,221,222],[120,166,187,190,197,201,202,219,220],[120,166,197,214,218],[120,166,235],[120,166,228,235],[120,233,234,235,236,237,238,242],[120,166,210,243],[120,166,187,197,233,234,236],[120,166,187,197,231,232,233,235,236],[120,234,235,238],[120,239,240,241,243],[120,235,238],[120,166,235,238],[120,166,187,234],[120,166,210,235,236],[120,244,245],[120,183,187,201,223,243],[120,166,210,223,244],[244,245],[183,187,223,243],[166,210,223,244]],"referencedMap":[[249,1],[247,2],[145,3],[128,2],[147,4],[129,5],[146,2],[151,6],[152,7],[148,7],[154,8],[149,7],[153,9],[150,10],[136,11],[133,12],[140,13],[134,11],[131,14],[139,2],[144,15],[141,2],[142,2],[143,2],[138,12],[135,16],[132,2],[137,17],[190,2],[225,18],[227,2],[226,19],[228,20],[224,21],[203,13],[199,22],[200,22],[201,23],[198,2],[65,24],[66,24],[68,25],[69,24],[70,24],[71,26],[72,2],[73,2],[74,2],[67,24],[166,27],[75,28],[155,29],[158,30],[159,2],[160,2],[161,2],[162,2],[163,2],[164,31],[165,32],[193,2],[195,33],[194,2],[252,34],[248,1],[250,35],[251,1],[191,12],[157,36],[253,2],[254,2],[257,37],[255,2],[258,38],[259,2],[260,39],[261,40],[272,41],[271,42],[256,2],[273,2],[275,43],[276,44],[274,45],[277,46],[278,47],[279,48],[280,49],[281,50],[282,51],[283,52],[284,53],[285,54],[286,55],[287,2],[156,2],[76,56],[77,56],[79,57],[80,58],[81,59],[82,60],[83,61],[84,62],[85,63],[86,64],[87,65],[88,66],[89,66],[90,67],[91,68],[92,69],[93,70],[78,2],[126,2],[94,71],[95,72],[96,73],[127,74],[97,75],[98,76],[99,77],[100,78],[101,79],[102,80],[103,81],[104,82],[105,83],[106,84],[107,85],[108,86],[110,87],[109,88],[111,89],[112,90],[113,2],[114,91],[115,92],[116,93],[117,94],[118,95],[119,96],[120,97],[121,98],[122,99],[123,100],[124,101],[125,102],[288,2],[289,12],[290,2],[291,2],[293,103],[292,2],[294,12],[319,104],[320,105],[295,106],[298,106],[317,104],[318,104],[308,104],[307,107],[305,104],[300,104],[313,104],[311,104],[315,104],[299,104],[312,104],[316,104],[301,104],[302,104],[314,104],[296,104],[303,104],[304,104],[306,104],[310,104],[321,108],[309,104],[297,104],[334,109],[333,2],[328,108],[330,110],[329,108],[322,108],[323,108],[325,108],[327,108],[331,110],[332,110],[324,110],[326,110],[336,111],[335,2],[337,2],[338,2],[339,2],[340,112],[130,2],[262,2],[215,113],[218,114],[216,115],[217,115],[177,2],[174,116],[176,116],[175,116],[173,116],[183,117],[178,118],[182,2],[179,2],[181,2],[180,2],[169,116],[170,116],[171,116],[167,2],[168,2],[172,116],[263,2],[267,119],[269,120],[268,119],[266,121],[270,122],[265,123],[264,2],[57,2],[64,124],[59,125],[60,126],[61,126],[62,127],[63,127],[58,128],[8,2],[10,2],[9,2],[2,2],[11,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[3,2],[4,2],[22,2],[19,2],[20,2],[21,2],[23,2],[24,2],[25,2],[5,2],[26,2],[27,2],[28,2],[29,2],[6,2],[33,2],[30,2],[31,2],[32,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[229,129],[230,2],[231,130],[56,2],[186,131],[185,132],[187,133],[184,134],[189,135],[197,136],[196,137],[188,2],[192,138],[214,139],[212,140],[213,141],[211,142],[205,143],[206,143],[207,2],[208,143],[210,144],[204,145],[209,143],[202,2],[220,146],[222,146],[223,147],[221,148],[219,149],[236,150],[237,151],[243,152],[232,153],[235,154],[234,155],[239,156],[242,157],[240,158],[241,159],[233,160],[238,161],[246,162],[244,163],[245,164],[47,2],[48,2],[49,2],[50,2],[51,2],[52,2],[43,2],[53,2],[54,2],[55,2],[44,2],[45,2],[46,2]],"exportedModulesMap":[[249,1],[247,2],[145,3],[128,2],[147,4],[129,5],[146,2],[151,6],[152,7],[148,7],[154,8],[149,7],[153,9],[150,10],[136,11],[133,12],[140,13],[134,11],[131,14],[139,2],[144,15],[141,2],[142,2],[143,2],[138,12],[135,16],[132,2],[137,17],[190,2],[225,18],[227,2],[226,19],[228,20],[224,21],[203,13],[199,22],[200,22],[201,23],[198,2],[65,24],[66,24],[68,25],[69,24],[70,24],[71,26],[72,2],[73,2],[74,2],[67,24],[166,27],[75,28],[155,29],[158,30],[159,2],[160,2],[161,2],[162,2],[163,2],[164,31],[165,32],[193,2],[195,33],[194,2],[252,34],[248,1],[250,35],[251,1],[191,12],[157,36],[253,2],[254,2],[257,37],[255,2],[258,38],[259,2],[260,39],[261,40],[272,41],[271,42],[256,2],[273,2],[275,43],[276,44],[274,45],[277,46],[278,47],[279,48],[280,49],[281,50],[282,51],[283,52],[284,53],[285,54],[286,55],[287,2],[156,2],[76,56],[77,56],[79,57],[80,58],[81,59],[82,60],[83,61],[84,62],[85,63],[86,64],[87,65],[88,66],[89,66],[90,67],[91,68],[92,69],[93,70],[78,2],[126,2],[94,71],[95,72],[96,73],[127,74],[97,75],[98,76],[99,77],[100,78],[101,79],[102,80],[103,81],[104,82],[105,83],[106,84],[107,85],[108,86],[110,87],[109,88],[111,89],[112,90],[113,2],[114,91],[115,92],[116,93],[117,94],[118,95],[119,96],[120,97],[121,98],[122,99],[123,100],[124,101],[125,102],[288,2],[289,12],[290,2],[291,2],[293,103],[292,2],[294,12],[319,104],[320,105],[295,106],[298,106],[317,104],[318,104],[308,104],[307,107],[305,104],[300,104],[313,104],[311,104],[315,104],[299,104],[312,104],[316,104],[301,104],[302,104],[314,104],[296,104],[303,104],[304,104],[306,104],[310,104],[321,108],[309,104],[297,104],[334,109],[333,2],[328,108],[330,110],[329,108],[322,108],[323,108],[325,108],[327,108],[331,110],[332,110],[324,110],[326,110],[336,111],[335,2],[337,2],[338,2],[339,2],[340,112],[130,2],[262,2],[215,113],[218,114],[216,115],[217,115],[177,2],[174,116],[176,116],[175,116],[173,116],[183,117],[178,118],[182,2],[179,2],[181,2],[180,2],[169,116],[170,116],[171,116],[167,2],[168,2],[172,116],[263,2],[267,119],[269,120],[268,119],[266,121],[270,122],[265,123],[264,2],[57,2],[64,124],[59,125],[60,126],[61,126],[62,127],[63,127],[58,128],[8,2],[10,2],[9,2],[2,2],[11,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[3,2],[4,2],[22,2],[19,2],[20,2],[21,2],[23,2],[24,2],[25,2],[5,2],[26,2],[27,2],[28,2],[29,2],[6,2],[33,2],[30,2],[31,2],[32,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[229,129],[230,2],[231,130],[56,2],[186,131],[185,132],[187,133],[184,134],[189,135],[197,136],[196,137],[188,2],[192,138],[214,139],[212,140],[213,141],[211,142],[205,143],[206,143],[207,2],[208,143],[210,144],[204,145],[209,143],[202,2],[220,146],[222,146],[223,147],[221,148],[219,149],[236,150],[237,151],[243,152],[232,153],[235,154],[234,155],[239,156],[242,157],[240,158],[241,159],[233,160],[238,161],[246,165],[244,166],[245,167],[47,2],[48,2],[49,2],[50,2],[51,2],[52,2],[43,2],[53,2],[54,2],[55,2],[44,2],[45,2],[46,2]],"semanticDiagnosticsPerFile":[249,247,145,128,147,129,146,151,152,148,154,149,153,150,136,133,140,134,131,139,144,141,142,143,138,135,132,137,190,225,227,226,228,224,203,199,200,201,198,65,66,68,69,70,71,72,73,74,67,166,75,155,158,159,160,161,162,163,164,165,193,195,194,252,248,250,251,191,157,253,254,257,255,258,259,260,261,272,271,256,273,275,276,274,277,278,279,280,281,282,283,284,285,286,287,156,76,77,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,78,126,94,95,96,127,97,98,99,100,101,102,103,104,105,106,107,108,110,109,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,288,289,290,291,293,292,294,319,320,295,298,317,318,308,307,305,300,313,311,315,299,312,316,301,302,314,296,303,304,306,310,321,309,297,334,333,328,330,329,322,323,325,327,331,332,324,326,336,335,337,338,339,340,130,262,215,218,216,217,177,174,176,175,173,183,178,182,179,181,180,169,170,171,167,168,172,263,267,269,268,266,270,265,264,57,64,59,60,61,62,63,58,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,229,230,231,56,186,185,187,184,189,197,196,188,192,214,212,213,211,205,206,207,208,210,204,209,202,220,222,223,221,219,236,237,243,232,235,234,239,242,240,241,233,238,246,244,245,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -+{"program":{"fileNames":["../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.esnext.intl.d.ts","../../../types/eth-ens-namehash.d.ts","../../../types/ethereum-ens-network-map.d.ts","../../../types/global.d.ts","../../../types/single-call-balance-checker-abi.d.ts","../../../types/@metamask/contract-metadata.d.ts","../../../types/@metamask/eth-hd-keyring.d.ts","../../../types/@metamask/eth-simple-keyring.d.ts","../../../types/@metamask/ethjs-provider-http.d.ts","../../../types/@metamask/ethjs-unit.d.ts","../../../types/@metamask/metamask-eth-abis.d.ts","../../../types/eth-json-rpc-infura/src/createProvider.d.ts","../../../types/eth-phishing-detect/src/config.json.d.ts","../../../types/eth-phishing-detect/src/detector.d.ts","../../base-controller/dist/types/BaseControllerV1.d.ts","../../../node_modules/superstruct/dist/error.d.ts","../../../node_modules/superstruct/dist/utils.d.ts","../../../node_modules/superstruct/dist/struct.d.ts","../../../node_modules/superstruct/dist/structs/coercions.d.ts","../../../node_modules/superstruct/dist/structs/refinements.d.ts","../../../node_modules/superstruct/dist/structs/types.d.ts","../../../node_modules/superstruct/dist/structs/utilities.d.ts","../../../node_modules/superstruct/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/assert.d.ts","../../../node_modules/@metamask/utils/dist/types/base64.d.ts","../../../node_modules/@metamask/utils/dist/types/hex.d.ts","../../../node_modules/@metamask/utils/dist/types/bytes.d.ts","../../../node_modules/@metamask/utils/dist/types/caip-types.d.ts","../../../node_modules/@metamask/utils/dist/types/checksum.d.ts","../../../node_modules/@metamask/utils/dist/types/coercers.d.ts","../../../node_modules/@metamask/utils/dist/types/collections.d.ts","../../../node_modules/@metamask/utils/dist/types/encryption-types.d.ts","../../../node_modules/@metamask/utils/dist/types/errors.d.ts","../../../node_modules/@metamask/utils/dist/types/json.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/globals.global.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/@ethereumjs/common/dist/enums.d.ts","../../../node_modules/@ethereumjs/common/dist/types.d.ts","../../../node_modules/buffer/index.d.ts","../../../node_modules/@ethereumjs/util/dist/constants.d.ts","../../../node_modules/@ethereumjs/util/dist/units.d.ts","../../../node_modules/@ethereumjs/util/dist/address.d.ts","../../../node_modules/@ethereumjs/util/dist/bytes.d.ts","../../../node_modules/@ethereumjs/util/dist/types.d.ts","../../../node_modules/@ethereumjs/util/dist/account.d.ts","../../../node_modules/@ethereumjs/util/dist/withdrawal.d.ts","../../../node_modules/@ethereumjs/util/dist/signature.d.ts","../../../node_modules/@ethereumjs/util/dist/encoding.d.ts","../../../node_modules/@ethereumjs/util/dist/asyncEventEmitter.d.ts","../../../node_modules/@ethereumjs/util/dist/internal.d.ts","../../../node_modules/@ethereumjs/util/dist/lock.d.ts","../../../node_modules/@ethereumjs/util/dist/provider.d.ts","../../../node_modules/@ethereumjs/util/dist/index.d.ts","../../../node_modules/@ethereumjs/common/dist/common.d.ts","../../../node_modules/@ethereumjs/common/dist/utils.d.ts","../../../node_modules/@ethereumjs/common/dist/index.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip2930Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/legacyTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/types.d.ts","../../../node_modules/@ethereumjs/tx/dist/baseTransaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/eip1559Transaction.d.ts","../../../node_modules/@ethereumjs/tx/dist/transactionFactory.d.ts","../../../node_modules/@ethereumjs/tx/dist/index.d.ts","../../../node_modules/@metamask/utils/dist/types/keyring.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@metamask/utils/dist/types/logging.d.ts","../../../node_modules/@metamask/utils/dist/types/misc.d.ts","../../../node_modules/@metamask/utils/dist/types/number.d.ts","../../../node_modules/@metamask/utils/dist/types/opaque.d.ts","../../../node_modules/@metamask/utils/dist/types/promise.d.ts","../../../node_modules/@metamask/utils/dist/types/time.d.ts","../../../node_modules/@metamask/utils/dist/types/transaction-types.d.ts","../../../node_modules/@metamask/utils/dist/types/versions.d.ts","../../../node_modules/@metamask/utils/dist/types/index.d.ts","../../../node_modules/immer/dist/utils/env.d.ts","../../../node_modules/immer/dist/utils/errors.d.ts","../../../node_modules/immer/dist/types/types-external.d.ts","../../../node_modules/immer/dist/types/types-internal.d.ts","../../../node_modules/immer/dist/utils/common.d.ts","../../../node_modules/immer/dist/utils/plugins.d.ts","../../../node_modules/immer/dist/core/scope.d.ts","../../../node_modules/immer/dist/core/finalize.d.ts","../../../node_modules/immer/dist/core/proxy.d.ts","../../../node_modules/immer/dist/core/immerClass.d.ts","../../../node_modules/immer/dist/core/current.d.ts","../../../node_modules/immer/dist/internal.d.ts","../../../node_modules/immer/dist/plugins/es5.d.ts","../../../node_modules/immer/dist/plugins/patches.d.ts","../../../node_modules/immer/dist/plugins/mapset.d.ts","../../../node_modules/immer/dist/plugins/all.d.ts","../../../node_modules/immer/dist/immer.d.ts","../../base-controller/dist/types/RestrictedControllerMessenger.d.ts","../../base-controller/dist/types/ControllerMessenger.d.ts","../../base-controller/dist/types/BaseControllerV2.d.ts","../../base-controller/dist/types/index.d.ts","../../controller-utils/dist/types/types.d.ts","../../controller-utils/dist/types/constants.d.ts","../../../node_modules/@metamask/eth-query/index.d.ts","../../../node_modules/@types/bn.js/index.d.ts","../../controller-utils/dist/types/util.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/abnf.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/utils.d.ts","../../../node_modules/@spruceid/siwe-parser/dist/parsers.d.ts","../../controller-utils/dist/types/siwe.d.ts","../../controller-utils/dist/types/index.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/types.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createEventEmitterProxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/createSwappableProxy.d.ts","../../../node_modules/@metamask/swappable-obj-proxy/dist/index.d.ts","../../network-controller/dist/types/constants.d.ts","../../../node_modules/@metamask/safe-event-emitter/index.d.ts","../../json-rpc-engine/dist/types/JsonRpcEngine.d.ts","../../json-rpc-engine/dist/types/createAsyncMiddleware.d.ts","../../json-rpc-engine/dist/types/createScaffoldMiddleware.d.ts","../../json-rpc-engine/dist/types/getUniqueId.d.ts","../../json-rpc-engine/dist/types/idRemapMiddleware.d.ts","../../json-rpc-engine/dist/types/mergeMiddleware.d.ts","../../json-rpc-engine/dist/types/index.d.ts","../../eth-json-rpc-provider/dist/types/safe-event-emitter-provider.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-engine.d.ts","../../eth-json-rpc-provider/dist/types/provider-from-middleware.d.ts","../../eth-json-rpc-provider/dist/types/index.d.ts","../../../node_modules/eth-block-tracker/dist/BlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/PollingBlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/SubscribeBlockTracker.d.ts","../../../node_modules/eth-block-tracker/dist/index.d.ts","../../network-controller/dist/types/types.d.ts","../../network-controller/dist/types/create-auto-managed-network-client.d.ts","../../network-controller/dist/types/NetworkController.d.ts","../../network-controller/dist/types/create-network-client.d.ts","../../network-controller/dist/types/index.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/utils.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/classes.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/errors.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/error-constants.d.ts","../../../node_modules/@metamask/rpc-errors/dist/types/index.d.ts","../../approval-controller/dist/types/ApprovalController.d.ts","../../approval-controller/dist/types/errors.d.ts","../../approval-controller/dist/types/index.d.ts","../../permission-controller/dist/types/permission-middleware.d.ts","../../permission-controller/dist/types/SubjectMetadataController.d.ts","../../permission-controller/dist/types/PermissionController.d.ts","../../permission-controller/dist/types/Permission.d.ts","../../permission-controller/dist/types/Caveat.d.ts","../../permission-controller/dist/types/errors.d.ts","../../permission-controller/dist/types/utils.d.ts","../../permission-controller/dist/types/rpc-methods/getPermissions.d.ts","../../permission-controller/dist/types/rpc-methods/requestPermissions.d.ts","../../permission-controller/dist/types/rpc-methods/revokePermissions.d.ts","../../permission-controller/dist/types/rpc-methods/index.d.ts","../../permission-controller/dist/types/index.d.ts","../src/SelectedNetworkController.ts","../src/SelectedNetworkMiddleware.ts","../src/index.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@types/deep-freeze-strict/index.d.ts","../../../node_modules/@types/eslint/helpers.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/eslint/index.d.ts","../../../node_modules/@types/graceful-fs/index.d.ts","../../../node_modules/@types/istanbul-lib-coverage/index.d.ts","../../../node_modules/@types/istanbul-lib-report/index.d.ts","../../../node_modules/@types/istanbul-reports/index.d.ts","../../../node_modules/chalk/index.d.ts","../../../node_modules/jest-diff/build/cleanupSemantic.d.ts","../../../node_modules/pretty-format/build/types.d.ts","../../../node_modules/pretty-format/build/index.d.ts","../../../node_modules/jest-diff/build/types.d.ts","../../../node_modules/jest-diff/build/diffLines.d.ts","../../../node_modules/jest-diff/build/printDiffs.d.ts","../../../node_modules/jest-diff/build/index.d.ts","../../../node_modules/jest-matcher-utils/build/index.d.ts","../../../node_modules/@types/jest/index.d.ts","../../../node_modules/@types/jest-when/index.d.ts","../../../node_modules/@types/json5/index.d.ts","../../../node_modules/@types/lodash/common/common.d.ts","../../../node_modules/@types/lodash/common/array.d.ts","../../../node_modules/@types/lodash/common/collection.d.ts","../../../node_modules/@types/lodash/common/date.d.ts","../../../node_modules/@types/lodash/common/function.d.ts","../../../node_modules/@types/lodash/common/lang.d.ts","../../../node_modules/@types/lodash/common/math.d.ts","../../../node_modules/@types/lodash/common/number.d.ts","../../../node_modules/@types/lodash/common/object.d.ts","../../../node_modules/@types/lodash/common/seq.d.ts","../../../node_modules/@types/lodash/common/string.d.ts","../../../node_modules/@types/lodash/common/util.d.ts","../../../node_modules/@types/lodash/index.d.ts","../../../node_modules/@types/minimatch/index.d.ts","../../../node_modules/@types/parse-json/index.d.ts","../../../node_modules/@types/pbkdf2/index.d.ts","../../../node_modules/@types/prettier/index.d.ts","../../../node_modules/@types/punycode/index.d.ts","../../../node_modules/@types/readable-stream/node_modules/safe-buffer/index.d.ts","../../../node_modules/@types/readable-stream/index.d.ts","../../../node_modules/@types/secp256k1/index.d.ts","../../../node_modules/@types/semver/classes/semver.d.ts","../../../node_modules/@types/semver/functions/parse.d.ts","../../../node_modules/@types/semver/functions/valid.d.ts","../../../node_modules/@types/semver/functions/clean.d.ts","../../../node_modules/@types/semver/functions/inc.d.ts","../../../node_modules/@types/semver/functions/diff.d.ts","../../../node_modules/@types/semver/functions/major.d.ts","../../../node_modules/@types/semver/functions/minor.d.ts","../../../node_modules/@types/semver/functions/patch.d.ts","../../../node_modules/@types/semver/functions/prerelease.d.ts","../../../node_modules/@types/semver/functions/compare.d.ts","../../../node_modules/@types/semver/functions/rcompare.d.ts","../../../node_modules/@types/semver/functions/compare-loose.d.ts","../../../node_modules/@types/semver/functions/compare-build.d.ts","../../../node_modules/@types/semver/functions/sort.d.ts","../../../node_modules/@types/semver/functions/rsort.d.ts","../../../node_modules/@types/semver/functions/gt.d.ts","../../../node_modules/@types/semver/functions/lt.d.ts","../../../node_modules/@types/semver/functions/eq.d.ts","../../../node_modules/@types/semver/functions/neq.d.ts","../../../node_modules/@types/semver/functions/gte.d.ts","../../../node_modules/@types/semver/functions/lte.d.ts","../../../node_modules/@types/semver/functions/cmp.d.ts","../../../node_modules/@types/semver/functions/coerce.d.ts","../../../node_modules/@types/semver/classes/comparator.d.ts","../../../node_modules/@types/semver/classes/range.d.ts","../../../node_modules/@types/semver/functions/satisfies.d.ts","../../../node_modules/@types/semver/ranges/max-satisfying.d.ts","../../../node_modules/@types/semver/ranges/min-satisfying.d.ts","../../../node_modules/@types/semver/ranges/to-comparators.d.ts","../../../node_modules/@types/semver/ranges/min-version.d.ts","../../../node_modules/@types/semver/ranges/valid.d.ts","../../../node_modules/@types/semver/ranges/outside.d.ts","../../../node_modules/@types/semver/ranges/gtr.d.ts","../../../node_modules/@types/semver/ranges/ltr.d.ts","../../../node_modules/@types/semver/ranges/intersects.d.ts","../../../node_modules/@types/semver/ranges/simplify.d.ts","../../../node_modules/@types/semver/ranges/subset.d.ts","../../../node_modules/@types/semver/internals/identifiers.d.ts","../../../node_modules/@types/semver/index.d.ts","../../../node_modules/@types/sinonjs__fake-timers/index.d.ts","../../../node_modules/@types/sinon/index.d.ts","../../../node_modules/@types/stack-utils/index.d.ts","../../../node_modules/@types/uuid/index.d.ts","../../../node_modules/@types/yargs-parser/index.d.ts","../../../node_modules/@types/yargs/index.d.ts"],"fileInfos":[{"version":"8730f4bf322026ff5229336391a18bcaa1f94d4f82416c8b2f3954e2ccaae2ba","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","4b421cbfb3a38a27c279dec1e9112c3d1da296f77a1a85ddadf7e7a425d45d18","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"3aafcb693fe5b5c3bd277bd4c3a617b53db474fe498fc5df067c5603b1eebde7","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"5f406584aef28a331c36523df688ca3650288d14f39c5d2e555c95f0d2ff8f6f","affectsGlobalScope":true},{"version":"22f230e544b35349cfb3bd9110b6ef37b41c6d6c43c3314a31bd0d9652fcec72","affectsGlobalScope":true},{"version":"7ea0b55f6b315cf9ac2ad622b0a7813315bb6e97bf4bb3fbf8f8affbca7dc695","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"eb26de841c52236d8222f87e9e6a235332e0788af8c87a71e9e210314300410a","affectsGlobalScope":true},{"version":"3be5a1453daa63e031d266bf342f3943603873d890ab8b9ada95e22389389006","affectsGlobalScope":true},{"version":"17bb1fc99591b00515502d264fa55dc8370c45c5298f4a5c2083557dccba5a2a","affectsGlobalScope":true},{"version":"7ce9f0bde3307ca1f944119f6365f2d776d281a393b576a18a2f2893a2d75c98","affectsGlobalScope":true},{"version":"6a6b173e739a6a99629a8594bfb294cc7329bfb7b227f12e1f7c11bc163b8577","affectsGlobalScope":true},{"version":"81cac4cbc92c0c839c70f8ffb94eb61e2d32dc1c3cf6d95844ca099463cf37ea","affectsGlobalScope":true},{"version":"b0124885ef82641903d232172577f2ceb5d3e60aed4da1153bab4221e1f6dd4e","affectsGlobalScope":true},{"version":"0eb85d6c590b0d577919a79e0084fa1744c1beba6fd0d4e951432fa1ede5510a","affectsGlobalScope":true},{"version":"da233fc1c8a377ba9e0bed690a73c290d843c2c3d23a7bd7ec5cd3d7d73ba1e0","affectsGlobalScope":true},{"version":"d154ea5bb7f7f9001ed9153e876b2d5b8f5c2bb9ec02b3ae0d239ec769f1f2ae","affectsGlobalScope":true},{"version":"bb2d3fb05a1d2ffbca947cc7cbc95d23e1d053d6595391bd325deb265a18d36c","affectsGlobalScope":true},{"version":"c80df75850fea5caa2afe43b9949338ce4e2de086f91713e9af1a06f973872b8","affectsGlobalScope":true},{"version":"9d57b2b5d15838ed094aa9ff1299eecef40b190722eb619bac4616657a05f951","affectsGlobalScope":true},{"version":"6c51b5dd26a2c31dbf37f00cfc32b2aa6a92e19c995aefb5b97a3a64f1ac99de","affectsGlobalScope":true},{"version":"6e7997ef61de3132e4d4b2250e75343f487903ddf5370e7ce33cf1b9db9a63ed","affectsGlobalScope":true},{"version":"2ad234885a4240522efccd77de6c7d99eecf9b4de0914adb9a35c0c22433f993","affectsGlobalScope":true},{"version":"5e5e095c4470c8bab227dbbc61374878ecead104c74ab9960d3adcccfee23205","affectsGlobalScope":true},{"version":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"2768ef564cfc0689a1b76106c421a2909bdff0acbe87da010785adab80efdd5c","affectsGlobalScope":true},{"version":"b248e32ca52e8f5571390a4142558ae4f203ae2f94d5bac38a3084d529ef4e58","affectsGlobalScope":true},{"version":"52d1bb7ab7a3306fd0375c8bff560feed26ed676a5b0457fa8027b563aecb9a4","affectsGlobalScope":true},"70bbfaec021ac4a0c805374225b55d70887f987df8b8dd7711d79464bb7b4385","869089d60b67219f63e6aca810284c89bae1b384b5cbc7ce64e53d82ad223ed5",{"version":"18338b6a4b920ec7d49b4ffafcbf0fa8a86b4bfd432966efd722dab611157cf4","affectsGlobalScope":true},"62a0875a0397b35a2364f1d401c0ce17975dfa4d47bf6844de858ae04da349f9","ee7491d0318d1fafcba97d5b72b450eb52671570f7a4ecd9e8898d40eaae9472","e3e7d217d89b380c1f34395eadc9289542851b0f0a64007dfe1fb7cf7423d24e","fd79909e93b4d50fd0ed9f3d39ddf8ba0653290bac25c295aac49f6befbd081b","345a9cc2945406f53051cd0e9b51f82e1e53929848eab046fdda91ee8aa7da31","9debe2de883da37a914e5e784a7be54c201b8f1d783822ad6f443ff409a5ea21","dee5d5c5440cda1f3668f11809a5503c30db0476ad117dd450f7ba5a45300e8f","f5e396c1424c391078c866d6f84afe0b4d2f7f85a160b9c756cd63b5b1775d93","5caa6f4fff16066d377d4e254f6c34c16540da3809cd66cd626a303bc33c419f","730d055528bdf12c8524870bb33d237991be9084c57634e56e5d8075f6605e02","75b22c74010ba649de1a1676a4c4b8b5bb4294fecd05089e2094429b16d7840c","e475453e7140e95542332943d3052fe4c7430ad1efce42b3e9157f1fee8cbc5f","ebfdf904255ce746c9d30117c2edef355fb19bf7650478d2405f39f0e4f302e6","f3f63b48addb8e2ea9d20bb671c3c306413b3daa39996d0ae52f63d8e32158e1","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","b4000a0a525fa921e896cbdb32ae802c9684f0fd371b5fc69e7310f7918cc2c3","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","b3d48529ae61dc27d0bfbfa2cb3e0dff8189644bd155bdf5df1e8e14669f7043","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","34693fb4a5e771e11668219221344dd1bd7d8b77ed005a1c1d965fb559be8406","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"e44ea2d6b7b853f6c81482416db43dafc11944561b810e469ae423085511ce7e","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"a7289d79eb84a59d2475b4d0136b4404be3cfdd17c3ea46b9194add1d645df01","affectsGlobalScope":true},"0bb26fa2a90ee890eed57ee812c71fa84d3d07850163ec4a204de86412cc57c1","132ca47da601c60141dd6f10bd08c70d0620177e5638439df2464ec3945b6d98",{"version":"55d2bbae076fed7269c3e16faeb32f988f558427b7a1c3bf04aa7551ab86ae90","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","cf83847c9264dcd592b6c89c1542925b899b277228687f3638614e3fa784cf76","3a41ebe7f089d50f447466b35b6cabb8b584c0994fc9809d0cd0a4ebc41e1239","7693b90b3075deaccafd5efb467bf9f2b747a3075be888652ef73e64396d8628","0c42d6cba77d9ad1cf45256ccb8489aa502fe2dbee1ec9048a29d49f5d532e73","2cf89c17245db65d175d4ef699cd68187516f9b3ae5c572fc0b9ad60f35dc223","5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"a34d65f61ec5aac5b53502c8b0bd4e00d217bccb95bf94d449e2571baa11fb8c","affectsGlobalScope":true},"8d42e5af5fb0a96a77e135ce84cc60636c9bad39d9dba043a4efe9d1bdeb3cc3","56fcc451e9065eb121c9cc4c1b9994a816306f3b0b3b1fce7ad59f0ac97a9999","8a6f12b74d3e6c4f5e1b918cb8e64ae16bc6756cf0d48bcc28a28e1bf26ca0cd","c3759b5bc5cc40f5988d86a497741a80fa91258629ae50a2b3735e774cd377cc","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","45dd82fb5aea9b12b2a90b427b28f3a014e8b2ee9b74087a5ab882841cb5fbc5",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"48b2f9302651eb31acd5be69bb4e6b35797a7fcd6b77391d10a4ccadf7dc3609","0c8c917ef15498c827bd494a0ef365e9f76deb211f8acbb86932e20489310788","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"9cdc2c6144b03822c9842505d09945bcf813b86827fdb260dd7586b63abc19bf","affectsGlobalScope":true},{"version":"2923dee3c897f03e91b54a210cdbefea7290562f0ac4b948667d4c9ee844b79e","affectsGlobalScope":true},"79169698d09a2be54b14f3bcad2575b414bf3525063fde0a1e4fcd5d6efd380e","051d939bcf77caa3cef3282708ab3a6fdfb741a7366e1d74a9e7603b67417ec3","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","a234d62ae81d012ebf23898a45672edf3e5c93ecf5a438a42b96c08dd68cde43","269929a24b2816343a178008ac9ae9248304d92a8ba8e233055e0ed6dbe6ef71","09ed02a725db002693236b6dfc49b2c6eb5557be1421d7fbe4f07cfe38211d92","09d801ff4a303d4976d4b9cb94af3a9097c4a70345e662d176975872d2998e51","c8558b01389b5f7610ac293aa612ccea2ae64d83af43b49f8142f190be1f414c","c40fdf7b2e18df49ce0568e37f0292c12807a0748be79e272745e7216bed2606",{"version":"b10b426c56e220b5093bf8a2446ee47af47263b7b1a03f4b18e42326b231b111","affectsGlobalScope":true},"4e228e78c1e9b0a75c70588d59288f63a6258e8b1fe4a67b0c53fe03461421d9","b4635ef36bee17e1304337d591c3b6b461ecdbc1876d0effbe6a581e62201fe5","205d50c24359ead003dc537b9b65d2a64208dfdffe368f403cf9e0357831db9e","1265fddcd0c68be9d2a3b29805d0280484c961264dd95e0b675f7bd91f777e78",{"version":"e4507242542bd499238f693d88b2d32e22177cc508854625f87bcc9bc3fa1256","affectsGlobalScope":true},{"version":"d942354e4966a98d3a92d1b1af0b4ac06f33af3f88116743e2c304c027ca26ef","affectsGlobalScope":true},"39f0808e5be3cb38674726c21fe2eb453c55e48a901679b4ce30fef85549b892","6afd66a7432ef100027ea110449e874196381e019e30eda7e7d8ca390366b7a8","befb8a9a78ac99d8fbc3ed392810489a7b90760c7a58934e8f1c8538f581cff3","e670bdf01540d35c170fae68edfd2f288eff909936780c379d6a9103b787b22c","867f95abf1df444aab146b19847391fc2f922a55f6a970a27ed8226766cee29f",{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true},"88003d9ab15507806f41b120be6d407c1afe566c2f6689ebe3a034dd5ec0c8dc","175323e2a79a6076e0bada8a390d535a3ea817158bf1b1f46e31efca9028a0a2","7a10053aadc19335532a4d02756db4865974fd69bea5439ddcc5bfdf062d9476","4967529644e391115ca5592184d4b63980569adf60ee685f968fd59ab1557188","aed9e712a9b168345362e8f3a949f16c99ca1e05d21328f05735dfdbb24414ef","b04fe6922ed3db93afdbd49cdda8576aa75f744592fceea96fb0d5f32158c4f5","ed8d6c8de90fc2a4faaebc28e91f2469928738efd5208fb75ade0fa607e892b7","d7c52b198d680fe65b1a8d1b001f0173ffa2536ca2e7082431d726ce1f6714cd","c07f251e1c4e415a838e5498380b55cfea94f3513229de292d2aa85ae52fc3e9","0ed401424892d6bf294a5374efe512d6951b54a71e5dd0290c55b6d0d915f6f7","b945be6da6a3616ef3a250bfe223362b1c7c6872e775b0c4d82a1bf7a28ff902","beea49237dd7c7110fabf3c7509919c9cb9da841d847c53cac162dc3479e2f87","0f45f8a529c450d8f394106cc622bff79e44a1716e1ac9c3cc68b43f7ecf65ee","c624ce90b04c27ce4f318ba6330d39bde3d4e306f0f497ce78d4bda5ab8e22ca","9b8253aa5cb2c82d505f72afdbf96e83b15cc6b9a6f4fadbbbab46210d5f1977","86a8f52e4b1ac49155e889376bcfa8528a634c90c27fec65aa0e949f77b740c5","aab5dd41c1e2316cc0b42a7dd15684f8582d5a1d16c0516276a2a8a7d0fecd9c","59948226626ee210045296ba1fc6cb0fe748d1ff613204e08e7157ab6862dee7","ec3e54d8b713c170fdc8110a7e4a6a97513a7ab6b05ac9e1100cb064d2bb7349","43beb30ecb39a603fde4376554887310b0699f25f7f39c5c91e3147b51bb3a26","666b77d7f06f49da114b090a399abbfa66d5b6c01a3fd9dc4f063a52ace28507","31997714a93fbc570f52d47d6a8ebfb021a34a68ea9ba58bbb69cdec9565657e","6032e4262822160128e644de3fc4410bcd7517c2f137525fd2623d2bb23cb0d3","8bd5c9b1016629c144fd228983395b9dbf0676a576716bc3d316cab612c33cd5","2ed90bd3925b23aed8f859ffd0e885250be0424ca2b57e9866dabef152e1d6b7","93f6bd17d92dab9db7897e1430a5aeaa03bcf51623156213d8397710367a76ce","3f62b770a42e8c47c7008726f95aa383e69d97e85e680d237b99fcb0ee601dd8","5b84cfe78028c35c3bb89c042f18bf08d09da11e82d275c378ae4d07d8477e6c","980d21b0081cbf81774083b1e3a46f4bbdcd2b68858df0f66d7fad9c82bc34bc","6a9c5127096b35264eb7cd21b2417bfc1d42cceca9ba4ce2bb0c3410b7816042","93b7325b49dfbf613d940ed0e471216657b2d77459dac34f1b5b1678f08f884c","b17f3bb7d8333479c7e45e5f3d876761b9bca58f97594eca3f6a944fd825e632","3c1f1236cce6d6e0c4e2c1b4371e6f72d7c14842ecd76a98ed0748ee5730c8f3","6d7f58d5ea72d7834946fd7104a734dc7d40661be8b2e1eaced1ddce3268ebaf","4c26222991e6c97d5a8f541d4f2c67585eda9e8b33cf9f52931b098045236e88","277983d414aa99d78655186c3ee1e1c38c302e336aff1d77b47fcdc39d8273fe","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","ca3353cc82b1981f0d25d71d7432d583a6ef882ccdea82d65fbe49af37be51cb","50679a8e27aacf72f8c40bcab15d7ef5e83494089b4726b83eec4554344d5cdc","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","5d697a4b315cc5bb3042ae869abffd10c3b0d7b182cda0e4c45d8819937e5796","563fa27fdaec8f195b84f71a7af0ef48d30d5cc830575db86da86a63a470c8e6","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","299567f84bfedd1468dca2755a829cb19e607a6811673788807dc8921e211bc9","795d9fb85aad92221504db74dd179b506bd189bba0c104426f7e7bb8a66ffee5","1311bc194e0a69fe61031e852c1c0b439e2a2a3d1d5e2d8ff795499b9f283459","4b7ce19369d7e7fae76720c2c6c7f671bf3fa0f7093edb864f1ac358ca7c456c","c972ef44deca1fa8fab465915ffa00f82e126aacf3dfc8979c03b1b066ce5bb6","30285a1011c6d6b52f3ba3abb0a984be8148c05cdefb8eb6eb562335a3991f35","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","d6e6620a30d582182acc3f0a992a0c311adc589f111096aea11ab83fc09a5ccc","6213b8f686f56beab22b59a0f468590fd3a4c5fa931236a017efeca91d7c9584","c451cec9a588b1f105a5ea2c6063d4fca112b9d70105cacdadda0e1ef67e9379","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","a12eaa942232703a8a8477a2f240ad5a2c26c595012ea8f128224e77984099c4","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","8129a34006218a6f3cdc81bbd438d5429eb18b08b4338a26977ac3b4df129d75","30d2170e1a718b5035611af55e3618b4ba8f42f0749bb52ee593da6082c4e2ce","98ef38666d88ec9699a722053e07ede65d3042f693fe7ff8c786e53dbb6fd43b","a3b8b6be7620897d1e481e8650c980a210a138fceb6e710eaf95fd9dd0dfe94a","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","9f9e5bae412fa5909fae636d6733aee27a108cc2ed5b13980611016336774d3c","662fe197bba64bd3f17ee118058cd2d0d2dbe33d7c0c865fd6365d90bfc44e1e","030519c351f800551cac2658038804969ca4584d2c0175a710602ac234ca1340","0278a6939ca83cd040b08ff8c5fc7838b6693ddc52f22526bf158e6b10e0246c","c2d6206e5ba4fd3063b01218c2b3b997afc1cfbeb49fcee991fa8595842ce53d","29c188a2c660f99f1b4835022e011c4268d7af989d4b7dda33c0a69ca1a777f8","1ed0bf138e87912d741e28333b58cbf814ae863783b3b404d2454cbabb9c5fc0","3452ee7d8ef0b1bbd47b2a56924a1dc3c79dc84a19d212e9dc496f92e4943aa0","99510e20e3d4816e283e59e8f0f31f603b2f026648240ffdb1ca9f24be678419","037d1fbeb96dc35600814be14d0fbf31acf35f1d7b443ea33514937de69c2bf2","64d1859b7dd9f419ba08e064c2b16b1a5edde0316d6c2bb1833c9381d4dffc3d","69b0f96ca137c0dba05f321a159141ad36f79cfba2fffcc29d131e280275e6f2","7bb64cf513a2b1cf7a94f81ca201f3d76a9b17af556f0cfc4e2707443e6caa66","a9add2a29da4cd0b617ae89f196b3f2172a031aeb086922cdf097236eef8b008","99afac3e6e683ee3111e499f9919953e9489cb39cad74363717aa3805e91db51","c49f92b83968f4ee0b6026396a9b6e2d6fee8b660d08a90efb03355ce3433a7a","4fdc6afe4d7ef6aeb32ac0818d47e99f98a31d0696abc4cb2af489c78ac1ba1d","d73d5a0e854037d43781b2d5d33f4b95ee509e0ddede677aade79fbee6a97cdc","35d14e1ae04be300828b1a1614316b9312a009cfd5e29fa56f94c2a9f60b12df","d160fe745f9c3b72d7b9036fdb2b6b500a520d43e36bb842c927b6fe59ea2c23",{"version":"4a6571cadd05e8087a34f7ca44f63a8f61aa5fcca650f0a577febc4877b3c34f","signature":"3a4f9e7087c566703447928d15c234bd0bcc63a2a50b6ec39ed37c9bc0342310"},{"version":"506bcad28e45e13c23c2c25c9691e0dc42d8755a0e24b9a48586f51b5bebae8f","signature":"3a12771c76c5ed979438dc0e390224bd3d8661bcbea18114f77c4c6d9de5b8b2"},{"version":"6fbdb35bc3b9cfb225c48108b5bacb5c0d67bbcb677cf13b912e15a852551023","signature":"ee06131adf64c6cd201de36563174cff0e160b81438be186a7bd85e6c2b54fa7"},"a5aaeca001d2f69093d04aac4db321e4c338fd9b20cbc4f0b0af3dc6ae0f235b","cc957354aa3c94c9961ebf46282cfde1e81d107fc5785a61f62c67f1dd3ac2eb","8041cfce439ff29d339742389de04c136e3029d6b1817f07b2d7fcbfb7534990","93de1c6dab503f053efe8d304cb522bb3a89feab8c98f307a674a4fae04773e9","29a46d003ca3c721e6405f00dee7e3de91b14e09701eba5d887bf76fb2d47d38","069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","9990f9e566bc3c2c6e38df81294fb756e7f5b7b0e5bb17ab75384e190548b4b6",{"version":"64d4b35c5456adf258d2cf56c341e203a073253f229ef3208fc0d5020253b241","affectsGlobalScope":true},"ee7d8894904b465b072be0d2e4b45cf6b887cdba16a467645c4e200982ece7ea","f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","df95e00612c1faa5e0e7ef0dba589b18665bbeb3221db2b6cee1fe4d0e61921f","afe73051ff6a03a9565cbd8ebb0e956ee3df5e913ad5c1ded64218aabfa3dcb5","8b06ac3faeacb8484d84ddb44571d8f410697f98d7bfa86c0fda60373a9f5215","7eb06594824ada538b1d8b48c3925a83e7db792f47a081a62cf3e5c4e23cf0ee","f5638f7c2f12a9a1a57b5c41b3c1ea7db3876c003bab68e6a57afd6bcc169af0","0d14fa22c41fdc7277e6f71473b20ebc07f40f00e38875142335d5b63cdfc9d2","d8aab31ba8e618cc3eea10b0945de81cb93b7e8150a013a482332263b9305322","462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","7adecb2c3238794c378d336a8182d4c3dd2c4fa6fa1785e2797a3db550edea62","dc12dc0e5aa06f4e1a7692149b78f89116af823b9e1f1e4eae140cd3e0e674e6","1bfc6565b90c8771615cd8cfcf9b36efc0275e5e83ac7d9181307e96eb495161","8a8a96898906f065f296665e411f51010b51372fa260d5373bf9f64356703190","7f82ef88bdb67d9a850dd1c7cd2d690f33e0f0acd208e3c9eba086f3670d4f73",{"version":"ccfd8774cd9b929f63ff7dcf657977eb0652e3547f1fcac1b3a1dc5db22d4d58","affectsGlobalScope":true},"d92dc90fecd2552db74d8dc3c6fb4db9145b2aa0efe2c127236ba035969068d4","96d14f21b7652903852eef49379d04dbda28c16ed36468f8c9fa08f7c14c9538","675e702f2032766a91eeadee64f51014c64688525da99dccd8178f0c599f13a8","458111fc89d11d2151277c822dfdc1a28fa5b6b2493cf942e37d4cd0a6ee5f22","19c816167e076e7c24f074389c6cf3ed87bdbb917d1ea439ca281f9d26db2439","187119ff4f9553676a884e296089e131e8cc01691c546273b1d0089c3533ce42","febf0b2de54781102b00f61653b21377390a048fbf5262718c91860d11ff34a6","98f9d826db9cd99d27a01a59ee5f22863df00ccf1aaf43e1d7db80ebf716f7c3","0aaef8cded245bf5036a7a40b65622dd6c4da71f7a35343112edbe112b348a1e","00baffbe8a2f2e4875367479489b5d43b5fc1429ecb4a4cc98cfc3009095f52a","dcd91d3b697cb650b95db5471189b99815af5db2a1cd28760f91e0b12ede8ed5","3c92b6dfd43cc1c2485d9eba5ff0b74a19bb8725b692773ef1d66dac48cda4bd","3cf0d343c2276842a5b617f22ba82af6322c7cfe8bb52238ffc0c491a3c21019","df996e25faa505f85aeb294d15ebe61b399cf1d1e49959cdfaf2cc0815c203f9",{"version":"f2eff8704452659641164876c1ef0df4174659ce7311b0665798ea3f556fa9ad","affectsGlobalScope":true},"8841e2aa774b89bd23302dede20663306dc1b9902431ac64b24be8b8d0e3f649","2b8264b2fefd7367e0f20e2c04eed5d3038831fe00f5efbc110ff0131aab899b","a73a445c1e0a6d0f8b48e8eb22dc9d647896783a7f8991cbbc31c0d94bf1f5a2","d88a5e779faf033be3d52142a04fbe1cb96009868e3bbdd296b2bc6c59e06c0e","cd1d2f103b79002cd94b85a640a103f094227a2c4c53bc8af1fdbf4e13d9729e","5e379df3d61561c2ed7789b5995b9ba2143bbba21a905e2381e16efe7d1fa424","f07a137bbe2de7a122c37bfea00e761975fb264c49f18003d398d71b3fb35a5f","3dce33e7eb25594863b8e615f14a45ab98190d85953436750644212d8a18c066","2b93035328f7778d200252681c1d86285d501ed424825a18f81e4c3028aa51d9","2ac9c8332c5f8510b8bdd571f8271e0f39b0577714d5e95c1e79a12b2616f069","42c21aa963e7b86fa00801d96e88b36803188018d5ad91db2a9101bccd40b3ff","d31eb848cdebb4c55b4893b335a7c0cca95ad66dee13cbb7d0893810c0a9c301","b9f96255e1048ed2ea33ec553122716f0e57fc1c3ad778e9aa15f5b46547bd23","7a9e0a564fee396cacf706523b5aeed96e04c6b871a8bebefad78499fbffc5bc","906c751ef5822ec0dadcea2f0e9db64a33fb4ee926cc9f7efa38afe5d5371b2a","5387c049e9702f2d2d7ece1a74836a14b47fbebe9bbeb19f94c580a37c855351","c68391fb9efad5d99ff332c65b1606248c4e4a9f1dd9a087204242b56c7126d6","e9cf02252d3a0ced987d24845dcb1f11c1be5541f17e5daa44c6de2d18138d0c","e8b02b879754d85f48489294f99147aeccc352c760d95a6fe2b6e49cd400b2fe","9f6908ab3d8a86c68b86e38578afc7095114e66b2fc36a2a96e9252aac3998e0","0eedb2344442b143ddcd788f87096961cd8572b64f10b4afc3356aa0460171c6","71405cc70f183d029cc5018375f6c35117ffdaf11846c35ebf85ee3956b1b2a6","c68baff4d8ba346130e9753cefe2e487a16731bf17e05fdacc81e8c9a26aae9d","2cd15528d8bb5d0453aa339b4b52e0696e8b07e790c153831c642c3dea5ac8af","479d622e66283ffa9883fbc33e441f7fc928b2277ff30aacbec7b7761b4e9579","ade307876dc5ca267ca308d09e737b611505e015c535863f22420a11fffc1c54","f8cdefa3e0dee639eccbe9794b46f90291e5fd3989fcba60d2f08fde56179fb9","86c5a62f99aac7053976e317dbe9acb2eaf903aaf3d2e5bb1cafe5c2df7b37a8","2b300954ce01a8343866f737656e13243e86e5baef51bd0631b21dcef1f6e954","a2d409a9ffd872d6b9d78ead00baa116bbc73cfa959fce9a2f29d3227876b2a1","b288936f560cd71f4a6002953290de9ff8dfbfbf37f5a9391be5c83322324898","61178a781ef82e0ff54f9430397e71e8f365fc1e3725e0e5346f2de7b0d50dfa","6a6ccb37feb3aad32d9be026a3337db195979cd5727a616fc0f557e974101a54","c649ea79205c029a02272ef55b7ab14ada0903db26144d2205021f24727ac7a3","38e2b02897c6357bbcff729ef84c736727b45cc152abe95a7567caccdfad2a1d","d6610ea7e0b1a7686dba062a1e5544dd7d34140f4545305b7c6afaebfb348341","3dee35db743bdba2c8d19aece7ac049bde6fa587e195d86547c882784e6ba34c","b15e55c5fa977c2f25ca0b1db52cfa2d1fd4bf0baf90a8b90d4a7678ca462ff1","f41d30972724714763a2698ae949fbc463afb203b5fa7c4ad7e4de0871129a17","843dd7b6a7c6269fd43827303f5cbe65c1fecabc30b4670a50d5a15d57daeeb9","f06d8b8567ee9fd799bf7f806efe93b67683ef24f4dea5b23ef12edff4434d9d","6017384f697ff38bc3ef6a546df5b230c3c31329db84cbfe686c83bec011e2b2","e1a5b30d9248549ca0c0bb1d653bafae20c64c4aa5928cc4cd3017b55c2177b0","a593632d5878f17295bd53e1c77f27bf4c15212822f764a2bfc1702f4b413fa0","a868a534ba1c2ca9060b8a13b0ffbbbf78b4be7b0ff80d8c75b02773f7192c29","da7545aba8f54a50fde23e2ede00158dc8112560d934cee58098dfb03aae9b9d","34baf65cfee92f110d6653322e2120c2d368ee64b3c7981dff08ed105c4f19b0","a1a261624efb3a00ff346b13580f70f3463b8cdcc58b60f5793ff11785d52cab","f83b320cceccfc48457a818d18fc9a006ab18d0bdd727aa2c2e73dc1b4a45e98","9d92b037978bb9525bc4b673ebddd443277542e010c0aef019c03a170ccdaa73","b0d10e46cfe3f6c476b69af02eaa38e4ccc7430221ce3109ae84bb9fb8282298","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e","70e9a18da08294f75bf23e46c7d69e67634c0765d355887b9b41f0d959e1426e","ed44ba6b95f08b758748be7902e0cc54178b1337c56d0e2469c77b03f63ac73b"],"options":{"composite":true,"declaration":true,"declarationMap":true,"emitDeclarationOnly":true,"esModuleInterop":true,"inlineSources":true,"module":1,"outDir":"./types","rootDir":"../src","sourceMap":true,"strict":true,"target":7},"fileIdsList":[[120,247],[120],[91,120,127,128,129,144],[120,128,129,145,146],[120,127,128],[120,127,144,147,150],[120,127,147,150,151],[120,148,149,150,152,153],[120,127,150],[120,127,144,147,148,149,152],[120,127,135],[120,127],[91,120,127],[80,120,127],[120,131,132,133,134,135,136,137,138,139,140,141,142,143],[120,127,133,134],[120,127,133,135],[120,166,224],[120,224,225],[120,224,225,226,227],[120,166],[120,198],[120,198,199,200],[64,120],[67,120],[64,67,120],[65,66,67,68,69,70,71,72,73,74,75,120,155,158,159,160,161,162,163,164,165],[58,64,65,120],[67,73,75,120,154],[120,157],[67,68,120],[64,120,161],[120,193,194],[120,247,248,249,250,251],[120,247,249],[120,156],[120,254,255,256],[92,120,127],[120,259],[120,260],[120,271],[120,265,270],[120,274,276,277,278,279,280,281,282,283,284,285,286],[120,274,275,277,278,279,280,281,282,283,284,285,286],[120,275,276,277,278,279,280,281,282,283,284,285,286],[120,274,275,276,278,279,280,281,282,283,284,285,286],[120,274,275,276,277,279,280,281,282,283,284,285,286],[120,274,275,276,277,278,280,281,282,283,284,285,286],[120,274,275,276,277,278,279,281,282,283,284,285,286],[120,274,275,276,277,278,279,280,282,283,284,285,286],[120,274,275,276,277,278,279,280,281,283,284,285,286],[120,274,275,276,277,278,279,280,281,282,284,285,286],[120,274,275,276,277,278,279,280,281,282,283,285,286],[120,274,275,276,277,278,279,280,281,282,283,284,286],[120,274,275,276,277,278,279,280,281,282,283,284,285],[76,120],[79,120],[80,85,111,120],[81,91,92,99,108,119,120],[81,82,91,99,120],[83,120],[84,85,92,100,120],[85,108,116,120],[86,88,91,99,120],[87,120],[88,89,120],[90,91,120],[91,120],[91,92,93,108,119,120],[91,92,93,108,120],[91,94,99,108,119,120],[91,92,94,95,99,108,116,119,120],[94,96,108,116,119,120],[76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126],[91,97,120],[98,119,120,124],[88,91,99,108,120],[100,120],[101,120],[79,102,120],[103,118,120,124],[104,120],[105,120],[91,106,120],[106,107,120,122],[80,91,108,109,110,120],[80,108,110,120],[108,109,120],[111,120],[112,120],[91,114,115,120],[114,115,120],[85,99,116,120],[117,120],[99,118,120],[80,94,105,119,120],[85,120],[108,120,121],[120,122],[120,123],[80,85,91,93,102,108,119,120,122,124],[108,120,125],[120,127,292],[120,295,334],[120,295,319,334],[120,334],[120,295],[120,295,320,334],[120,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333],[120,320,334],[120,335],[120,339],[120,203],[120,203,214,215],[120,215,216,217],[120,178],[120,178,179,180,181,182],[120,167,168,169,170,171,172,173,174,175,176,177],[120,263,266],[120,263,266,267,268],[120,265],[120,262,269],[120,264],[57,59,60,61,62,63,120],[57,58,120],[59,120],[58,59,120],[57,59,120],[120,166,187,228],[120,229,230],[120,166,183,184,185],[120,184],[120,185],[56,120,184,185,186],[120,188],[120,188,189,192,196],[120,195],[120,166,190,191],[120,211,212,213],[120,210,211],[120,166,210,211],[120,166,203,210],[120,166,203],[120,166,204],[120,204,205,206,207,208,209],[120,166,187,190,197,201,202,219,220],[120,219],[120,202,219,221,222],[120,166,197,214,218],[120,166,235],[120,166,187,197,233,234,236],[120,166,187,197,231,232,233,235,236],[120,166,187,234],[120,166,228,235],[120,233,234,235,236,237,238,242],[120,166,210,243],[120,234,235,238],[120,239,240,241,243],[120,235,238],[120,166,235,238],[120,166,210,235,236],[120,183,187,201,223,243],[120,166,210,223,244],[120,244,245],[183,187,223,243],[166,210,223,244],[244,245]],"referencedMap":[[249,1],[247,2],[145,3],[128,2],[147,4],[129,5],[146,2],[151,6],[152,7],[148,7],[154,8],[149,7],[153,9],[150,10],[136,11],[133,12],[140,13],[134,11],[131,14],[139,2],[144,15],[141,2],[142,2],[143,2],[138,12],[135,16],[132,2],[137,17],[190,2],[225,18],[227,2],[226,19],[228,20],[224,21],[203,13],[199,22],[200,22],[201,23],[198,2],[65,24],[66,24],[68,25],[69,24],[70,24],[71,26],[72,2],[73,2],[74,2],[67,24],[166,27],[75,28],[155,29],[158,30],[159,2],[160,2],[161,2],[162,2],[163,2],[164,31],[165,32],[193,2],[195,33],[194,2],[252,34],[248,1],[250,35],[251,1],[191,12],[157,36],[253,2],[254,2],[257,37],[255,2],[258,38],[259,2],[260,39],[261,40],[272,41],[271,42],[256,2],[273,2],[275,43],[276,44],[274,45],[277,46],[278,47],[279,48],[280,49],[281,50],[282,51],[283,52],[284,53],[285,54],[286,55],[287,2],[156,2],[76,56],[77,56],[79,57],[80,58],[81,59],[82,60],[83,61],[84,62],[85,63],[86,64],[87,65],[88,66],[89,66],[90,67],[91,68],[92,69],[93,70],[78,2],[126,2],[94,71],[95,72],[96,73],[127,74],[97,75],[98,76],[99,77],[100,78],[101,79],[102,80],[103,81],[104,82],[105,83],[106,84],[107,85],[108,86],[110,87],[109,88],[111,89],[112,90],[113,2],[114,91],[115,92],[116,93],[117,94],[118,95],[119,96],[120,97],[121,98],[122,99],[123,100],[124,101],[125,102],[288,2],[289,12],[290,2],[291,2],[293,103],[292,2],[294,12],[319,104],[320,105],[295,106],[298,106],[317,104],[318,104],[308,104],[307,107],[305,104],[300,104],[313,104],[311,104],[315,104],[299,104],[312,104],[316,104],[301,104],[302,104],[314,104],[296,104],[303,104],[304,104],[306,104],[310,104],[321,108],[309,104],[297,104],[334,109],[333,2],[328,108],[330,110],[329,108],[322,108],[323,108],[325,108],[327,108],[331,110],[332,110],[324,110],[326,110],[336,111],[335,2],[337,2],[338,2],[339,2],[340,112],[130,2],[262,2],[215,113],[216,114],[217,114],[218,115],[177,2],[174,116],[176,116],[175,116],[173,116],[183,117],[178,118],[182,2],[179,2],[181,2],[180,2],[169,116],[170,116],[171,116],[167,2],[168,2],[172,116],[263,2],[267,119],[269,120],[268,119],[266,121],[270,122],[265,123],[264,2],[57,2],[64,124],[59,125],[60,126],[61,126],[62,127],[63,127],[58,128],[8,2],[10,2],[9,2],[2,2],[11,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[3,2],[4,2],[22,2],[19,2],[20,2],[21,2],[23,2],[24,2],[25,2],[5,2],[26,2],[27,2],[28,2],[29,2],[6,2],[33,2],[30,2],[31,2],[32,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[229,129],[230,2],[231,130],[56,2],[186,131],[185,132],[184,133],[187,134],[189,135],[197,136],[196,137],[188,2],[192,138],[214,139],[212,140],[213,141],[211,142],[204,143],[205,144],[206,144],[207,2],[208,144],[210,145],[209,144],[221,146],[202,2],[220,147],[222,147],[223,148],[219,149],[236,150],[235,151],[234,152],[233,153],[237,154],[243,155],[232,156],[239,157],[242,158],[240,159],[241,160],[238,161],[244,162],[245,163],[246,164],[47,2],[48,2],[49,2],[50,2],[51,2],[52,2],[43,2],[53,2],[54,2],[55,2],[44,2],[45,2],[46,2]],"exportedModulesMap":[[249,1],[247,2],[145,3],[128,2],[147,4],[129,5],[146,2],[151,6],[152,7],[148,7],[154,8],[149,7],[153,9],[150,10],[136,11],[133,12],[140,13],[134,11],[131,14],[139,2],[144,15],[141,2],[142,2],[143,2],[138,12],[135,16],[132,2],[137,17],[190,2],[225,18],[227,2],[226,19],[228,20],[224,21],[203,13],[199,22],[200,22],[201,23],[198,2],[65,24],[66,24],[68,25],[69,24],[70,24],[71,26],[72,2],[73,2],[74,2],[67,24],[166,27],[75,28],[155,29],[158,30],[159,2],[160,2],[161,2],[162,2],[163,2],[164,31],[165,32],[193,2],[195,33],[194,2],[252,34],[248,1],[250,35],[251,1],[191,12],[157,36],[253,2],[254,2],[257,37],[255,2],[258,38],[259,2],[260,39],[261,40],[272,41],[271,42],[256,2],[273,2],[275,43],[276,44],[274,45],[277,46],[278,47],[279,48],[280,49],[281,50],[282,51],[283,52],[284,53],[285,54],[286,55],[287,2],[156,2],[76,56],[77,56],[79,57],[80,58],[81,59],[82,60],[83,61],[84,62],[85,63],[86,64],[87,65],[88,66],[89,66],[90,67],[91,68],[92,69],[93,70],[78,2],[126,2],[94,71],[95,72],[96,73],[127,74],[97,75],[98,76],[99,77],[100,78],[101,79],[102,80],[103,81],[104,82],[105,83],[106,84],[107,85],[108,86],[110,87],[109,88],[111,89],[112,90],[113,2],[114,91],[115,92],[116,93],[117,94],[118,95],[119,96],[120,97],[121,98],[122,99],[123,100],[124,101],[125,102],[288,2],[289,12],[290,2],[291,2],[293,103],[292,2],[294,12],[319,104],[320,105],[295,106],[298,106],[317,104],[318,104],[308,104],[307,107],[305,104],[300,104],[313,104],[311,104],[315,104],[299,104],[312,104],[316,104],[301,104],[302,104],[314,104],[296,104],[303,104],[304,104],[306,104],[310,104],[321,108],[309,104],[297,104],[334,109],[333,2],[328,108],[330,110],[329,108],[322,108],[323,108],[325,108],[327,108],[331,110],[332,110],[324,110],[326,110],[336,111],[335,2],[337,2],[338,2],[339,2],[340,112],[130,2],[262,2],[215,113],[216,114],[217,114],[218,115],[177,2],[174,116],[176,116],[175,116],[173,116],[183,117],[178,118],[182,2],[179,2],[181,2],[180,2],[169,116],[170,116],[171,116],[167,2],[168,2],[172,116],[263,2],[267,119],[269,120],[268,119],[266,121],[270,122],[265,123],[264,2],[57,2],[64,124],[59,125],[60,126],[61,126],[62,127],[63,127],[58,128],[8,2],[10,2],[9,2],[2,2],[11,2],[12,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[3,2],[4,2],[22,2],[19,2],[20,2],[21,2],[23,2],[24,2],[25,2],[5,2],[26,2],[27,2],[28,2],[29,2],[6,2],[33,2],[30,2],[31,2],[32,2],[34,2],[7,2],[35,2],[40,2],[41,2],[36,2],[37,2],[38,2],[39,2],[1,2],[42,2],[229,129],[230,2],[231,130],[56,2],[186,131],[185,132],[184,133],[187,134],[189,135],[197,136],[196,137],[188,2],[192,138],[214,139],[212,140],[213,141],[211,142],[204,143],[205,144],[206,144],[207,2],[208,144],[210,145],[209,144],[221,146],[202,2],[220,147],[222,147],[223,148],[219,149],[236,150],[235,151],[234,152],[233,153],[237,154],[243,155],[232,156],[239,157],[242,158],[240,159],[241,160],[238,161],[244,165],[245,166],[246,167],[47,2],[48,2],[49,2],[50,2],[51,2],[52,2],[43,2],[53,2],[54,2],[55,2],[44,2],[45,2],[46,2]],"semanticDiagnosticsPerFile":[249,247,145,128,147,129,146,151,152,148,154,149,153,150,136,133,140,134,131,139,144,141,142,143,138,135,132,137,190,225,227,226,228,224,203,199,200,201,198,65,66,68,69,70,71,72,73,74,67,166,75,155,158,159,160,161,162,163,164,165,193,195,194,252,248,250,251,191,157,253,254,257,255,258,259,260,261,272,271,256,273,275,276,274,277,278,279,280,281,282,283,284,285,286,287,156,76,77,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,78,126,94,95,96,127,97,98,99,100,101,102,103,104,105,106,107,108,110,109,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,288,289,290,291,293,292,294,319,320,295,298,317,318,308,307,305,300,313,311,315,299,312,316,301,302,314,296,303,304,306,310,321,309,297,334,333,328,330,329,322,323,325,327,331,332,324,326,336,335,337,338,339,340,130,262,215,216,217,218,177,174,176,175,173,183,178,182,179,181,180,169,170,171,167,168,172,263,267,269,268,266,270,265,264,57,64,59,60,61,62,63,58,8,10,9,2,11,12,13,14,15,16,17,18,3,4,22,19,20,21,23,24,25,5,26,27,28,29,6,33,30,31,32,34,7,35,40,41,36,37,38,39,1,42,229,230,231,56,186,185,184,187,189,197,196,188,192,214,212,213,211,204,205,206,207,208,210,209,221,202,220,222,223,219,236,235,234,233,237,243,232,239,242,240,241,238,244,245,246,47,48,49,50,51,52,43,53,54,55,44,45,46],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.9.5"} -\ No newline at end of file -diff --git a/dist/types/SelectedNetworkController.d.ts.map b/dist/types/SelectedNetworkController.d.ts.map -index f9de27de6d312eeb12e583354bd0df73b08598a4..eb0dcc3cd8563769606ae8c6748d2ad1bd5665c1 100644 ---- a/dist/types/SelectedNetworkController.d.ts.map -+++ b/dist/types/SelectedNetworkController.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"SelectedNetworkController.d.ts","sourceRoot":"","sources":["../../src/SelectedNetworkController.ts"],"names":[],"mappings":"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"} -\ No newline at end of file -+{"version":3,"file":"SelectedNetworkController.d.ts","sourceRoot":"","sources":["../../src/SelectedNetworkController.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,2CAA2C,EAC3C,+CAA+C,EAC/C,+BAA+B,EAC/B,iCAAiC,EACjC,aAAa,EACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,+BAA+B,EAC/B,WAAW,IAAI,qCAAqC,EACpD,cAAc,IAAI,kCAAkC,EACrD,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,eAAO,MAAM,cAAc,8BAA8B,CAAC;AAc1D,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,eAAO,MAAM,eAAe,YAAsB,CAAC;AAEnD,eAAO,MAAM,oCAAoC;;;;CAMhD,CAAC;AAEF,eAAO,MAAM,mCAAmC;;CAE/C,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,yCAAyC,GAAG;IACtD,IAAI,EAAE,OAAO,mCAAmC,CAAC,WAAW,CAAC;IAC7D,OAAO,EAAE,CAAC,8BAA8B,EAAE,KAAK,EAAE,CAAC,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,sDAAsD,GAAG;IACnE,IAAI,EAAE,OAAO,oCAAoC,CAAC,QAAQ,CAAC;IAC3D,OAAO,EAAE,MAAM,8BAA8B,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,0DAA0D,GAAG;IACvE,IAAI,EAAE,OAAO,oCAAoC,CAAC,2BAA2B,CAAC;IAC9E,OAAO,EAAE,yBAAyB,CAAC,6BAA6B,CAAC,CAAC;CACnE,CAAC;AAEF,MAAM,MAAM,0DAA0D,GAAG;IACvE,IAAI,EAAE,OAAO,oCAAoC,CAAC,2BAA2B,CAAC;IAC9E,OAAO,EAAE,yBAAyB,CAAC,6BAA6B,CAAC,CAAC;CACnE,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,sDAAsD,GACtD,0DAA0D,GAC1D,0DAA0D,CAAC;AAE/D,MAAM,MAAM,cAAc,GACtB,2CAA2C,GAC3C,+CAA+C,GAC/C,+BAA+B,GAC/B,kCAAkC,GAClC,qCAAqC,CAAC;AAE1C,MAAM,MAAM,+BAA+B,GACzC,yCAAyC,CAAC;AAE5C,MAAM,MAAM,aAAa,GACrB,iCAAiC,GACjC,+BAA+B,CAAC;AAEpC,MAAM,MAAM,kCAAkC,GAAG,6BAA6B,CAC5E,OAAO,cAAc,EACrB,gCAAgC,GAAG,cAAc,EACjD,+BAA+B,GAAG,aAAa,EAC/C,cAAc,CAAC,MAAM,CAAC,EACtB,aAAa,CAAC,MAAM,CAAC,CACtB,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,KAAK,CAAC,EAAE,8BAA8B,CAAC;IACvC,SAAS,EAAE,kCAAkC,CAAC;IAC9C,yBAAyB,EAAE,OAAO,CAAC;IACnC,wBAAwB,EAAE,CACxB,QAAQ,EAAE,CAAC,gBAAgB,EAAE;QAAE,eAAe,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,KAC/D,IAAI,CAAC;IACV,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,iBAAiB,CAAC;CACjC,CAAC;AAEF;;GAEG;AACH,qBAAa,yBAA0B,SAAQ,cAAc,CAC3D,OAAO,cAAc,EACrB,8BAA8B,EAC9B,kCAAkC,CACnC;;IAKC;;;;;;;;;OASG;gBACS,EACV,SAAS,EACT,KAAyB,EACzB,yBAAyB,EACzB,wBAAwB,EACxB,cAAc,GACf,EAAE,gCAAgC;IAkKnC,2BAA2B,CACzB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,eAAe;IA0BlC,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe;IAS5D;;;;;OAKG;IACH,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY;CA6CzD"} -\ No newline at end of file diff --git a/.yarn/patches/@metamask-snaps-controllers-npm-9.5.0-8b21e3c072.patch b/.yarn/patches/@metamask-snaps-controllers-npm-9.5.0-8b21e3c072.patch new file mode 100644 index 000000000000..314a33b9f881 --- /dev/null +++ b/.yarn/patches/@metamask-snaps-controllers-npm-9.5.0-8b21e3c072.patch @@ -0,0 +1,13 @@ +diff --git a/package.json b/package.json +index e738c058c38f8bc5c14bab9644ecced32eefa075..9aba0d03fc642572fcbdc2c791e335df72d70f35 100644 +--- a/package.json ++++ b/package.json +@@ -9,7 +9,7 @@ + "sideEffects": false, + "exports": { + ".": { +- "import": "./dist/index.mjs", ++ "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + }, diff --git a/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch b/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch deleted file mode 100644 index 82ddce260b99..000000000000 --- a/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch +++ /dev/null @@ -1,30 +0,0 @@ -diff --git a/dist/chunk-37VHIRUJ.js b/dist/chunk-37VHIRUJ.js -index a909a4ef20305665a07db5c25b4a9ff7eb0a447e..98dd75bf33a9716dc6cca96a38d184645f6ec033 100644 ---- a/dist/chunk-37VHIRUJ.js -+++ b/dist/chunk-37VHIRUJ.js -@@ -53,8 +53,8 @@ function assertIsKeyringOrigins(value, ErrorWrapper) { - } - function createOriginRegExp(matcher) { - const escaped = matcher.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -- const regex = escaped.replace(/\*/gu, ".*"); -- return RegExp(regex, "u"); -+ const regex = escaped.replace(/\\\*/gu, '.*'); -+ return RegExp(`${regex}$`, 'u'); - } - function checkAllowedOrigin(matcher, origin) { - if (matcher === "*" || matcher === origin) { -diff --git a/dist/chunk-K2OTEZZZ.mjs b/dist/chunk-K2OTEZZZ.mjs -index 15be5da7563a5bdf464d7e9c28ed6f04863e378a..7f38bf328e71c1feb2b8850ba050ce9e55801668 100644 ---- a/dist/chunk-K2OTEZZZ.mjs -+++ b/dist/chunk-K2OTEZZZ.mjs -@@ -53,8 +53,8 @@ function assertIsKeyringOrigins(value, ErrorWrapper) { - } - function createOriginRegExp(matcher) { - const escaped = matcher.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); -- const regex = escaped.replace(/\*/gu, ".*"); -- return RegExp(regex, "u"); -+ const regex = escaped.replace(/\\\*/gu, '.*'); -+ return RegExp(`${regex}$`, 'u'); - } - function checkAllowedOrigin(matcher, origin) { - if (matcher === "*" || matcher === origin) { diff --git a/.yarn/patches/@metamask-transaction-controller-npm-32.0.0-e23c2c3443.patch b/.yarn/patches/@metamask-transaction-controller-npm-32.0.0-e23c2c3443.patch deleted file mode 100644 index eb2684b678b7..000000000000 Binary files a/.yarn/patches/@metamask-transaction-controller-npm-32.0.0-e23c2c3443.patch and /dev/null differ diff --git a/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch b/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch index 019aa54534e5..a3c730d3a46b 100644 --- a/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch +++ b/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch @@ -92,3 +92,29 @@ index bb433432ec76331e12d6b62e200f06530055cb16..9caf4051aa96bd14ee2890ef6c79bf5b return EnhancerArray; }(Array)); function freezeDraftable(val) { +diff --git a/dist/redux-toolkit.esm.js b/dist/redux-toolkit.esm.js +index f26a1669405b4dd92dfecd791dc536078a7e2e12..591e7495fcaf3233d26cfb9c4eae09fd7ae3eb98 100644 +--- a/dist/redux-toolkit.esm.js ++++ b/dist/redux-toolkit.esm.js +@@ -192,9 +192,6 @@ function getMessage(type) { + } + function createActionCreatorInvariantMiddleware(options) { + if (options === void 0) { options = {}; } +- if (process.env.NODE_ENV === "production") { +- return function () { return function (next) { return function (action) { return next(action); }; }; }; +- } + var _c = options.isActionCreator, isActionCreator2 = _c === void 0 ? isActionCreator : _c; + return function () { return function (next) { return function (action) { + if (isActionCreator2(action)) { +diff --git a/package.json b/package.json +index 684ea845ee663f719bff6c140001baebdaa69344..568d6215514a8625bfb3be5e49b6cbfe11231e6a 100644 +--- a/package.json ++++ b/package.json +@@ -23,7 +23,6 @@ + "access": "public" + }, + "main": "dist/index.js", +- "module": "dist/redux-toolkit.esm.js", + "unpkg": "dist/redux-toolkit.umd.min.js", + "types": "dist/index.d.ts", + "devDependencies": { diff --git a/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch b/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch new file mode 100644 index 000000000000..854707ea51a1 --- /dev/null +++ b/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch @@ -0,0 +1,33 @@ +diff --git a/lib/impl/core-in-iframe.js b/lib/impl/core-in-iframe.js +index c47cf3bff860d6b1855341c00b80fc6c40f9d6d5..275eb0f312ff396819fa406c154a3562842db49d 100644 +--- a/lib/impl/core-in-iframe.js ++++ b/lib/impl/core-in-iframe.js +@@ -116,7 +116,9 @@ class CoreInIframe { + this._log.enabled = !!this._settings.debug; + window.addEventListener('message', this.boundHandleMessage); + window.addEventListener('unload', this.boundDispose); +- await iframe.init(this._settings); ++ const modifiedSettings = Object.assign({}, this.settings); ++ modifiedSettings.env = 'webextension'; ++ await iframe.init(modifiedSettings); + if (this._settings.sharedLogger !== false) { + iframe.initIframeLogger(); + } +diff --git a/lib/popup/index.js b/lib/popup/index.js +index 9b13c370a5ac8b4e4fc0315ed40cdf615d0bb0cb..4dbd97fc28df49beb73379451974ec48a8a42ea7 100644 +--- a/lib/popup/index.js ++++ b/lib/popup/index.js +@@ -229,10 +229,12 @@ class PopupManager extends events_1.default { + } + else if (message.type === events_2.POPUP.LOADED) { + this.handleMessage(message); ++ const modifiedSettings = Object.assign({}, this.settings); ++ modifiedSettings.env = 'webextension'; + this.channel.postMessage({ + type: events_2.POPUP.INIT, + payload: { +- settings: this.settings, ++ settings: modifiedSettings, + useCore: true, + }, + }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f3091f878e..4a26b1fb619d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,414 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [12.4.0] +### Uncategorized +- fix: flaky test `Test Snap Interactive UI test interactive ui elements` ([#26792](https://github.com/MetaMask/metamask-extension/pull/26792)) +- feat: Update Polygon from `MATIC` to `POL` ([#26671](https://github.com/MetaMask/metamask-extension/pull/26671)) +- feat: implement client side malicious network request detection ([#25839](https://github.com/MetaMask/metamask-extension/pull/25839)) +- fix: Improve migration 121.1 state validation ([#26773](https://github.com/MetaMask/metamask-extension/pull/26773)) +- chore: Bump Snaps dependencies ([#26675](https://github.com/MetaMask/metamask-extension/pull/26675)) +- refactor: extract Send-specific functionality out of AssetPicker ([#26558](https://github.com/MetaMask/metamask-extension/pull/26558)) +- fix: rename migration 126 to 121.1 ([#26742](https://github.com/MetaMask/metamask-extension/pull/26742)) +- chore: Bump `storybook`, `@storybook/*` to `^7.6.20`, `storybook-dark-mode` from `^3.0.3` to `^4.0.2` ([#26703](https://github.com/MetaMask/metamask-extension/pull/26703)) +- fix: Sentry app state null data to show null as value. ([#26522](https://github.com/MetaMask/metamask-extension/pull/26522)) +- chore: Master sync ([#26737](https://github.com/MetaMask/metamask-extension/pull/26737)) +- fix: Stop using a hardcoded Snap ID for notifications ([#26739](https://github.com/MetaMask/metamask-extension/pull/26739)) +- chore: MMI Fixes passing the state to route using history.push ([#26722](https://github.com/MetaMask/metamask-extension/pull/26722)) +- Merge origin/develop into master-sync +- test: Add integration test for insufficient gas ([#26711](https://github.com/MetaMask/metamask-extension/pull/26711)) +- test: [Snaps E2E] Add changes to fix flakiness in Snaps UI Images test ([#26725](https://github.com/MetaMask/metamask-extension/pull/26725)) +- test: Add integration test for gas estimate failed alert ([#26681](https://github.com/MetaMask/metamask-extension/pull/26681)) +- fix: flaky test `Click bridge button @no-mmi loads portfolio tab from asset overview when flag is turned off` ([#26654](https://github.com/MetaMask/metamask-extension/pull/26654)) +- fix: flaky test `Navigation Signature - Different signature types initiates and queues multiple signatures and confirms` ([#26707](https://github.com/MetaMask/metamask-extension/pull/26707)) +- feat: adding context to get current confirmation in re-designed confirmation pages PR-1 ([#26587](https://github.com/MetaMask/metamask-extension/pull/26587)) +- fix: `wallet_addEthereumChain` does not attach a `result` under certain conditions ([#26726](https://github.com/MetaMask/metamask-extension/pull/26726)) +- fix: Add IOTX icon ([#26723](https://github.com/MetaMask/metamask-extension/pull/26723)) +- test: Add integration tests for network busy alert ([#26679](https://github.com/MetaMask/metamask-extension/pull/26679)) +- feat: Temporarily hide Approve redesigned pages ([#26676](https://github.com/MetaMask/metamask-extension/pull/26676)) +- perf: use an interstitial page to load `popup.html`; load scripts using `defer`ed script tags ([#26555](https://github.com/MetaMask/metamask-extension/pull/26555)) +- feat: Add metrics to track where signature rejection occurred ([#26469](https://github.com/MetaMask/metamask-extension/pull/26469)) +- chore: update @metamask/bitcoin-wallet-snap to 0.5.0 ([#26701](https://github.com/MetaMask/metamask-extension/pull/26701)) +- fix: adding missing token images ([#26708](https://github.com/MetaMask/metamask-extension/pull/26708)) +- feat: Added Edit networks screen modal ([#26097](https://github.com/MetaMask/metamask-extension/pull/26097)) +- perf: add trace for UI startup ([#26636](https://github.com/MetaMask/metamask-extension/pull/26636)) +- fix: Address design review on contract interaction and deployment red… ([#26659](https://github.com/MetaMask/metamask-extension/pull/26659)) +- test: [Snaps E2E] Add test cases for signature confirmations redesign to signature insights snaps test ([#26691](https://github.com/MetaMask/metamask-extension/pull/26691)) +- feat: updated ui for adding chain id screen ([#25777](https://github.com/MetaMask/metamask-extension/pull/25777)) +- fix: update moonbeam and moonriver network and token logos ([#26677](https://github.com/MetaMask/metamask-extension/pull/26677)) +- chore: MMI adds back the current Tx confirmation view to MMI ([#26539](https://github.com/MetaMask/metamask-extension/pull/26539)) +- fix(snaps): Use ApprovalType instead DIALOG_APPROVAL_TYPES in confirmation page ([#26655](https://github.com/MetaMask/metamask-extension/pull/26655)) +- fix: catch error for getTokenStandardAndDetails ([#26269](https://github.com/MetaMask/metamask-extension/pull/26269)) +- chore: Master sync ([#26641](https://github.com/MetaMask/metamask-extension/pull/26641)) +- chore: update gitignore ([#26642](https://github.com/MetaMask/metamask-extension/pull/26642)) +- fix: flaky test `Phishing Detection should navigate the user to PhishFort to dispute a Phishfort Block` ([#26651](https://github.com/MetaMask/metamask-extension/pull/26651)) +- fix: flaky tests `Sentry errors before initialization, after opting into metrics @no-mmi should capture UI application state`... ([#26648](https://github.com/MetaMask/metamask-extension/pull/26648)) +- fix: flaky test `Vault Decryptor Page is able to decrypt the vault us..` due to empty file load ([#26612](https://github.com/MetaMask/metamask-extension/pull/26612)) +- Merge branch 'develop' into master-sync +- fix: flaky test `Increase Token Allowance increases token spending ca..` ([#26640](https://github.com/MetaMask/metamask-extension/pull/26640)) +- chore: bump smart transactions controller ([#26644](https://github.com/MetaMask/metamask-extension/pull/26644)) +- chore: Polish multichain token list styles ([#26300](https://github.com/MetaMask/metamask-extension/pull/26300)) +- Merge origin/develop into master-sync +- feat: upgrade network controller to v20 ([#26150](https://github.com/MetaMask/metamask-extension/pull/26150)) +- docs: Add publish a release to Sentry flow steps ([#26605](https://github.com/MetaMask/metamask-extension/pull/26605)) +- chore: set bridge network allowlists from feature flags ([#26147](https://github.com/MetaMask/metamask-extension/pull/26147)) +- chore: anonymize send analytic properties #26627 ([#26628](https://github.com/MetaMask/metamask-extension/pull/26628)) +- chore: add user IDs to send page analytics ([#26600](https://github.com/MetaMask/metamask-extension/pull/26600)) +- fix: bump accounts controller and migration to fix undefined selectedAccount ([#26573](https://github.com/MetaMask/metamask-extension/pull/26573)) +- feat: Integrate Snaps into the redesigned confirmations ([#26435](https://github.com/MetaMask/metamask-extension/pull/26435)) +- refactor: Replace usages of the deprecated `setProviderType` ([#22619](https://github.com/MetaMask/metamask-extension/pull/22619)) +- refactor: Use generic helper function to initiate signatures ([#26584](https://github.com/MetaMask/metamask-extension/pull/26584)) +- test: [Snaps E2E] Update snaps dialog test to include Custom dialog type ([#26598](https://github.com/MetaMask/metamask-extension/pull/26598)) +- feat: new receive flow ([#26148](https://github.com/MetaMask/metamask-extension/pull/26148)) +- fix: remove speed up and cancel controller validation ([#26492](https://github.com/MetaMask/metamask-extension/pull/26492)) +- fix: flaky test `Test Snap Name Lookup tests name-lookup functionalit...` ([#26583](https://github.com/MetaMask/metamask-extension/pull/26583)) +- feat: Add contract deployment redesigned transaction screen ([#26382](https://github.com/MetaMask/metamask-extension/pull/26382)) +- feat: add transaction performance metrics ([#26332](https://github.com/MetaMask/metamask-extension/pull/26332)) +- test: add tests for insufficient funds alert ([#26512](https://github.com/MetaMask/metamask-extension/pull/26512)) +- feat: account watcher e2e ([#26524](https://github.com/MetaMask/metamask-extension/pull/26524)) +- feat: update add team label workflow ([#26548](https://github.com/MetaMask/metamask-extension/pull/26548)) +- feat: Add approval static simulation ([#26514](https://github.com/MetaMask/metamask-extension/pull/26514)) +- fix: Snapshot unit tests ([#26585](https://github.com/MetaMask/metamask-extension/pull/26585)) +- chore: Rename `permittedChains` permission to `endowment:permitted-chains` ([#26534](https://github.com/MetaMask/metamask-extension/pull/26534)) +- feat: Redesign Approve confirmation ([#26464](https://github.com/MetaMask/metamask-extension/pull/26464)) +- feat: Enable hardware wallets for smart transactions, sign a transaction only once ([#26251](https://github.com/MetaMask/metamask-extension/pull/26251)) +- fix: Allowlist Snap UI card component ([#26565](https://github.com/MetaMask/metamask-extension/pull/26565)) +- fix(deps): Bump `@metamask/eth-json-rpc-middleware` to `^14.0.0`, `@metamask/transaction-controller` to `^35.1.1` ([#26143](https://github.com/MetaMask/metamask-extension/pull/26143)) +- fix: adding warning for origin on redesigned pages ([#26306](https://github.com/MetaMask/metamask-extension/pull/26306)) +- fix: track `swapAndSend` transaction type ([#26535](https://github.com/MetaMask/metamask-extension/pull/26535)) +- feat: added AccountWatcher as preinstalled snap and added to menu list ([#26402](https://github.com/MetaMask/metamask-extension/pull/26402)) +- fix: stick add team label version to commit hash ([#26540](https://github.com/MetaMask/metamask-extension/pull/26540)) +- fix: correct duplicate notifications event tracking in global menu ([#26525](https://github.com/MetaMask/metamask-extension/pull/26525)) +- feat: migrate protect intrinsics test to e2e ([#26197](https://github.com/MetaMask/metamask-extension/pull/26197)) +- fix: NetworkChangeToast width in wide screen mode ([#26532](https://github.com/MetaMask/metamask-extension/pull/26532)) +- fix: missing deadline in swaps stx status screen ([#25779](https://github.com/MetaMask/metamask-extension/pull/25779)) +- fix: Snap Address component UI/UX (Snaps custom UI) ([#26477](https://github.com/MetaMask/metamask-extension/pull/26477)) +- feat(snaps): Removed Snaps name-lookup permission code fences ([#26393](https://github.com/MetaMask/metamask-extension/pull/26393)) +- docs: Include MV2 build commands in README ([#26486](https://github.com/MetaMask/metamask-extension/pull/26486)) +- test: add `driver.clickElementAndWaitForWindowToClose` helper method ([#26449](https://github.com/MetaMask/metamask-extension/pull/26449)) +- chore: Integrate SnapInsightsController ([#26411](https://github.com/MetaMask/metamask-extension/pull/26411)) +- feat: Update @blockaid/ppom_release to release 1.5.2 ([#26494](https://github.com/MetaMask/metamask-extension/pull/26494)) +- chore: Master sync ([#26497](https://github.com/MetaMask/metamask-extension/pull/26497)) +- Merge origin/develop into master-sync +- feat(notifications): use shared libraries NotificationServicesController ([#26480](https://github.com/MetaMask/metamask-extension/pull/26480)) +- perf: add parallel fetching for the network fee dropdown ([#26489](https://github.com/MetaMask/metamask-extension/pull/26489)) +- chore: remove token and nft detection modals ([#26403](https://github.com/MetaMask/metamask-extension/pull/26403)) +- chore: Add Near Icon ([#26459](https://github.com/MetaMask/metamask-extension/pull/26459)) +- fix: Restore `responsive` e2e driver option ([#25932](https://github.com/MetaMask/metamask-extension/pull/25932)) +- chore: downgrade prettier-eslint to match prettier version ([#26145](https://github.com/MetaMask/metamask-extension/pull/26145)) +- test: Add manual scenario for upgrade testing ([#26317](https://github.com/MetaMask/metamask-extension/pull/26317)) +- build(chore): switch to `defer` since it guarantees execution order once chunked ([#26425](https://github.com/MetaMask/metamask-extension/pull/26425)) +- fix: Update send transactions with custom nonce.csv ([#26451](https://github.com/MetaMask/metamask-extension/pull/26451)) +- fix: `rpcIdentifierUtility` client side grouping before emitting CustomRPC event ([#26266](https://github.com/MetaMask/metamask-extension/pull/26266)) +- feat(notifications): use notification services push controller ([#26448](https://github.com/MetaMask/metamask-extension/pull/26448)) +- feat: Add footers to Snap home pages ([#26463](https://github.com/MetaMask/metamask-extension/pull/26463)) +- fix: Remove double padding on Snap home page ([#26462](https://github.com/MetaMask/metamask-extension/pull/26462)) +- chore(webpack): update `html-bundler-webpack-plugin` from `v3.6.5` to `v3.17.3` ([#26371](https://github.com/MetaMask/metamask-extension/pull/26371)) +- chore: Master sync ([#26395](https://github.com/MetaMask/metamask-extension/pull/26395)) +- chore: Bump Snaps packages ([#26086](https://github.com/MetaMask/metamask-extension/pull/26086)) +- fix: Improve AccountListMenu/Item performance ([#26379](https://github.com/MetaMask/metamask-extension/pull/26379)) +- fix: Codespaces `corepack enable` ([#25161](https://github.com/MetaMask/metamask-extension/pull/25161)) +- fix: display toast message if user quickly sends transaction on different networks ([#26114](https://github.com/MetaMask/metamask-extension/pull/26114)) +- fix: problem with origins in the Snaps permission UI ([#26422](https://github.com/MetaMask/metamask-extension/pull/26422)) +- feat: Add abstraction for Snaps permissions ([#25175](https://github.com/MetaMask/metamask-extension/pull/25175)) +- test: add transaction contract interaction integration tests ([#26272](https://github.com/MetaMask/metamask-extension/pull/26272)) +- fix: timeout and "Rerun failed tests" ([#26239](https://github.com/MetaMask/metamask-extension/pull/26239)) +- chore: migrate BridgeController to BaseController v2 ([#26109](https://github.com/MetaMask/metamask-extension/pull/26109)) +- feat: Enable why did you render ([#26339](https://github.com/MetaMask/metamask-extension/pull/26339)) +- fix: Delete invalid `SelectedNetworkController` state ([#26428](https://github.com/MetaMask/metamask-extension/pull/26428)) +- test: ensure bridge button handles clicks according to feature flags ([#25812](https://github.com/MetaMask/metamask-extension/pull/25812)) +- build(webpack): polyfill `setImmediate` ([#26398](https://github.com/MetaMask/metamask-extension/pull/26398)) +- feat: feature-flagged cross-chain swaps route [METABRIDGE-867] ([#25811](https://github.com/MetaMask/metamask-extension/pull/25811)) +- chore: Remove i18n translations from Developer Options Settings Page ([#26380](https://github.com/MetaMask/metamask-extension/pull/26380)) +- fix: Do not break application if no token details are found using getTokenStandardAndDetails ([#26324](https://github.com/MetaMask/metamask-extension/pull/26324)) +- fix: Flaky contract interaction test ([#26420](https://github.com/MetaMask/metamask-extension/pull/26420)) +- fix: Enter key on Create Account checkbox should not trigger show/hide ([#26394](https://github.com/MetaMask/metamask-extension/pull/26394)) +- fix: notifications use better events ([#26410](https://github.com/MetaMask/metamask-extension/pull/26410)) +- fix: Restore snaps-controllers version following patch ([#26412](https://github.com/MetaMask/metamask-extension/pull/26412)) +- fix: Improve hex copy button ([#26384](https://github.com/MetaMask/metamask-extension/pull/26384)) +- refactor: use core profile syncing controllers. ([#26370](https://github.com/MetaMask/metamask-extension/pull/26370)) +- test: snap account contract interaction ([#26234](https://github.com/MetaMask/metamask-extension/pull/26234)) +- feat: updated SSK version in e2e and added test for creating multiple… ([#26378](https://github.com/MetaMask/metamask-extension/pull/26378)) +- Merge origin/develop into master-sync +- chore: MMI move duck and selector to TS ([#26125](https://github.com/MetaMask/metamask-extension/pull/26125)) +- refactor(notifications): use contentful package as dev dependency ([#26381](https://github.com/MetaMask/metamask-extension/pull/26381)) +- fix: remove submitRequest from dapp permission ([#26319](https://github.com/MetaMask/metamask-extension/pull/26319)) +- feat: Add integration test for blockaid on contract interaction ([#26366](https://github.com/MetaMask/metamask-extension/pull/26366)) +- refactor: add performance tracing infrastructure ([#26044](https://github.com/MetaMask/metamask-extension/pull/26044)) +- refactor: replace deprecated mixins with Text component in slippage-buttons ([#25638](https://github.com/MetaMask/metamask-extension/pull/25638)) +- refactor: replace deprecated mixins with text component in loading-swaps-quotes ([#25553](https://github.com/MetaMask/metamask-extension/pull/25553)) +- feat: Add metrics for alerts (transactions redesign) ([#26121](https://github.com/MetaMask/metamask-extension/pull/26121)) +- fix(25350): fix flakey token importing e2e test ([#26351](https://github.com/MetaMask/metamask-extension/pull/26351)) +- fix: enable Save button on Add Contact page for address input ([#26155](https://github.com/MetaMask/metamask-extension/pull/26155)) +- test: Add test for migration 120.2 and fix docs ([#26333](https://github.com/MetaMask/metamask-extension/pull/26333)) +- chore: normalize separator in `content` on the `viewport` `meta` tag ([#26268](https://github.com/MetaMask/metamask-extension/pull/26268)) +- fix: Stop logging pipeline stream errors in the service worker if they match 'Premature close' ([#26336](https://github.com/MetaMask/metamask-extension/pull/26336)) +- build: add alternative build process to enable faster developer builds ([#22506](https://github.com/MetaMask/metamask-extension/pull/22506)) +- fix: issue where `setNetworkClientIdForDomain` was called without checking whether the origin was eligible for setting its own network ([#26323](https://github.com/MetaMask/metamask-extension/pull/26323)) +- fix: get permit and order signatures token decimals ([#26292](https://github.com/MetaMask/metamask-extension/pull/26292)) +- feat: Update Redesign Signature Permit to show ellipsis at max 15 digits ([#26227](https://github.com/MetaMask/metamask-extension/pull/26227)) +- fix: remove the ability to send to btc accounts in send page ([#26271](https://github.com/MetaMask/metamask-extension/pull/26271)) +- fix: Adding migration 125 to remove Deprecated TxController Key from state ([#26267](https://github.com/MetaMask/metamask-extension/pull/26267)) +- fix: Revert "fix: remove submitRequest from dapp permission" ([#26293](https://github.com/MetaMask/metamask-extension/pull/26293)) +- refactor: convert `icon-factory.js` to typescript ([#23823](https://github.com/MetaMask/metamask-extension/pull/23823)) +- fix(26065): remove persisted state mostRecentRetrievedState after initialization if no errors ([#26206](https://github.com/MetaMask/metamask-extension/pull/26206)) +- refactor: ENABLE_MV3 flag cleanup ([#26059](https://github.com/MetaMask/metamask-extension/pull/26059)) +- test: fix flaky test Import flow @no-mmi Import wallet using Secret Recovery Phrase ([#26275](https://github.com/MetaMask/metamask-extension/pull/26275)) +- chore: Fully remove `eth_sign` ([#24756](https://github.com/MetaMask/metamask-extension/pull/24756)) +- fix: remove submitRequest from dapp permission ([#26276](https://github.com/MetaMask/metamask-extension/pull/26276)) +- chore: Update `actions/cache` from v3 to v4 ([#26020](https://github.com/MetaMask/metamask-extension/pull/26020)) +- feat: QR-based add NGRAVE ZERO Hardware ([#25080](https://github.com/MetaMask/metamask-extension/pull/25080)) +- fix: Fix GitHub release description ([#26247](https://github.com/MetaMask/metamask-extension/pull/26247)) +- feat(btc): use new snap account flow for Bitcoin accounts ([#26183](https://github.com/MetaMask/metamask-extension/pull/26183)) +- refactor: replace deprecated mixins with text component in transaction-confirmed ([#25551](https://github.com/MetaMask/metamask-extension/pull/25551)) +- fix: improve warning in add network modal ([#26250](https://github.com/MetaMask/metamask-extension/pull/26250)) +- fix: Fix `create_release_pull_request` OOM error ([#26249](https://github.com/MetaMask/metamask-extension/pull/26249)) +- chore: Create a story for TokenCurrencyDisplay component ([#26172](https://github.com/MetaMask/metamask-extension/pull/26172)) +- chore: refactoring onboarding to remove deprecated components ([#26207](https://github.com/MetaMask/metamask-extension/pull/26207)) +- fix: Fix CircleCI `create_release_pull_request` job ([#26246](https://github.com/MetaMask/metamask-extension/pull/26246)) +- fix: flaky test `Import flow @no-mmi Import Account using json file` ([#26240](https://github.com/MetaMask/metamask-extension/pull/26240)) +- fix: sentry sessions ([#26192](https://github.com/MetaMask/metamask-extension/pull/26192)) +- test: [Page Object Model] rename process to flow ([#26228](https://github.com/MetaMask/metamask-extension/pull/26228)) +- test: header integration test for contract interaction ([#25981](https://github.com/MetaMask/metamask-extension/pull/25981)) +- chore: Pass along hashed `rpcUrl` during `CustomNetworkAdded` event ([#26203](https://github.com/MetaMask/metamask-extension/pull/26203)) +- chore: Create a story for PageContainerHeader component ([#26031](https://github.com/MetaMask/metamask-extension/pull/26031)) +- chore: Create a story for GasTiming component ([#25557](https://github.com/MetaMask/metamask-extension/pull/25557)) +- New Crowdin translations by Github Action ([#26230](https://github.com/MetaMask/metamask-extension/pull/26230)) +- chore: update @metamask/bitcoin-wallet-snap to 0.4.0 ([#26229](https://github.com/MetaMask/metamask-extension/pull/26229)) +- fix: flaky test `Sentry errors before initialization, after opting into metrics @no-mmi should send error events in background` ([#26216](https://github.com/MetaMask/metamask-extension/pull/26216)) +- chore: Create a story for NftCollectionImage component ([#26069](https://github.com/MetaMask/metamask-extension/pull/26069)) +- fix: update icons ([#26180](https://github.com/MetaMask/metamask-extension/pull/26180)) +- refactor: replace Typography with Text component in restore-vault.js ([#25636](https://github.com/MetaMask/metamask-extension/pull/25636)) +- chore: Create a story for convert-token-to-nft-modal component ([#25561](https://github.com/MetaMask/metamask-extension/pull/25561)) +- refactor: replace deprecated mixins with Text component in qr-code-view ([#25637](https://github.com/MetaMask/metamask-extension/pull/25637)) +- test: Add manual scenario for network polling scenario ([#26195](https://github.com/MetaMask/metamask-extension/pull/26195)) +- feat: Add experimental settings toggle for transactions redesign ([#26010](https://github.com/MetaMask/metamask-extension/pull/26010)) +- feat: Support Permit variants: PermitSingle, PermitBatch, PermitTransferFrom, PermitBatchTransferFrom, TradeOrder, Seaport ([#26107](https://github.com/MetaMask/metamask-extension/pull/26107)) +- feat: updated dapp permission screen ([#25703](https://github.com/MetaMask/metamask-extension/pull/25703)) +- fix: improve performance in large signature request confirmations ([#26209](https://github.com/MetaMask/metamask-extension/pull/26209)) +- refactor: remove password manager mention ([#25985](https://github.com/MetaMask/metamask-extension/pull/25985)) +- New Crowdin translations by Github Action ([#25939](https://github.com/MetaMask/metamask-extension/pull/25939)) +- chore: remove opera manifest files as they are not used ([#26200](https://github.com/MetaMask/metamask-extension/pull/26200)) +- fix(deps): bump fast-xml-parser from 4.3.4 to 4.4.1. ([#26202](https://github.com/MetaMask/metamask-extension/pull/26202)) +- test: [Snaps E2E] remove unnecessary steps from snaps UI Images test ([#25640](https://github.com/MetaMask/metamask-extension/pull/25640)) +- fix: truncate long tokenId ([#26179](https://github.com/MetaMask/metamask-extension/pull/26179)) +- chore: Add en_GB locale ([#26196](https://github.com/MetaMask/metamask-extension/pull/26196)) +- chore: upgrade to Sentry 8 ([#25999](https://github.com/MetaMask/metamask-extension/pull/25999)) +- refactor: add unlock checks for notification related controllers ([#26189](https://github.com/MetaMask/metamask-extension/pull/26189)) +- fix: interpret multipart errors correctly and allow ignore ([#26113](https://github.com/MetaMask/metamask-extension/pull/26113)) +- feat: migrate global unit tests from Mocha to Jest ([#26104](https://github.com/MetaMask/metamask-extension/pull/26104)) +- fix: node being setup twice ([#26052](https://github.com/MetaMask/metamask-extension/pull/26052)) +- fix: setupControllerConnection outstream end event listener ([#26141](https://github.com/MetaMask/metamask-extension/pull/26141)) +- fix: Address performance issues with 'Portfolio Dashboard' loading in test environment ([#26182](https://github.com/MetaMask/metamask-extension/pull/26182)) +- chore: migrating interactive-replacement-token-page to ts ([#26115](https://github.com/MetaMask/metamask-extension/pull/26115)) +- chore: update @metamask/bitcoin-wallet-snap to 0.3.0 ([#26168](https://github.com/MetaMask/metamask-extension/pull/26168)) +- test: fix potential api-spec test race condition when adding to task queue ([#26171](https://github.com/MetaMask/metamask-extension/pull/26171)) +- fix(user-preference-currency-display): remove unused prop ethLogoHeight ([#24517](https://github.com/MetaMask/metamask-extension/pull/24517)) +- fix: update logos for flare-mainnet and songbird ([#25560](https://github.com/MetaMask/metamask-extension/pull/25560)) +- feat: define account name during creation ([#25191](https://github.com/MetaMask/metamask-extension/pull/25191)) +- chore: MMI move custody component to TS ([#26096](https://github.com/MetaMask/metamask-extension/pull/26096)) +- chore: add portfolio ephemeral domain URL ([#26163](https://github.com/MetaMask/metamask-extension/pull/26163)) +- fix: Flaky test `4byte setting ` ([#26111](https://github.com/MetaMask/metamask-extension/pull/26111)) +- fix: PPOM blockaid update ([#26154](https://github.com/MetaMask/metamask-extension/pull/26154)) +- fix: flaky BTC e2e tests ([#26082](https://github.com/MetaMask/metamask-extension/pull/26082)) +- chore: Add extra event props ([#26123](https://github.com/MetaMask/metamask-extension/pull/26123)) +- refactor: fix event names used to track notifications ([#25521](https://github.com/MetaMask/metamask-extension/pull/25521)) +- test: [Snaps E2E] Create test for snap dialog JSX functionality ([#25493](https://github.com/MetaMask/metamask-extension/pull/25493)) +- chore: update BNB logos ([#26140](https://github.com/MetaMask/metamask-extension/pull/26140)) +- chore: cleanup `.prettierignore` file ([#24828](https://github.com/MetaMask/metamask-extension/pull/24828)) +- chore: Bump `@metamask/ens-controller` to v12 ([#26127](https://github.com/MetaMask/metamask-extension/pull/26127)) +- chore: Bump `@metamask/transaction-controller` to v34 ([#26124](https://github.com/MetaMask/metamask-extension/pull/26124)) +- chore: Create a story for Snackbar component ([#25515](https://github.com/MetaMask/metamask-extension/pull/25515)) +- fix: add new helper function for `openMenuSafe` to mitigate all ocurrences for opening menu with MMI build ([#26079](https://github.com/MetaMask/metamask-extension/pull/26079)) +- feat: make add-team-label use the reusable workflow ([#25807](https://github.com/MetaMask/metamask-extension/pull/25807)) +- chore: MMI-5301 adds enums for custody type and status ([#26006](https://github.com/MetaMask/metamask-extension/pull/26006)) +- fix: remove btc account from permission connect lists ([#25980](https://github.com/MetaMask/metamask-extension/pull/25980)) +- feat: update network list item to include start accessory and end ([#25507](https://github.com/MetaMask/metamask-extension/pull/25507)) +- chore: mmi 5305 mmi pages typescript migration ([#26081](https://github.com/MetaMask/metamask-extension/pull/26081)) +- fix: Move Snaps hooks out of code fence ([#26120](https://github.com/MetaMask/metamask-extension/pull/26120)) +- feat: Mitigate risk for distracted users on queued transactions from different dApps ([#25852](https://github.com/MetaMask/metamask-extension/pull/25852)) +- feat: Add metrics event for advanced details section toggling ([#26083](https://github.com/MetaMask/metamask-extension/pull/26083)) +- fix: display link to privacy-policy explanation in onboarding flow ([#26038](https://github.com/MetaMask/metamask-extension/pull/26038)) +- chore: Create a story for InvalidCustomNetworkAlert component ([#25600](https://github.com/MetaMask/metamask-extension/pull/25600)) +- fix: number formatting on swap + send tx detail ([#26029](https://github.com/MetaMask/metamask-extension/pull/26029)) +- fix: Flaky test `Account Custom Name..` ([#26062](https://github.com/MetaMask/metamask-extension/pull/26062)) +- fix: snap flakiness on `installSnapSimpleKeyring` function ([#26039](https://github.com/MetaMask/metamask-extension/pull/26039)) +- fix: lock Chrome version to 126 ([#26101](https://github.com/MetaMask/metamask-extension/pull/26101)) +- fix: remove halo for tokens ([#26016](https://github.com/MetaMask/metamask-extension/pull/26016)) +- refactor: replace typography with text component in creation-successful.js ([#25552](https://github.com/MetaMask/metamask-extension/pull/25552)) +- fix: `vault decryption` broken tests due to update on window handling ([#26074](https://github.com/MetaMask/metamask-extension/pull/26074)) +- docs: Centralize Author/Team Mapping for Commit Tracking ([#25986](https://github.com/MetaMask/metamask-extension/pull/25986)) +- fix: flaky test: Check the toggle for hex data ([#25899](https://github.com/MetaMask/metamask-extension/pull/25899)) +- chore: migrated institutional ui components to ts ([#25858](https://github.com/MetaMask/metamask-extension/pull/25858)) +- chore: removed unused component ([#26000](https://github.com/MetaMask/metamask-extension/pull/26000)) +- chore: update Bitcoin Snap to version 0.2.5 ([#26058](https://github.com/MetaMask/metamask-extension/pull/26058)) +- refactor: replace Typography with Text component in metametrics.js ([#25630](https://github.com/MetaMask/metamask-extension/pull/25630)) +- refactor: replace typography with text component in review recovery phrase ([#25265](https://github.com/MetaMask/metamask-extension/pull/25265)) +- test: new switchToWindowWithTitle w/ Extension communication ([#25362](https://github.com/MetaMask/metamask-extension/pull/25362)) +- ci: Trimming the gitdiff output before writing to output file ([#26057](https://github.com/MetaMask/metamask-extension/pull/26057)) +- chore: tweak send page styling ([#25982](https://github.com/MetaMask/metamask-extension/pull/25982)) +- fix: mmi flaky tests `Reveal SRP through settings completes quiz and reveals SRP QR after wrong answers` , `Sign Typed Data Signature Request can initiate and reject a Signature Request of Sign Typed Data`, `Sign Typed Data Signature Request can queue multiple Signature Requests of Sign Typed Data and confirm` ([#26055](https://github.com/MetaMask/metamask-extension/pull/26055)) +- chore: Create a story for IconButton component ([#25277](https://github.com/MetaMask/metamask-extension/pull/25277)) +- fix: center token icon ([#26013](https://github.com/MetaMask/metamask-extension/pull/26013)) +- fix: flaky test `Import flow @no-mmi Import wallet using Secret Recovery Phrase with pasting word by word` ([#26049](https://github.com/MetaMask/metamask-extension/pull/26049)) +- fix: flaky test 25912 ([#25913](https://github.com/MetaMask/metamask-extension/pull/25913)) +- chore: add privacy query params to portfolio navigation ([#25958](https://github.com/MetaMask/metamask-extension/pull/25958)) +- chore: Temporarily disable Playwright Swaps tests ([#26050](https://github.com/MetaMask/metamask-extension/pull/26050)) +- fix: Remove special reject button case from api spec tests ([#26048](https://github.com/MetaMask/metamask-extension/pull/26048)) +- test(e2e): unlock trezor account ([#25824](https://github.com/MetaMask/metamask-extension/pull/25824)) +- fix: Flaky "Signature Approved Event" e2e test ([#26040](https://github.com/MetaMask/metamask-extension/pull/26040)) +- feat: Migration #122 set redesignedConfirmationsEnabled to true ([#25769](https://github.com/MetaMask/metamask-extension/pull/25769)) +- fix: Revert "refactor: use withKeyring method (#25435)" ([#25435](https://github.com/MetaMask/metamask-extension/pull/25435)) +- fix: :label: update the text in the popup to enable notifications ([#26026](https://github.com/MetaMask/metamask-extension/pull/26026)) +- fix: map the supported block explorers ([#25908](https://github.com/MetaMask/metamask-extension/pull/25908)) +- fix: update css for modals ([#25961](https://github.com/MetaMask/metamask-extension/pull/25961)) +- fix: Fix permssions for `update-attributions` workflow ([#26019](https://github.com/MetaMask/metamask-extension/pull/26019)) +- fix: add migration for profile syncing controller ([#26004](https://github.com/MetaMask/metamask-extension/pull/26004)) +- test: Adding e2e for SIWE and re-enabling redesign for SIWE ([#25831](https://github.com/MetaMask/metamask-extension/pull/25831)) +- test: UX: Multichain: Add E2E for signaling network change from Network menu to dapp, Autoswitching networks ([#25765](https://github.com/MetaMask/metamask-extension/pull/25765)) +- feat: Move ENABLE_CONFIRMATION_REDESIGN feature flag to the developer settings page ([#25520](https://github.com/MetaMask/metamask-extension/pull/25520)) +- fix: `yarn:start:test:flask` is broken `Lavapack is not defined` ([#25995](https://github.com/MetaMask/metamask-extension/pull/25995)) +- feat: add utility function to get supported chains from the Security Alerts API ([#25716](https://github.com/MetaMask/metamask-extension/pull/25716)) +- fix: `vault-decryption` test since the order of announcement modals changed ([#25997](https://github.com/MetaMask/metamask-extension/pull/25997)) +- fix: updated switch to this account condition ([#25609](https://github.com/MetaMask/metamask-extension/pull/25609)) +- fix: flaky test Settings Redirects to ENS domains when user inputs ENS into address bar ([#25782](https://github.com/MetaMask/metamask-extension/pull/25782)) +- chore: MMI-5248 introduce the token allowance functionality for MMI ([#25967](https://github.com/MetaMask/metamask-extension/pull/25967)) +- fix: vertically align asset image ([#25988](https://github.com/MetaMask/metamask-extension/pull/25988)) +- feat: Adding state per window in e2e, excluding null state ([#25900](https://github.com/MetaMask/metamask-extension/pull/25900)) +- fix: attribution link ([#25947](https://github.com/MetaMask/metamask-extension/pull/25947)) +- feat: Enable hardware wallets for smart transactions in swaps ([#25742](https://github.com/MetaMask/metamask-extension/pull/25742)) +- fix: fix link redirection ([#25983](https://github.com/MetaMask/metamask-extension/pull/25983)) +- fix: fix overlapping modals ([#25962](https://github.com/MetaMask/metamask-extension/pull/25962)) +- feat: Show the Close extension button on the Smart Transaction Status Page for a pending dapp transaction ([#25965](https://github.com/MetaMask/metamask-extension/pull/25965)) +- fix(multichain): use accounts{Added,Removed} to fetch/clear balances ([#25884](https://github.com/MetaMask/metamask-extension/pull/25884)) +- test: Add integration tests for permit simulation section ([#25856](https://github.com/MetaMask/metamask-extension/pull/25856)) +- fix: fixed max width for permissions page ([#25870](https://github.com/MetaMask/metamask-extension/pull/25870)) +- fix: show current network if domains are undefined ([#25960](https://github.com/MetaMask/metamask-extension/pull/25960)) +- fix: notification slowness and crashes ([#25946](https://github.com/MetaMask/metamask-extension/pull/25946)) +- ci: Disabling non-lint CI on the l10n_crowdin_action branch ([#25809](https://github.com/MetaMask/metamask-extension/pull/25809)) +- refactor: use `withKeyring` method ([#25435](https://github.com/MetaMask/metamask-extension/pull/25435)) +- feat: add BTC support survey link ([#25875](https://github.com/MetaMask/metamask-extension/pull/25875)) +- fix: re-organize files under assets folder ([#25897](https://github.com/MetaMask/metamask-extension/pull/25897)) +- fix: fix css nft detail ([#25931](https://github.com/MetaMask/metamask-extension/pull/25931)) +- fix: Implement Auto-Enable Feature for Basic Functionality in Metamask Extension v12.1.0 ([#25944](https://github.com/MetaMask/metamask-extension/pull/25944)) +- fix: Handle error when offscreen document already exists ([#25138](https://github.com/MetaMask/metamask-extension/pull/25138)) +- test: Expand coverage of sourcemap validator ([#25115](https://github.com/MetaMask/metamask-extension/pull/25115)) +- feat: Add full screen Snap Home and Dialog ([#25670](https://github.com/MetaMask/metamask-extension/pull/25670)) +- chore: swaps codeowners reorg ([#24803](https://github.com/MetaMask/metamask-extension/pull/24803)) +- fix: track token detection enabled ([#25822](https://github.com/MetaMask/metamask-extension/pull/25822)) +- fix: rm locales in other languages ([#25936](https://github.com/MetaMask/metamask-extension/pull/25936)) +- fix: fix ([#25907](https://github.com/MetaMask/metamask-extension/pull/25907)) +- fix: password reset ([#25847](https://github.com/MetaMask/metamask-extension/pull/25847)) +- New Crowdin translations by Github Action ([#24889](https://github.com/MetaMask/metamask-extension/pull/24889)) +- fix: Remove abandoned test:unit:jest command ([#25905](https://github.com/MetaMask/metamask-extension/pull/25905)) +- fix(22851): check if active device to prevent autoconnect for hw ([#25503](https://github.com/MetaMask/metamask-extension/pull/25503)) +- test: Removed step from e2e tests ([#25910](https://github.com/MetaMask/metamask-extension/pull/25910)) +- fix: calcTokenAmount BigNumber more than 15 digits error ([#25799](https://github.com/MetaMask/metamask-extension/pull/25799)) +- feat: add custom form check alerts ([#25259](https://github.com/MetaMask/metamask-extension/pull/25259)) +- fix: test failure on firefox ([#25895](https://github.com/MetaMask/metamask-extension/pull/25895)) +- fix: disables "swap and send" for MMI ([#25886](https://github.com/MetaMask/metamask-extension/pull/25886)) +- fix: Fixed flaky test 24645 ([#25786](https://github.com/MetaMask/metamask-extension/pull/25786)) +- chore: refactor SwapsController so it extends from BaseControllerV2 ([#25681](https://github.com/MetaMask/metamask-extension/pull/25681)) +- feat: Replace "Manage in settings" with "No thanks" in the STX Opt In modal, only show the modal for non-zero balances ([#25848](https://github.com/MetaMask/metamask-extension/pull/25848)) +- feat: Display advanced section within confirmation by default for some users ([#25687](https://github.com/MetaMask/metamask-extension/pull/25687)) +- chore: bump assets-controllers to v36.0.0 ([#25857](https://github.com/MetaMask/metamask-extension/pull/25857)) +- fix: add name to scuttling exception list ([#25849](https://github.com/MetaMask/metamask-extension/pull/25849)) +- fix: update build version to align with firefox's newer version restrictions ([#25456](https://github.com/MetaMask/metamask-extension/pull/25456)) +- feat: regression label ([#25691](https://github.com/MetaMask/metamask-extension/pull/25691)) +- chore: Master sync ([#25816](https://github.com/MetaMask/metamask-extension/pull/25816)) +- fix: contract data in metrics ([#25759](https://github.com/MetaMask/metamask-extension/pull/25759)) +- fix: flaky test `ERC721 NFTs testdapp interaction` ([#25854](https://github.com/MetaMask/metamask-extension/pull/25854)) +- fix: flaky test `Create BTC Account cannot create multiple BTC accounts...` ([#25861](https://github.com/MetaMask/metamask-extension/pull/25861)) +- feat: support creation of Bitcoin testnet accounts ([#25772](https://github.com/MetaMask/metamask-extension/pull/25772)) +- fix: use of an header in a dedicated call ([#25828](https://github.com/MetaMask/metamask-extension/pull/25828)) +- feat(tests): add btc e2e tests ([#25663](https://github.com/MetaMask/metamask-extension/pull/25663)) +- feat: NFT details new design ([#25524](https://github.com/MetaMask/metamask-extension/pull/25524)) +- feat: Add fuzzy matching for name lookup ([#25264](https://github.com/MetaMask/metamask-extension/pull/25264)) +- fix: edit path to dist folder ([#25826](https://github.com/MetaMask/metamask-extension/pull/25826)) +- chore: Patch security issue in snaps-utils ([#25827](https://github.com/MetaMask/metamask-extension/pull/25827)) +- feat: add option of copy to info row component ([#25682](https://github.com/MetaMask/metamask-extension/pull/25682)) +- fix: skip blockaid validations for users internal accounts ([#25695](https://github.com/MetaMask/metamask-extension/pull/25695)) +- chore: refactor custody component ([#25684](https://github.com/MetaMask/metamask-extension/pull/25684)) +- Merge origin/develop into master-sync +- chore: update @metamask/bitcoin-wallet-snap to 0.2.4 ([#25808](https://github.com/MetaMask/metamask-extension/pull/25808)) +- chore: removed unused getCustodianAccountsByAddress method ([#25798](https://github.com/MetaMask/metamask-extension/pull/25798)) +- feat: Make Jest unit tests run faster in GitHub actions ([#25726](https://github.com/MetaMask/metamask-extension/pull/25726)) +- revert: un-revert metrics and signature refactor test ([#25758](https://github.com/MetaMask/metamask-extension/pull/25758)) +- feat: Add `ui_customizations` metric for transactions ([#25736](https://github.com/MetaMask/metamask-extension/pull/25736)) +- test: add e2e tests for navigation (#25652) ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) +- chore: remove `BTC_BETA_SUPPORT` flag ([#25776](https://github.com/MetaMask/metamask-extension/pull/25776)) +- chore: update @metamask/bitcoin-wallet-snap to 0.2.3 ([#25775](https://github.com/MetaMask/metamask-extension/pull/25775)) +- feat: add more whitelisted portfolio URLs ([#25767](https://github.com/MetaMask/metamask-extension/pull/25767)) +- fix: Fix page width for fullscreen mode send page ([#25639](https://github.com/MetaMask/metamask-extension/pull/25639)) +- chore: Update Snaps codeowners list ([#25581](https://github.com/MetaMask/metamask-extension/pull/25581)) +- fix: fine-tune for `Delineator` component styles ([#25760](https://github.com/MetaMask/metamask-extension/pull/25760)) +- feat: decode transaction data ([#25597](https://github.com/MetaMask/metamask-extension/pull/25597)) +- feat: add `Delineator` component ([#25610](https://github.com/MetaMask/metamask-extension/pull/25610)) +- feat(ramps): update isNativeTokenBuyable to include BTC ([#25621](https://github.com/MetaMask/metamask-extension/pull/25621)) +- fix: Fix issue 25285 max insufficient funds for gas ([#25574](https://github.com/MetaMask/metamask-extension/pull/25574)) +- feat: add BTC experimental toggle ([#25672](https://github.com/MetaMask/metamask-extension/pull/25672)) +- build: bump gas-fee-controller to v18 and remove patch ([#25679](https://github.com/MetaMask/metamask-extension/pull/25679)) +- fix: show correct asset and balance when BTC account is the selected account ([#25719](https://github.com/MetaMask/metamask-extension/pull/25719)) +- feat(btc): add BTC account creation menu entry ([#25625](https://github.com/MetaMask/metamask-extension/pull/25625)) +- fix: flaky test `Test Snap Metrics test snap update rejected metric` ([#25744](https://github.com/MetaMask/metamask-extension/pull/25744)) +- chore(deps): bump @metamask/accounts-controller from ^17.0.0 to ^17.2.0 ([#25676](https://github.com/MetaMask/metamask-extension/pull/25676)) +- fix: use LAVAMOAT_UPDATE_TOKEN in attributions workflow ([#25731](https://github.com/MetaMask/metamask-extension/pull/25731)) +- fix: caveat mutations for non-EVM accounts ([#25739](https://github.com/MetaMask/metamask-extension/pull/25739)) +- test: Add UI integration tests ([#24428](https://github.com/MetaMask/metamask-extension/pull/24428)) +- fix: revert "test: add e2e tests for navigation (#25652)" ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) +- chore: Revert "test: e2e metrics test and refactor" ([#25722](https://github.com/MetaMask/metamask-extension/pull/25722)) +- feat: bundle pre-installed Bitcoin Wallet Snap ([#25715](https://github.com/MetaMask/metamask-extension/pull/25715)) +- fix: protect against phishing domain redirects in main/sub frames for http(s) requests ([#25153](https://github.com/MetaMask/metamask-extension/pull/25153)) +- fix: Fix crash of Transaction screen with smart transaction ([#25717](https://github.com/MetaMask/metamask-extension/pull/25717)) +- fix: Hide MMI Account Mistmatch BannerAlert from Sign-in with Ethereum (SIWE) Redesign Page ([#25662](https://github.com/MetaMask/metamask-extension/pull/25662)) +- fix: flaky test `Create token, approve token and approve token without gas approves an already created token and displays the token approval data` ([#25706](https://github.com/MetaMask/metamask-extension/pull/25706)) +- feat: Enable SIWE Signature Redesign ([#25660](https://github.com/MetaMask/metamask-extension/pull/25660)) +- fix: flaky test `Request-queue UI changes handles three confirmations on three confirmations concurrently` ([#25675](https://github.com/MetaMask/metamask-extension/pull/25675)) +- feat: move unit tests from Circleci to Github actions ([#25570](https://github.com/MetaMask/metamask-extension/pull/25570)) +- test: e2e metrics test and refactor ([#25632](https://github.com/MetaMask/metamask-extension/pull/25632)) +- test: add e2e tests for navigation ([#25652](https://github.com/MetaMask/metamask-extension/pull/25652)) +- feat: support security alerts API ([#25544](https://github.com/MetaMask/metamask-extension/pull/25544)) +- feat(ramps): add flag to ensure ramp networks are only fetched once ([#25686](https://github.com/MetaMask/metamask-extension/pull/25686)) +- fix: allow ramps dev environment on Flask ([#25659](https://github.com/MetaMask/metamask-extension/pull/25659)) +- feat: added check for if the selected account is BTC in transaction-list ([#25642](https://github.com/MetaMask/metamask-extension/pull/25642)) +- feat: Gas Fees Redesign PoC ([#24714](https://github.com/MetaMask/metamask-extension/pull/24714)) +- fix: show connected toast only for EVM accounts ([#25628](https://github.com/MetaMask/metamask-extension/pull/25628)) +- fix: changed logic to use the new banner alert ([#25626](https://github.com/MetaMask/metamask-extension/pull/25626)) +- fix: set network client id for domain ([#25646](https://github.com/MetaMask/metamask-extension/pull/25646)) +- feat: improvement for how we display big and small numbers ([#25438](https://github.com/MetaMask/metamask-extension/pull/25438)) +- chore: restore bot workflow to update attributions ([#25211](https://github.com/MetaMask/metamask-extension/pull/25211)) +- test: add swap e2e tests on Tenderly network ([#25060](https://github.com/MetaMask/metamask-extension/pull/25060)) +- fix: UX: Multichain: Add safeguard to throw error when confirmation chainId doesn't match current chainId ([#25634](https://github.com/MetaMask/metamask-extension/pull/25634)) +- chore: updates MMI custody controller ([#25631](https://github.com/MetaMask/metamask-extension/pull/25631)) +- fix: flaky test `Test Snap Get Locale test snap_getLocale functionality` ([#25648](https://github.com/MetaMask/metamask-extension/pull/25648)) +- fix: Skip blockaid validation for SIWE signature types ([#25612](https://github.com/MetaMask/metamask-extension/pull/25612)) +- feat: Add support for security alerts on zkSync, Berachain, Scroll and Metachain One on extension ([#25555](https://github.com/MetaMask/metamask-extension/pull/25555)) +- fix: Multichain: UX: Check for transactions on all networks and QueuedRequestCount ([#25614](https://github.com/MetaMask/metamask-extension/pull/25614)) +- feat: define which keyring methods Portfolio can call ([#25633](https://github.com/MetaMask/metamask-extension/pull/25633)) +- chore: flaky E2E tests improved ([#25565](https://github.com/MetaMask/metamask-extension/pull/25565)) +- feat: add SIWE mismatch account warning alert ([#25613](https://github.com/MetaMask/metamask-extension/pull/25613)) +- fix: support multichain in blockexplorer and qr code ([#25526](https://github.com/MetaMask/metamask-extension/pull/25526)) +- fix: decimal places displayed on token value on permit pages ([#25410](https://github.com/MetaMask/metamask-extension/pull/25410)) +- feat: added BTC variant to ramps-card and illustration image ([#25615](https://github.com/MetaMask/metamask-extension/pull/25615)) +- fix: Remove unused fixtures and fix test name in smart swaps disabled spec ([#25616](https://github.com/MetaMask/metamask-extension/pull/25616)) +- chore: Update @metamask/smart-transactions-controller from 10.1.2 to 10.1.6 ([#25611](https://github.com/MetaMask/metamask-extension/pull/25611)) +- fix: Fix issue 22837 about unknown error during ledger pair ([#25462](https://github.com/MetaMask/metamask-extension/pull/25462)) +- test: add e2e to swap with snap account ([#25558](https://github.com/MetaMask/metamask-extension/pull/25558)) +- chore: exclude running git diff job for the e2e quality gate in `develop`, `master` and release branches ([#25605](https://github.com/MetaMask/metamask-extension/pull/25605)) +- chore: [Delivery] Update author mapping list for PR ([#25606](https://github.com/MetaMask/metamask-extension/pull/25606)) +- fix: page object selector not found ([#25624](https://github.com/MetaMask/metamask-extension/pull/25624)) +- test: Initial PR for integrating the Page Object Model (POM) into e2e test suite ([#25373](https://github.com/MetaMask/metamask-extension/pull/25373)) +- refactor: Replace deprecated mixins with Text component in selected-account.component.js ([#25262](https://github.com/MetaMask/metamask-extension/pull/25262)) +- refactor: Replace deprecated mixins with Text component in unlock-page.component.js ([#25227](https://github.com/MetaMask/metamask-extension/pull/25227)) +- chore: Create a story for RestoreVaultPage component ([#25284](https://github.com/MetaMask/metamask-extension/pull/25284)) +- fix(snaps): Fix alignment of install origin is `snap-authorship-expanded` ([#25583](https://github.com/MetaMask/metamask-extension/pull/25583)) +- feat: Remove blockaid migration BannerAlert ([#25556](https://github.com/MetaMask/metamask-extension/pull/25556)) +- fix: failingt e2e `Click bridge button from asset page @no-mmi loads portfolio tab when flag is turned off` ([#25607](https://github.com/MetaMask/metamask-extension/pull/25607)) +- chore: adds quality gate for rerunning e2e spec files that are new or have been modified ([#24556](https://github.com/MetaMask/metamask-extension/pull/24556)) +- chore(deps): bump assets controller to v34.0.0 ([#25540](https://github.com/MetaMask/metamask-extension/pull/25540)) +- chore: add bridge controller, store and api utils ([#25044](https://github.com/MetaMask/metamask-extension/pull/25044)) +- fix: add eth_signTypedData and eth_signTypedData_v3 to `methodsRequiringNetworkSwitch` ([#25562](https://github.com/MetaMask/metamask-extension/pull/25562)) + ## [12.1.0] ### Added - Launched a feature displaying the percentage increase or decrease for tokens within the UI ([#24223](https://github.com/MetaMask/metamask-extension/pull/24223)) @@ -4996,7 +5404,8 @@ Update styles and spacing on the critical error page ([#20350](https://github.c - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v12.4.0...HEAD +[12.4.0]: https://github.com/MetaMask/metamask-extension/compare/v12.1.0...v12.4.0 [12.1.0]: https://github.com/MetaMask/metamask-extension/compare/v12.0.6...v12.1.0 [12.0.6]: https://github.com/MetaMask/metamask-extension/compare/v12.0.5...v12.0.6 [12.0.5]: https://github.com/MetaMask/metamask-extension/compare/v12.0.4...v12.0.5 diff --git a/README.md b/README.md index 5c3e8f5823c1..531a1e4fc3b6 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ If you are not a MetaMask Internal Developer, or are otherwise developing on a f - If debugging unhandled exceptions, you'll need to add a value for `SENTRY_DSN` [Sentry Dsn](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), see [Developing on MetaMask - Sentry](./development/README.md#sentry). - Optionally, replace the `PASSWORD` value with your development wallet password to avoid entering it each time you open the app. - Run `yarn install` to install the dependencies. -- Build the project to the `./dist/` folder with `yarn dist`. +- Build the project to the `./dist/` folder with `yarn dist` (for Chromium-based browsers) or `yarn dist:mv2` (for Firefox) - - Optionally, you may run `yarn start` to run dev mode. + - Optionally, to create a development build you can instead run `yarn start` (for Chromium-based browsers) or `yarn start:mv2` (for Firefox) - Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. - See the [build system readme](./development/build/README.md) for build system usage information. @@ -85,6 +85,7 @@ To start a development build (e.g. with logging and file watching) run `yarn sta Alternatively, one can skip wallet onboarding and preload the vault state with a specific SRP by adding `TEST_SRP=''` and `PASSWORD=''` to the `.metamaskrc` file and running `yarn start:skip-onboarding`. +You can also start a development build using the `yarn webpack` command, or `yarn webpack --watch`. This uses an alternative build system that is much faster, but not yet production ready. See the [Webpack README](./development/webpack/README.md) for more information. #### React and Redux DevTools @@ -138,7 +139,7 @@ Note: The `yarn start:test` command (which initiates the testDev build type) has Once you have your test build ready, choose the browser for your e2e tests: - For Firefox, run `yarn test:e2e:firefox`. - - Note: If you are running Firefox as a snap package on Linux, ensure you enable the appropriate environment variable: `FIREFOX_SNAP=true yarn test:e2e:firefox` + - Note: If you are running Firefox as a snap package on Linux, ensure you enable the appropriate environment variable: `FIREFOX_SNAP=true yarn test:e2e:firefox` - For Chrome, run `yarn test:e2e:chrome`. These scripts support additional options for debugging. Use `--help`to see all available options. diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index ff81307825d1..5e1b32229bd1 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -42,7 +42,7 @@ "message": "Verbinden Sie Ihre QR-basierte Hardware-Wallet." }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (demnächst)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Die Adresse in der Anmeldeanfrage entspricht nicht der Adresse des Kontos, mit dem Sie sich anmelden." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Sie müssen ein Konto auswählen!" }, + "accountTypeNotSupported": { + "message": "Kontotyp nicht unterstützt" + }, "accounts": { "message": "Konten" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Ein neues Konto hinzufügen" }, + "addNewBitcoinAccount": { + "message": "Ein neues Bitcoin-Konto hinzufügen (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Ein neues Bitcoin-Konto hinzufügen (Testnet)" + }, "addNewToken": { "message": "Neues Token hinzufügen" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "NFTs hinzufügen" }, + "addRpcUrl": { + "message": "RPC-URL hinzufügen" + }, "addSnapAccountToggle": { "message": "„Konto-Snap (Beta) hinzufügen“ aktivieren" }, @@ -305,12 +317,21 @@ "message": "Sie können kein Token finden? Sie können ein beliebiges Token manuell hinzufügen, indem Sie seine Adresse eingeben. Token-Contract-Adressen finden Sie auf $1.", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "URL hinzufügen" + }, "addingCustomNetwork": { "message": "Netzwerk wird hinzugefügt" }, "addingTokens": { "message": "Hinzufügen von Token" }, + "additionalNetworks": { + "message": "Zusätzliche Netzwerke" + }, + "additionalRpcUrl": { + "message": "Weitere RPC-URL" + }, "address": { "message": "Adresse" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Erweiterte Einstellungen" }, + "advancedDetailsDataDesc": { + "message": "Daten" + }, + "advancedDetailsHexDesc": { + "message": "Hexadezimal" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Dies ist die Transaktionsnummer eines Kontos. Die Nonce für die erste Transaktion ist 0 und sie erhöht sich in fortlaufender Reihenfolge." + }, "advancedGasFeeDefaultOptIn": { "message": "Speichern Sie diese Werte als Standard für das $1-Netzwerk.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Warnhinweis" }, + "alertActionBuy": { + "message": "EHT kaufen" + }, + "alertActionUpdateGas": { + "message": "Gas-Limit aktualisieren" + }, + "alertActionUpdateGasFee": { + "message": "Gebühr aktualisieren" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Gas-Optionen aktualisieren" + }, "alertBannerMultipleAlertsDescription": { "message": "Wenn Sie diese Anfrage genehmigen, könnten Dritte, die für Betrügereien bekannt sind, alle Ihre Assets an sich reißen." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Dies kann in „Einstellungen > Benachrichtigungen“ geändert werden." }, + "alertMessageGasEstimateFailed": { + "message": "Wir sind nicht in der Lage, eine genaue Gebühr anzugeben, und diese Schätzung könnte zu hoch sein. Wir schlagen vor, dass Sie ein individuelles Gas-Limit eingeben, aber es besteht das Risiko, dass die Transaktion trotzdem fehlschlägt." + }, + "alertMessageGasFeeLow": { + "message": "Wenn Sie eine niedrige Gebühr wählen, müssen Sie mit langsameren Transaktionen und längeren Wartezeiten rechnen. Für schnellere Transaktionen wählen Sie die Gebührenoptionen Markt oder Aggressiv." + }, + "alertMessageGasTooLow": { + "message": "Um mit dieser Transaktion fortzufahren, müssen Sie das Gas-Limit auf 21.000 oder mehr erhöhen." + }, + "alertMessageInsufficientBalance": { + "message": "Sie haben nicht genug ETH auf Ihrem Konto, um die Transaktionsgebühren zu bezahlen." + }, + "alertMessageNetworkBusy": { + "message": "Die Gas-Preise sind hoch und die Schätzungen sind weniger genau." + }, + "alertMessageNoGasPrice": { + "message": "Wir können mit dieser Transaktion nicht fortfahren, bis Sie die Gebühr manuell aktualisieren." + }, + "alertMessagePendingTransactions": { + "message": "Diese Transaktion wird erst dann durchgeführt, wenn eine vorherige Transaktion abgeschlossen ist. Erfahren Sie, wie Sie eine Transaktion abbrechen oder beschleunigen können." + }, + "alertMessageSignInDomainMismatch": { + "message": "Die Website, die die Anfrage stellt, ist nicht die Website, bei der Sie sich anmelden. Dies könnte ein Versuch sein, Ihre Anmeldedaten zu stehlen." + }, + "alertMessageSignInWrongAccount": { + "message": "Diese Seite fordert Sie auf, sich mit dem falschen Konto anzumelden." + }, + "alertMessageSigningOrSubmitting": { + "message": "Diese Transaktion wird erst durchgeführt, wenn Ihre vorherige Transaktion abgeschlossen ist." + }, "alertModalAcknowledge": { "message": "Ich habe das Risiko erkannt und möchte trotzdem fortfahren" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Alle Benachrichtigungen überprüfen" }, + "alertReasonGasEstimateFailed": { + "message": "Ungenaue Gebühr" + }, + "alertReasonGasFeeLow": { + "message": "Langsame Geschwindigkeit" + }, + "alertReasonGasTooLow": { + "message": "Niedriges Gas-Limit" + }, + "alertReasonInsufficientBalance": { + "message": "Unzureichende Gelder" + }, + "alertReasonNetworkBusy": { + "message": "Netzwerk ist ausgelastet" + }, + "alertReasonNoGasPrice": { + "message": "Gebührenschätzung nicht verfügbar" + }, + "alertReasonPendingTransactions": { + "message": "Ausstehende Transaktion" + }, + "alertReasonSignIn": { + "message": "Verdächtige Anmeldeanfrage" + }, + "alertReasonWrongAccount": { + "message": "Falsches Konto" + }, "alertSettingsUnconnectedAccount": { "message": "Eine Webseite mit einem nicht verknüpften Konto durchsuchen" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Alle Genehmigungen" }, + "allTimeHigh": { + "message": "Allzeithoch" + }, + "allTimeLow": { + "message": "Allzeittief" + }, "allYourNFTsOf": { "message": "Alle Ihre NFTs von $1.", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 und $2 ", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Ankündigungen" - }, "appDescription": { "message": "Ethereum Browsererweiterung", "description": "The description of the application" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Versuch, den Swap kostenlos zu stornieren" }, + "attributes": { + "message": "Attribute" + }, "attributions": { "message": "Zuschreibungen" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Grundfunktionalität ist ausgeschaltet" }, + "basicConfigurationDescription": { + "message": "MetaMask bietet grundlegende Funktionen wie Token-Details und Gas-Einstellungen über Internetdienste. Wenn Sie Internetdienste nutzen, wird Ihre IP-Adresse weitergegeben, in diesem Fall an MetaMask. Das ist genau so, wie wenn Sie eine beliebige Website besuchen. MetaMask verwendet diese Daten vorübergehend und verkauft Ihre Daten niemals. Sie können ein VPN verwenden oder diese Dienste abschalten, aber das kann Ihr MetaMask-Erlebnis beeinträchtigen. Um mehr zu erfahren, lesen Sie unsere $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Grundfunktionalität" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask Beta wird Sie nie nach Ihrer geheimen Wiederherstellungsphrase fragen." }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Bitcoin-Aktivität wird nicht unterstützt" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Bei Aktivierung dieser Funktion haben Sie die Möglichkeit, ein Bitcoin-Konto zu Ihrer MetaMask-Erweiterung hinzuzufügen, das von Ihrer bestehenden geheimen Wiederherstellungsphrase abgeleitet ist. Hierbei handelt es sich um eine experimentelle Beta-Funktion, deren Nutzung also auf eigene Gefahr erfolgt. Falls Sie uns Feedback zu dieser neuen Bitcoin-Funktion geben möchten, füllen Sie bitte dieses $1 aus.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Aktivierung der Funktion „Ein neues Bitcoin-Konto hinzufügen (Beta)“" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Bei Aktivierung dieser Funktion haben Sie die Möglichkeit, ein Bitcoin-Konto für das Test-Netzwerk hinzuzufügen." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Aktivierung der Funktion „Ein neues Bitcoin-Konto hinzufügen (Testnet)“" + }, "blockExplorerAccountAction": { "message": "Konto", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Wir empfehlen nicht, mit dieser Anfrage fortzufahren." + }, "blockaidDescriptionApproveFarming": { "message": "Wenn Sie diese Anfrage genehmigen, könnte eine dritte Partei, die für Betrügereien bekannt ist, Ihre gesamten Assets an sich reißen." }, @@ -669,7 +807,7 @@ "message": "Wenn Sie diese Anfrage genehmigen, kann jemand Ihre bei Blur aufgelisteten Assets stehlen." }, "blockaidDescriptionErrored": { - "message": "Aufgrund eines Fehlers wurde diese Anfrage vom Sicherheitsanbieter nicht überprüft. Gehen Sie mit Bedacht vor." + "message": "Aufgrund eines Fehlers konnten wir nicht auf Sicherheitsalarme prüfen. Fahren Sie nur fort, wenn Sie jeder involvierten Adresse vertrauen." }, "blockaidDescriptionMaliciousDomain": { "message": "Sie interagieren mit einer schädlichen Domain. Wenn Sie diese Anfrage bestätigen, verlieren Sie eventuell Ihre Assets." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Wenn Sie diese Anfrage genehmigen, wird eine dritte Partei, die für Betrügereien bekannt ist, Ihre gesamten Assets an sich reißen." }, + "blockaidDescriptionWarning": { + "message": "Dies könnte eine betrügerische Anfrage sein. Fahren Sie nur fort, wenn Sie allen beteiligten Adressen vertrauen." + }, "blockaidMessage": { "message": "Wahrung der Privatsphäre – keine Daten werden an Dritte weitergegeben. Verfügbar auf Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Base und Sepolia." }, @@ -690,7 +831,7 @@ "message": "Dies ist eine betrügerische Anfrage." }, "blockaidTitleMayNotBeSafe": { - "message": "Die Anfrage ist möglicherweise nicht sicher." + "message": "Seien Sie vorsichtig" }, "blockaidTitleSuspicious": { "message": "Dies ist eine verdächtige Anfrage." @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Gekauft für" + }, "bridge": { "message": "Bridge" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Sie müssen MetaMask unter Google Chrome nutzen, um sich mit Ihrer Hardware-Wallet zu verbinden." }, + "circulatingSupply": { + "message": "Zirkulierende Versorgung" + }, "clear": { "message": "Löschen" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Klicken Sie hier, um die Tokens manuell hinzuzufügen." + "message": "Sie können Tokens jederzeit manuell hinzufügen." }, "close": { "message": "Schließen" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Name der Sammlung" + }, "comboNoOptions": { "message": "Keine Option gefunden", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Ich habe die Benachrichtigungen zur Kenntnis genommen und möchte trotzdem fortfahren" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Ich habe die Benachrichtigung zur Kenntnis genommen und möchte trotzdem fortfahren" + }, "confirmAlertModalDetails": { "message": "Wenn Sie sich anmelden, könnten Dritte, die für Betrügereien bekannt sind, all Ihre Assets an sich reißen. Lesen Sie bitte die Benachrichtigungen, bevor Sie fortfahren." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Verbindung mit $1 bestätigen" }, + "confirmDeletion": { + "message": "Löschung bestätigen" + }, + "confirmFieldPaymaster": { + "message": "Gebühr bezahlt von" + }, + "confirmFieldTooltipPaymaster": { + "message": "Die Gebühr für diese Transaktion wird durch den Paymaster Smart Contract bezahlt." + }, "confirmPassword": { "message": "Passwort bestätigen" }, "confirmRecoveryPhrase": { "message": "Geheime Wiederherstellungsphrase bestätigen" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Bestätigen Sie diese Transaktion nur, wenn Sie den Inhalt vollständig verstehen und der anfragenden Website vertrauen." + "confirmRpcUrlDeletionMessage": { + "message": "Sind Sie sicher, dass Sie die RPC-URL löschen möchten? Ihre Informationen werden für dieses Netzwerk nicht gespeichert." + }, + "confirmTitleDescPermitSignature": { + "message": "Diese Website möchte die Genehmigung, Ihre Tokens auszugeben." + }, + "confirmTitleDescSIWESignature": { + "message": "Eine Website möchte, dass Sie sich anmelden, um zu beweisen, dass Sie dieses Konto besitzen." }, "confirmTitleDescSignature": { "message": "Bestätigen Sie diese Nachricht nur, wenn Sie dem Inhalt zustimmen und der anfragenden Website vertrauen." }, + "confirmTitlePermitSignature": { + "message": "Antrag auf Ausgabenobergrenze" + }, + "confirmTitleSIWESignature": { + "message": "Anmeldeanfrage" + }, "confirmTitleSignature": { "message": "Signaturanfrage" }, @@ -961,7 +1135,7 @@ "message": "Verbunden mit" }, "connecting": { - "message": "Verbindung wird hergestellt ..." + "message": "Verbinden" }, "connectingTo": { "message": "Verbindung mit $1 wird hergestellt" @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Konto erstellen" }, + "creatorAddress": { + "message": "Adresse des Erstellers" + }, "crossChainSwapsLink": { "message": "Netzwerkübergreifender Austausch mit MetaMask Portfolio" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Daten" }, + "dataCollectionForMarketing": { + "message": "Datenerhebung für das Marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "Wir verwenden MetaMetrics, um zu erfahren, wie Sie mit unserer Marketingkommunikation umgehen. Wir können relevante Neuigkeiten (wie Produktmerkmale und andere Materialien) teilen." + }, + "dataCollectionWarningPopoverButton": { + "message": "Okay" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Sie haben die Datenerhebung für unsere Marketingzwecke deaktiviert. Dies gilt nur für dieses Gerät. Wenn Sie MetaMask auf anderen Geräten verwenden, stellen Sie sicher, dass Sie sich auch dort abmelden." + }, "dataHex": { "message": "Hexadezimal" }, + "dataUnavailable": { + "message": "Daten nicht verfügbar" + }, + "dateCreated": { + "message": "Datum erstellt" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Anfrage entschlüsseln" }, + "defaultRpcUrl": { + "message": "Standard-RPC-URL" + }, "delete": { "message": "Löschen" }, @@ -1311,6 +1509,9 @@ "message": "$1-Netzwerk löschen?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "RPC-URL löschen" + }, "deposit": { "message": "Einzahlung" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Details" }, - "developerOptions": { - "message": "Entwickler-Optionen" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Setzt den Booleschen Wert isShown für alle Ankündigungen auf false zurück. Ankündigungen sind die Benachrichtigungen, die im Was-neu-ist-Popup-Modal angezeigt werden." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Setzt verschiedene Status im Zusammenhang mit dem Onboarding zurück und leitet zur Onboarding-Seite „Sichern Sie Ihre Wallet“ weiter." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Führt dazu, dass ein Zeitstempel kontinuierlich in session.storage gespeichert wird." - }, "disabledGasOptionToolTipMessage": { "message": "“$1” ist deaktiviert, weil es nicht das Minimum einer zehnprozentigen Erhöhung gegenüber der ursprünglichen Gasgebühr erfüllt.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Unbekannte Bearbeitungszeit" }, + "editNetworkLink": { + "message": "Das ursprüngliche Netzwerk bearbeiten" + }, "editNonceField": { "message": "Nonce bearbeiten" }, @@ -1552,12 +1744,12 @@ "message": "$1 aktivieren", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Automatische Token-Erkennung aktivieren" - }, "enabled": { "message": "Aktiviert" }, + "enabledNetworks": { + "message": "Aktivierte Netzwerke" + }, "encryptionPublicKeyNotice": { "message": "$1 wünscht Ihren öffentlichen Verschlüsselungsschlüssel. Durch Ihre Zustimmung kann diese Seite verschlüsselte Nachrichten an Sie verfassen.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Geschätzte Gebühr" }, + "estimatedFeeTooltip": { + "message": "Betrag, der für die Bearbeitung der Transaktion im Netzwerk gezahlt wurde." + }, "ethGasPriceFetchWarning": { "message": "Der Gas-Preis, der sich aus der Gas-Hauptschätzungsdienst ergibt, ist derzeit nicht verfügbar." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Auf Etherscan anzeigen" }, + "existingChainId": { + "message": "Die von Ihnen eingegebenen Informationen sind mit einer bestehenden Chain-ID verknüpft." + }, + "existingRpcUrl": { + "message": "Diese URL ist mit einer anderen Chain-ID verknüpft." + }, "expandView": { "message": "Ansicht erweitern" }, @@ -1735,6 +1936,9 @@ "message": "Dateiimport fehlgeschlagen? Bitte hier klicken!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Finden Sie das Richtige auf:" + }, "flaskWelcomeUninstall": { "message": "Sie sollten diese Erweiterung deinstallieren.", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Passwort vergessen?" }, + "form": { + "message": "Formular" + }, "from": { "message": "Von" }, @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "hoch" }, + "highestCurrentBid": { + "message": "Höchstes aktuelles Gebot" + }, + "highestFloorPrice": { + "message": "Höchster Mindestpreis" + }, "history": { "message": "Verlauf" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Mit dieser Aktion werden Tokens bearbeitet, die bereits in Ihrer Wallet aufgelistet sind und die dazu verwendet werden können, Sie zu betrügen. Genehmigen Sie diese Aktion nur, wenn Sie sicher sind, dass Sie den Wert dieser Tokens ändern möchten. Erfahren Sie mehr über $1." }, + "l1Fee": { + "message": "L1-Gebühr" + }, + "l1FeeTooltip": { + "message": "L1-Gas-Gebühr" + }, + "l2Fee": { + "message": "L2-Gebühr" + }, + "l2FeeTooltip": { + "message": "L2-Gas-Gebühr" + }, "lastConnected": { "message": "Zuletzt verbunden" }, - "lastPriceSold": { - "message": "Letzter Verkaufspreis" - }, "lastSold": { "message": "Zuletzt verkauft" }, @@ -2498,6 +2720,12 @@ "message": "Stellen Sie sicher, dass niemand zuschaut.", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Marktkapitalisierung" + }, + "marketDetails": { + "message": "Marktdetails" + }, "max": { "message": "Max." }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Maximale Gebühr" }, + "maxFeeTooltip": { + "message": "Eine maximale Gebühr, die für die Bezahlung der Transaktion vorgesehen ist." + }, "maxPriorityFee": { "message": "Maximale Prioritätsgebühr" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Methode" }, - "methodDataTransactionDescription": { - "message": "Dies ist die konkrete Aktion, die durchgeführt wird. Diese Daten können gefälscht sein, daher sollten Sie der Website auf der anderen Seite vertrauen." + "methodDataTransactionDesc": { + "message": "Funktion, die auf der Grundlage der dekodierten Eingabedaten ausgeführt wird." }, "methodNotSupported": { "message": "Bei diesem Konto nicht unterstützt." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Metriken" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Ihr ausgewähltes Konto ($1) unterscheidet sich von dem Konto, das versucht, zu unterzeichnen ($2)." }, @@ -2675,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Das native Token dieses Netzwerks ist $1. Dieses Token wird für die Gas-Gebühr verwendet.", + "message": "Das native Token dieses Netzwerks ist $1. Dieses Token wird für die Gas-Gebühr verwendet. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Basis" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Der diesem Netzwerk zugeordnete Name." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Netzwerkoptionen" + }, "networkProvider": { "message": "Netzwerkanbieter" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "„$1“ wurde erfolgreich hinzugefügt!" }, + "newNetworkEdited": { + "message": "„$1“ wurde erfolgreich bearbeitet!" + }, "newNftAddedMessage": { "message": "NFT wurde erfolgreich hinzugefügt!" }, @@ -2872,6 +3116,9 @@ "nftAlreadyAdded": { "message": "NFT wurde bereits hinzugefügt." }, + "nftAutoDetectionEnabled": { + "message": "Automatische NFT-Erkennung aktiviert" + }, "nftDisclaimer": { "message": "Haftungsausschluss: MetaMask bezieht die Mediendatei aus der Quellen-URL. Diese URL wird manchmal vom Markt, auf dem das NFT erstellt wurde, geändert." }, @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Keine Auflösung für die Domain angegeben." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snaps und die meisten Hardware-Wallets funktionieren nicht mit Ihrer aktuellen Browserversion." + }, "noNFTs": { "message": "Noch keine NFTs" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Transaktions-Nonce anpassen" }, - "nonceFieldDescription": { - "message": "Aktivieren Sie dies, um die Nonce (Transaktionsnummer) auf den Bestätigungsbildschirmen zu ändern. Dies ist eine erweiterte Funktion, verwenden Sie diese vorsichtig." + "nonceFieldDesc": { + "message": "Aktivieren Sie diese Funktion, um die Nonce (Transaktionsnummer) beim Senden von Assets zu ändern. Es handelt sich hierbei um eine erweiterte Funktion, also nutzen Sie sie mit Bedacht." }, "nonceFieldHeading": { "message": "Eigene Nonce" @@ -3001,7 +3251,7 @@ "message": "Prioritätsgebühr (GWEI)" }, "notificationItemCheckBlockExplorer": { - "message": "Überprüfen Sie den BlockExplorer" + "message": "Auf dem Block-Explorer überprüfen" }, "notificationItemCollection": { "message": "Sammlung" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 neues Token in diesem Konto gefunden." }, + "numberOfTokens": { + "message": "Anzahl von Tokens" + }, "ofTextNofM": { "message": "von" }, @@ -3170,8 +3423,36 @@ "on": { "message": "An" }, - "onboarding": { - "message": "Onboarding" + "onboardedMetametricsAccept": { + "message": "Ich stimme zu" + }, + "onboardedMetametricsDisagree": { + "message": "Nein, danke" + }, + "onboardedMetametricsKey1": { + "message": "Neueste Entwicklungen" + }, + "onboardedMetametricsKey2": { + "message": "Produktmerkmale" + }, + "onboardedMetametricsKey3": { + "message": "Andere relevante Werbematerialien" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Zusätzlich zu $1 möchten wir Daten verwenden, um zu verstehen, wie Sie mit Marketingkommunikation umgehen.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Dies hilft uns, das, was wir mit Ihnen teilen, zu personalisieren, wie z. B.:" + }, + "onboardedMetametricsParagraph3": { + "message": "Denken Sie daran, dass wir die von Ihnen bereitgestellten Daten niemals verkaufen und Sie sich jederzeit abmelden können." + }, + "onboardedMetametricsTitle": { + "message": "Helfen Sie uns, Ihr Erlebnis zu verbessern" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Das IPFS-Gateway ermöglicht es, auf von Dritten gehostete Daten zuzugreifen und diese einzusehen. Sie können ein benutzerdefiniertes IPFS-Gateway hinzufügen oder weiterhin das Standard-Gateway verwenden." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Wenn wir Metriken sammeln, wird es immer wie folgt sein ..." }, - "onboardingMetametricsDisagree": { - "message": "Nein, danke!" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Helfen Sie uns, MetaMask zu verbessern." }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Wir verwenden diese Daten, um zu erfahren, wie Sie mit unserer Marketingkommunikation umgehen. Wir können relevante Neuigkeiten (wie Produktmerkmale) teilen." + }, "onboardingPinExtensionBillboardAccess": { "message": "Voller Zugriff" }, @@ -3289,6 +3570,22 @@ "message": "Phishing-Warnungen basieren auf der Kommunikation mit $1. jsDeliver hat Zugriff auf Ihre IP-Adresse. $2 ansehen.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 T", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 M", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 W", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 J", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Snaps entdecken" - }, - "openSeaToBlockaidDescription": { - "message": "Sicherheitsbenachrichtigungen sind in diesem Netzwerk nicht mehr verfügbar. Die Installation eines Snaps kann Ihre Sicherheit erhöhen." - }, - "openSeaToBlockaidTitle": { - "message": "Vorsicht!" - }, "operationFailed": { "message": "Vorgang fehlgeschlagen" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Verbundene Websites sind nun Genehmigungen" }, + "permitSimulationDetailInfo": { + "message": "Sie erteilen dem Spender die Genehmigung, diese Menge an Tokens von Ihrem Konto auszugeben." + }, "personalAddressDetected": { "message": "Personalisierte Adresse identifiziert. Bitte füge die Token-Contract-Adresse ein." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Beliebte benutzerdefinierte Netzwerke" }, + "popularNetworkAddToolTip": { + "message": "Einige dieser Netzwerke werden von Dritten betrieben. Die Verbindungen können weniger zuverlässig sein oder Dritten ermöglichen, Aktivitäten zu verfolgen. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfolio" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Zurück" }, + "price": { + "message": "Preis" + }, + "priceUnavailable": { + "message": "Preis nicht verfügbar" + }, "primaryCurrencySetting": { "message": "Hauptwährung" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Angebotskurs" }, + "rank": { + "message": "Rang" + }, "reAddAccounts": { "message": "alle anderen Konten erneut hinzuzufügen" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Empfangen" }, - "receiveTokensCamelCase": { - "message": "Tokens erhalten" - }, "recipientAddressPlaceholder": { "message": "Öffentliche Adresse (0x) oder ENS-Name eingeben" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Zurücksetzen" }, - "resetStates": { - "message": "Status zurücksetzen" - }, "resetWallet": { "message": "Wallet zurücksetzen" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Konten durchsuchen" }, + "searchNfts": { + "message": "NFTs suchen" + }, "searchTokens": { "message": "Tokens suchen" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Meine Wallet sichern (empfohlen)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Aufschreiben und an mehreren geheimen Orten aufbewahren" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "In einem Passwort-Manager speichern" + "message": "Aufschreiben und an mehreren geheimen Orten aufbewahren" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "In einem Schließfach aufbewahren" }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Token auswählen" }, "selectNFTPrivacyPreference": { - "message": "NFT-Erkennung in den Einstellungen aktivieren" + "message": "Automatische NFT-Erkennung aktivieren" }, "selectPathHelp": { - "message": "Wenn Sie nicht die Konten sehen, die Sie erwarten, versuchen Sie, den HD-Pfad zu ändern." + "message": "Wenn Sie nicht die Konten sehen, die Sie erwarten, versuchen Sie, den HD-Pfad oder das aktuell ausgewählte Netzwerk zu ändern." }, "selectType": { "message": "Typ auswählen" @@ -4287,9 +4585,6 @@ "send": { "message": "Senden" }, - "sendAToken": { - "message": "Token senden" - }, "sendBugReport": { "message": "Übermitteln Sie uns einen Fehlerbericht." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Sepolia-Testnetzwerk" }, - "serviceWorkerKeepAlive": { - "message": "serviceWorkerKeepAlive" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask nutzt diese vertrauenswürdigen Dienstleistungen von Drittanbietern, um die Benutzerfreundlichkeit und Sicherheit der Produkte zu verbessern." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Dies hängt bei jedem Netzwerk von unterschiedlichen APIs Dritter ab, die Ihre Ethereum- und IP-Adresse offenlegen." }, + "showLess": { + "message": "Weniger anzeigen" + }, "showMore": { "message": "Mehr anzeigen" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Unterzeichnen Sie diese Nachricht nur, wenn Sie den Inhalt vollständig verstehen und der anfragenden Seite vertrauen." }, - "signatureRequestWarning": { - "message": "Das Unterzeichnen dieser Nachricht könnte gefährlich sein. Sie könnten der Gegenseite dieser Nachricht vollständige Kontrolle über Ihr Konto und Ihre Assets gewähren. Das bedeutet, dass sie Ihr Konto jederzeit leeren könnten. Seien Sie vorsichtig. $1." - }, "signed": { "message": "Unterzeichnet" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Signieren" }, + "signingInWith": { + "message": "Anmelden mit" + }, "simulationDetailsFailed": { "message": "Es ist ein Fehler beim Laden Ihrer Schätzung aufgetreten." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Geschätzte Saldoänderungen" }, + "siweIssued": { + "message": "Ausgestellt" + }, + "siweNetwork": { + "message": "Netzwerk" + }, + "siweRequestId": { + "message": "Anfrage-ID" + }, + "siweResources": { + "message": "Ressourcen" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Sie melden sich bei einer Website an und es sind keine Änderungen an Ihrem Konto vorgesehen." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Überspringen" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Von Snaps Dritter kontrollierte Konten." }, + "snapConnectTo": { + "message": "Mit $1 verbinden", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Lassen Sie $1 automatisch und ohne Ihre Zustimmung mit $2 verbinden.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 möchte $2 verwenden", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Webseite" }, + "snapHomeMenu": { + "message": "Snap-Startmenü" + }, "snapInstallRequest": { "message": "Durch die Installation von $1 erhält es die folgenden Berechtigungen.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Quelle" }, + "speed": { + "message": "Geschwindigkeit" + }, "speedUp": { "message": "Beschleunigen" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Ausgabenlimit zu groß" }, + "spender": { + "message": "Spender" + }, "spendingCap": { "message": "Ausgabenobergrenze" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Die Statusprotokolle enthalten Ihre öffentlichen Kontoadressen und gesendeten Transaktionen." }, - "states": { - "message": "Status" - }, "status": { "message": "Status" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Abgesendet" }, + "suggestedBySnap": { + "message": "Vorgeschlagen von $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Vorgeschlagener Name:" + }, "suggestedTokenSymbol": { "message": "Vorgeschlagenes Tickersymbol:" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Preisangaben abrufen" + "message": "Angebote einholen..." }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm... etwas ist schiefgelaufen. Versuchen Sie es erneut, oder wenden Sie sich an den Kundensupport, wenn der Fehler weiterhin besteht." @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Sie haben gewechselt zu" + "message": "Sie verwenden jetzt" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Das Wechseln der Netzwerke wird alle ausstehenden Bestätigungen stornieren." @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "diese Sammlung" }, + "threeMonthsAbbreviation": { + "message": "3 M", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Zeit" }, @@ -5498,45 +5836,6 @@ "message": "An: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Sie sind einem Risiko auf Phishing-Angriffe ausgesetzt. Schützen Sie sich selbst, indem Sie eth_sign deaktivieren." - }, - "toggleEthSignDescriptionField": { - "message": "Wenn Sie diese Einstellung aktivieren, können Sie Signaturanfragen erhalten, die nicht lesbar sind. Wenn Sie eine Nachricht unterzeichnen, die Sie nicht verstehen, könnten Sie sich damit einverstanden erklären, Ihre Gelder und NFTs zu verschenken." - }, - "toggleEthSignField": { - "message": "eth_sign-Anfragen ein- oder ausschalten" - }, - "toggleEthSignModalBannerBoldText": { - "message": " werden Sie eventuell betrogen." - }, - "toggleEthSignModalBannerText": { - "message": "Wenn Sie gebeten werden, diese Einstellung zu aktivieren," - }, - "toggleEthSignModalCheckBox": { - "message": "Ich bin mir darüber im Klaren, dass ich meine Gelder und meine NFTs verlieren könnte, wenn ich eth_sign-Anfragen aktivere. " - }, - "toggleEthSignModalDescription": { - "message": "Das Erlauben von eth_sign-Anfragen kann Sie für Phishing-Angriffe verwundbar machen. Prüfen Sie die URL immer und sein Sie vorsichtig, wenn Sie Nachrichten unterzeichnen, die Code enthalten." - }, - "toggleEthSignModalFormError": { - "message": "Der Text ist falsch." - }, - "toggleEthSignModalFormLabel": { - "message": "Geben Sie „Ich unterzeichne nur, was ich verstehe“ ein, um fortzufahren." - }, - "toggleEthSignModalFormValidation": { - "message": "Ich unterzeichne nur, was ich verstehe." - }, - "toggleEthSignModalTitle": { - "message": "Nutzung auf eigenes Risiko" - }, - "toggleEthSignOff": { - "message": "AUS (empfohlen)" - }, - "toggleEthSignOn": { - "message": "EIN (nicht empfohlen)" - }, "toggleRequestQueueDescription": { "message": "Dadurch können Sie ein Netzwerk für jede Website auswählen, anstatt ein einziges Netzwerk für alle Websites auszuwählen. Diese Funktion verhindert, dass Sie manuell zwischen den Netzwerken wechseln müssen, was die Benutzerfreundlichkeit auf bestimmten Websites beeinträchtigen könnte." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Token-Contract-Adresse" }, + "tokenDecimal": { + "message": "Token-Dezimale" + }, "tokenDecimalFetchFailed": { "message": "Tokendezimal erforderlich. Finden Sie es auf: $1" }, @@ -5580,13 +5882,16 @@ "message": "Token-ID" }, "tokenList": { - "message": "Token-Listen:" + "message": "Token-Listen" }, "tokenScamSecurityRisk": { "message": "Token-Betrügereien und Sicherheitsrisiken" }, "tokenShowUp": { - "message": "Ihre Tokens werden möglicherweise nicht automatisch in Ihrer Wallet angezeigt." + "message": "Ihre Tokens werden möglicherweise nicht automatisch in Ihrer Wallet angezeigt. " + }, + "tokenStandard": { + "message": "Token-Standard" }, "tokenSymbol": { "message": "Tokensymbol" @@ -5598,6 +5903,9 @@ "message": "$1 neue Tokens gefunden", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Tokens in der Sammlung" + }, "tooltipApproveButton": { "message": "Ich verstehe" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Gesamt" }, + "totalVolume": { + "message": "Gesamtvolumen" + }, "transaction": { "message": "Transaktion" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "Transaktion mit einem Wert von $1 bei $2 erstellt." }, + "transactionDataFunction": { + "message": "Funktion" + }, "transactionDetailDappGasMoreInfo": { "message": "Seite vorgeschlagen" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Übertragung von" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Wir haben Probleme mit der Verbindung zu Ihrem Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Laut unseren Aufzeichnungen stimmt diese URL nicht mit einem bekannten Anbieter für diese Chain-ID überein." + }, "unapproved": { "message": "Nicht genehmigt" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Update" }, + "updateOrEditNetworkInformations": { + "message": "Aktualisieren Sie Ihre Informationen oder" + }, "updateRequest": { "message": "Aktualisierungsanfrage" }, "updatedWithDate": { "message": "$1 aktualisiert" }, + "uploadDropFile": { + "message": "Legen Sie Ihre Datei hier ab" + }, + "uploadFile": { + "message": "Datei hochladen" + }, "urlErrorMsg": { "message": "URIs benötigen die korrekten HTTP/HTTPS Präfixe." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Was ist das?" }, + "wrongChainId": { + "message": "Diese Chain-ID stimmt nicht mit dem Netzwerknamen überein." + }, + "wrongNetworkName": { + "message": "Laut unseren Aufzeichnungen stimmt dieser Netzwerkname nicht mit dieser Chain-ID überein." + }, "xOfYPending": { "message": "$1 von $2 ausstehend", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Ihre Konten" }, - "yourFundsMayBeAtRisk": { - "message": "Ihre Gelder könnte gefährdet sein" + "yourActivity": { + "message": "Ihre Aktivität" + }, + "yourBalance": { + "message": "Ihr Kontostand" }, "yourNFTmayBeAtRisk": { "message": "Ihr NFT könnte gefährdet sein" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 745a4614378a..d8f080e7752c 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -42,7 +42,7 @@ "message": "Συνδέστε το πορτοφόλι υλικού μέσω QR" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (Έρχεται Σύντομα)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Η διεύθυνση στο αίτημα σύνδεσης δεν ταιριάζει με τη διεύθυνση του λογαριασμού που χρησιμοποιείτε για να συνδεθείτε." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Πρέπει να επιλέξετε έναν λογαριασμό!" }, + "accountTypeNotSupported": { + "message": "Ο τύπος λογαριασμού δεν υποστηρίζεται" + }, "accounts": { "message": "Λογαριασμοί" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Προσθήκη νέου λογαριασμού Ethereum" }, + "addNewBitcoinAccount": { + "message": "Προσθήκη νέου λογαριασμού Bitcoin (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Προσθήκη νέου λογαριασμού Bitcoin (Testnet)" + }, "addNewToken": { "message": "Προσθήκη νέου token" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Προσθήκη των NFT" }, + "addRpcUrl": { + "message": "Προσθήκη διεύθυνσης URL RPC" + }, "addSnapAccountToggle": { "message": "Ενεργοποίηση της λειτουργίας \"Προσθήκη λογαριασμού Snap (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "Δεν μπορείτε να βρείτε ένα token; Μπορείτε να προσθέσετε χειροκίνητα οποιοδήποτε token επικολλώντας τη διεύθυνσή του. Οι διευθύνσεις συμβολαίων token μπορούν να βρεθούν στο $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Προσθήκη διεύθυνσης URL" + }, "addingCustomNetwork": { "message": "Προσθήκη δικτύου" }, "addingTokens": { "message": "Προσθήκη tokens" }, + "additionalNetworks": { + "message": "Επιπλέον δίκτυα" + }, + "additionalRpcUrl": { + "message": "Επιπλέον διεύθυνση URL RPC" + }, "address": { "message": "Διεύθυνση" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Προηγμένη ρύθμιση παραμέτρων" }, + "advancedDetailsDataDesc": { + "message": "Δεδομένα" + }, + "advancedDetailsHexDesc": { + "message": "Δεκαεξαδικός" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Πρόκειται για τον αριθμό συναλλαγής ενός λογαριασμού. Το nonce για την πρώτη συναλλαγή είναι 0 και αυξάνεται με διαδοχική σειρά." + }, "advancedGasFeeDefaultOptIn": { "message": "Αποθηκεύστε αυτές τις τιμές ως προεπιλεγμένες για το δίκτυο $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Ειδοποίηση" }, + "alertActionBuy": { + "message": "Αγορά ETH" + }, + "alertActionUpdateGas": { + "message": "Ενημέρωση ορίου των τελών συναλλαγών" + }, + "alertActionUpdateGasFee": { + "message": "Ενημέρωση των τελών συναλλαγών" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Ενημέρωση επιλογών των τελών συναλλαγών" + }, "alertBannerMultipleAlertsDescription": { "message": "Εάν εγκρίνετε αυτό το αίτημα, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Αυτό μπορεί να αλλάξει στις \"Ρυθμίσεις > Ειδοποιήσεις\"" }, + "alertMessageGasEstimateFailed": { + "message": "Δεν μπορούμε να παράσχουμε τα τέλη με ακρίβεια και αυτή η εκτίμηση μπορεί να είναι υψηλή. Σας προτείνουμε να εισάγετε ένα προσαρμοσμένο όριο τελών συναλλαγών, αλλά υπάρχει κίνδυνος η συναλλαγή να αποτύχει και πάλι." + }, + "alertMessageGasFeeLow": { + "message": "Όταν επιλέγετε χαμηλά τέλη, να αναμένετε πιο αργές συναλλαγές και μεγαλύτερους χρόνους αναμονής. Για ταχύτερες συναλλαγές, επιλέξτε τις επιλογές χρέωσης Market (Αγοράς) ή Aggressive (Υψηλότερη αγοραστική τιμή)." + }, + "alertMessageGasTooLow": { + "message": "Για να συνεχίσετε με αυτή τη συναλλαγή, θα πρέπει να αυξήσετε το όριο των τελών συναλλαγών σε 21000 ή περισσότερο." + }, + "alertMessageInsufficientBalance": { + "message": "Δεν έχετε αρκετά ETH στον λογαριασμό σας για να πληρώσετε τα τέλη συναλλαγών." + }, + "alertMessageNetworkBusy": { + "message": "Οι τιμές των τελών συναλλαγών είναι υψηλές και οι εκτιμήσεις είναι λιγότερο ακριβείς." + }, + "alertMessageNoGasPrice": { + "message": "Δεν μπορούμε να συνεχίσουμε με αυτή τη συναλλαγή μέχρι να ενημερώσετε τα τέλη μη αυτόματα." + }, + "alertMessagePendingTransactions": { + "message": "Αυτή η συναλλαγή δεν θα πραγματοποιηθεί μέχρι να ολοκληρωθεί μια προηγούμενη συναλλαγή. Μάθετε πώς να ακυρώσετε ή να επισπεύσετε μια συναλλαγή." + }, + "alertMessageSignInDomainMismatch": { + "message": "Ο ιστότοπος που υποβάλλει το αίτημα δεν είναι ο ιστότοπος στον οποίο έχετε συνδεθεί. Αυτό θα μπορούσε να είναι μια απόπειρα κλοπής των στοιχείων σύνδεσής σας." + }, + "alertMessageSignInWrongAccount": { + "message": "Αυτός ο ιστότοπος σας ζητάει να συνδεθείτε χρησιμοποιώντας λάθος λογαριασμό." + }, + "alertMessageSigningOrSubmitting": { + "message": "Αυτή η συναλλαγή θα πραγματοποιηθεί μόνο όταν ολοκληρωθεί η προηγούμενη συναλλαγή σας." + }, "alertModalAcknowledge": { "message": "Αναγνωρίζω τον κίνδυνο και εξακολουθώ να θέλω να συνεχίσω" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Έλεγχος όλων των ειδοποιήσεων" }, + "alertReasonGasEstimateFailed": { + "message": "Ανακριβή τέλη" + }, + "alertReasonGasFeeLow": { + "message": "Αργή ταχύτητα" + }, + "alertReasonGasTooLow": { + "message": "Χαμηλό όριο τελών συναλλαγών" + }, + "alertReasonInsufficientBalance": { + "message": "Ανεπαρκή κεφάλαια" + }, + "alertReasonNetworkBusy": { + "message": "Το δίκτυο είναι απασχολημένο" + }, + "alertReasonNoGasPrice": { + "message": "Δεν είναι διαθέσιμη η εκτίμηση των τελών" + }, + "alertReasonPendingTransactions": { + "message": "Εκκρεμής συναλλαγή" + }, + "alertReasonSignIn": { + "message": "Ύποπτο αίτημα σύνδεσης" + }, + "alertReasonWrongAccount": { + "message": "Λάθος λογαριασμός" + }, "alertSettingsUnconnectedAccount": { "message": "Περιήγηση σε έναν ιστότοπο με έναν μη συνδεδεμένο επιλέγμενο λογαριασμό" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Όλες οι άδειες χρήσης" }, + "allTimeHigh": { + "message": "Υψηλό όλων των εποχών" + }, + "allTimeLow": { + "message": "Χαμηλό όλων των εποχών" + }, "allYourNFTsOf": { "message": "Όλα τα NFT σας από το $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 και $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Ανακοινώσεις" - }, "appDescription": { "message": "Ένα Πορτοφόλι Ethereum στο Πρόγραμμα Περιήγησής σας", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Επιλογές περιουσιακών στοιχείων" }, "attemptSendingAssets": { - "message": "Εάν επιχειρήσετε να στείλετε περιουσιακά στοιχεία απευθείας από ένα δίκτυο σε ένα άλλο, αυτό ενδέχεται να οδηγήσει σε μόνιμη απώλεια περιουσιακών στοιχείων. Βεβαιωθείτε ότι χρησιμοποιείτε μια διασύνδεση." + "message": "Ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία εάν προσπαθήσετε να τα στείλετε από άλλο δίκτυο. Μεταφέρετε κεφάλαια με ασφάλεια μεταξύ δικτύων χρησιμοποιώντας μια διασύνδεση." }, "attemptSendingAssetsWithPortfolio": { "message": "Μπορεί να χάσετε τα περιουσιακά σας στοιχεία αν προσπαθήσετε να τα στείλετε από άλλο δίκτυο. Μεταφέρετε χρήματα με ασφάλεια μεταξύ δικτύων χρησιμοποιώντας μια διασύνδεση, όπως το $1" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Προσπάθεια ακύρωσης των ανταλλαγών δωρεάν" }, + "attributes": { + "message": "Χαρακτηριστικά" + }, "attributions": { "message": "Αποδόσεις" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Οι βασικές λειτουργίες είναι απενεργοποιημένες" }, + "basicConfigurationDescription": { + "message": "Το MetaMask παρέχει βασικές λειτουργίες, όπως λεπτομέρειες για τα tokens και ρυθμίσεις τελών συναλλαγών μέσω υπηρεσιών διαδικτύου. Όταν χρησιμοποιείτε υπηρεσίες διαδικτύου, η διεύθυνση IP σας κοινοποιείται, σε αυτή την περίπτωση με στο MetaMask. Αυτό συμβαίνει ακριβώς όπως όταν επισκέπτεστε οποιονδήποτε ιστότοπο. Το MetaMask χρησιμοποιεί αυτά τα δεδομένα προσωρινά και δεν πωλεί ποτέ τα δεδομένα σας. Μπορείτε να χρησιμοποιήσετε ένα VPN ή να απενεργοποιήσετε αυτές τις υπηρεσίες, αλλά αυτό μπορεί να επηρεάσει την εμπειρία σας στο MetaMask. Για να μάθετε περισσότερα διαβάστε την $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Βασικές λειτουργίες" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "Η δοκιμαστική έκδοση του MetaMask δεν θα σας ζητήσει ποτέ τη Μυστική Φράση Ανάκτησής σας." }, + "billionAbbreviation": { + "message": "Δ", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Δεν υποστηρίζεται η δραστηριότητα στα Bitcoins" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Η ενεργοποίηση αυτής της λειτουργίας θα σας δώσει τη δυνατότητα να προσθέσετε έναν λογαριασμό Bitcoin στην επέκταση σας του MetaMask που προέρχεται από την υπάρχουσα Μυστική Φράση Ανάκτησης. Πρόκειται για μια πειραματική λειτουργία Beta, οπότε θα πρέπει να τη χρησιμοποιήσετε με δική σας ευθύνη. Για να μας δώσετε τα σχόλιά σας σχετικά με αυτή τη νέα εμπειρία Bitcoin, συμπληρώστε αυτή την $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Ενεργοποίηση της λειτουργίας \"Προσθήκη νέου λογαριασμού Bitcoin (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Η ενεργοποίηση αυτής της λειτουργίας θα σας δώσει τη δυνατότητα να προσθέσετε έναν λογαριασμό Bitcoin για το δίκτυο δοκιμών." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Ενεργοποίηση της λειτουργίας \"Προσθήκη νέου λογαριασμού Bitcoin (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Λογαριασμός", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Δεν συνιστούμε να προχωρήσετε σε αυτό το αίτημα." + }, "blockaidDescriptionApproveFarming": { "message": "Εάν εγκρίνετε αυτό το αίτημα, ένα τρίτο μέρος που είναι γνωστό για απάτες μπορεί να πάρει όλα τα περιουσιακά σας στοιχεία." }, @@ -669,7 +807,7 @@ "message": "Εάν εγκρίνετε αυτό το αίτημα, κάποιος μπορεί να κλέψει τα περιουσιακά σας στοιχεία που είναι καταχωρημένα στο Blur." }, "blockaidDescriptionErrored": { - "message": "Λόγω κάποιου σφάλματος, αυτό το αίτημα δεν επαληθεύτηκε από τον πάροχο ασφαλείας. Προχωρήστε με προσοχή." + "message": "Λόγω κάποιου σφάλματος, δεν μπορέσαμε να ελέγξουμε τις ειδοποιήσεις ασφαλείας. Συνεχίστε μόνο αν εμπιστεύεστε κάθε διεύθυνση που εμπλέκεται." }, "blockaidDescriptionMaliciousDomain": { "message": "Αλληλεπιδράτε με έναν κακόβουλο τομέα. Εάν εγκρίνετε αυτό το αίτημα, ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Εάν εγκρίνετε αυτό το αίτημα, ένας τρίτος που είναι γνωστός για απάτες θα πάρει όλα τα περιουσιακά σας στοιχεία." }, + "blockaidDescriptionWarning": { + "message": "Πρόκειται για ένα παραπλανητικό αίτημα. Συνεχίστε μόνο αν εμπιστεύεστε κάθε διεύθυνση που εμπλέκεται." + }, "blockaidMessage": { "message": "Διαφύλαξη της ιδιωτικότητας - δεν κοινοποιούνται δεδομένα σε τρίτους. Διατίθεται στα Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Base και Sepolia." }, @@ -690,7 +831,7 @@ "message": "Αυτό είναι ένα παραπλανητικό αίτημα" }, "blockaidTitleMayNotBeSafe": { - "message": "Το αίτημα μπορεί να μην είναι ασφαλές" + "message": "Να είστε προσεκτικοί" }, "blockaidTitleSuspicious": { "message": "Αυτό είναι ένα ύποπτο αίτημα" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Αγοράστηκε για" + }, "bridge": { "message": "Διασύνδεση" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Θα πρέπει να χρησιμοποιήσετε το MetaMask στο Google Chrome για να συνδεθείτε στο Πορτοφόλι Υλικού." }, + "circulatingSupply": { + "message": "Διαθέσιμη προσφορά" + }, "clear": { "message": "Εκκαθάριση" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Κάντε κλικ εδώ για να προσθέσετε χειροκίνητα τα tokens." + "message": "Μπορείτε πάντα να προσθέσετε τα tokens χειροκίνητα." }, "close": { "message": "Κλείσιμο" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Όνομα συλλογής" + }, "comboNoOptions": { "message": "Δεν βρέθηκαν επιλογές", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Έχω ενημερωθεί για τις ειδοποιήσεις και εξακολουθώ να θέλω να συνεχίσω" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Έχω ενημερωθεί για την ειδοποίηση και εξακολουθώ να θέλω να συνεχίσω" + }, "confirmAlertModalDetails": { "message": "Εάν συνδεθείτε, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία. Ελέγξτε τις ειδοποιήσεις πριν συνεχίσετε." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Επιβεβαίωση σύνδεσης με το $1" }, + "confirmDeletion": { + "message": "Επιβεβαίωση διαγραφής" + }, + "confirmFieldPaymaster": { + "message": "Τα τέλη καταβλήθηκαν από" + }, + "confirmFieldTooltipPaymaster": { + "message": "Τα τέλη για αυτή τη συναλλαγή θα καταβληθούν από τον υπεύθυνο πληρωμών του έξυπνου συμβολαίου." + }, "confirmPassword": { "message": "Επιβεβαίωση Κωδικού Πρόσβασης" }, "confirmRecoveryPhrase": { "message": "Επιβεβαιώστε τη Μυστική Φράση Ανάκτησης" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Επιβεβαιώστε αυτή τη συναλλαγή μόνο εάν κατανοείτε πλήρως το περιεχόμενο και εμπιστεύεστε τον ιστότοπο που τη ζητάει." + "confirmRpcUrlDeletionMessage": { + "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε τη διεύθυνση URL RPC; Οι πληροφορίες σας δεν θα αποθηκευτούν για αυτό το δίκτυο." + }, + "confirmTitleDescPermitSignature": { + "message": "Αυτός ο ιστότοπος ζητάει άδεια για να δαπανήσει τα tokens σας." + }, + "confirmTitleDescSIWESignature": { + "message": "Ένας ιστότοπος θέλει να συνδεθείτε για να αποδείξετε ότι είστε ο κάτοχος αυτού του λογαριασμού." }, "confirmTitleDescSignature": { "message": "Επιβεβαιώστε αυτό το μήνυμα μόνο εάν εγκρίνετε το περιεχόμενο και εμπιστεύεστε τον ιστότοπο που το ζητάει." }, + "confirmTitlePermitSignature": { + "message": "Αίτημα ανώτατου ορίου δαπανών" + }, + "confirmTitleSIWESignature": { + "message": "Αίτημα σύνδεσης" + }, "confirmTitleSignature": { "message": "Αίτημα υπογραφής" }, @@ -961,7 +1135,7 @@ "message": "Συνδέεται με" }, "connecting": { - "message": "Σύνδεση..." + "message": "Σύνδεση" }, "connectingTo": { "message": "Σύνδεση με $1" @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Δημιουργία λογαριασμού" }, + "creatorAddress": { + "message": "Διεύθυνση δημιουργού" + }, "crossChainSwapsLink": { "message": "Εναλλαγή σε διάφορα δίκτυα με το MetaMask Portfolio" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Δεδομένα" }, + "dataCollectionForMarketing": { + "message": "Συγκέντρωση δεδομένων για μάρκετινγκ" + }, + "dataCollectionForMarketingDescription": { + "message": "Θα χρησιμοποιήσουμε το MetaMetrics για να μάθουμε πώς αλληλεπιδράτε με τις επικοινωνίες μάρκετινγκ. Ενδέχεται να κοινοποιησούμε σχετικές πληροφορίες (όπως χαρακτηριστικά προϊόντων και άλλο υλικό)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Εντάξει" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Απενεργοποιήσατε τη συλλογή δεδομένων για σκοπούς μάρκετινγκ. Αυτό ισχύει μόνο για αυτή τη συσκευή. Εάν χρησιμοποιείτε το MetaMask και σε άλλες συσκευές, βεβαιωθείτε ότι έχετε επιλέξει την απενεργοποίηση και εκεί." + }, "dataHex": { "message": "Δεκαεξαδικός" }, + "dataUnavailable": { + "message": "μη διαθέσιμα δεδομένα" + }, + "dateCreated": { + "message": "Ημερομηνία δημιουργίας" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Αποκρυπτογράφηση αιτήματος" }, + "defaultRpcUrl": { + "message": "Προεπιλεγμένη διεύθυνση URL RPC" + }, "delete": { "message": "Διαγραφή" }, @@ -1311,6 +1509,9 @@ "message": "Διαγραφή του δικτύου $1;", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Διαγραφή της διεύθυνσης URL RPC" + }, "deposit": { "message": "Κατάθεση" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Λεπτομέρειες" }, - "developerOptions": { - "message": "Επιλογές προγραμματιστών" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Επαναφέρει το δυαδικό isShown σε ψευδές για όλες τις ανακοινώσεις. Οι ανακοινώσεις είναι οι ειδοποιήσεις που εμφανίζονται στο αναδυόμενο παράθυρο \"Τι νέο υπάρχει\"." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Επαναφέρει διάφορες καταστάσεις που σχετίζονται με την ενσωμάτωση και ανακατευθύνει στη σελίδα ενσωμάτωσης \"Ασφαλίστε το πορτοφόλι σας\"." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Αυτό έχει ως αποτέλεσμα τη συνεχή αποθήκευση μιας χρονοσφραγίδας στο session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "Το \"1$\" είναι απενεργοποιημένο επειδή δεν πληροί την ελάχιστη αύξηση 10% σε σχέση με τα αρχικά τέλη συναλλαγής.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Άγνωστος χρόνος επεξεργασίας" }, + "editNetworkLink": { + "message": "επεξεργασία του αρχικού δικτύου" + }, "editNonceField": { "message": "Επεξεργασία Nonce" }, @@ -1552,12 +1744,12 @@ "message": "ενεργοποίηση $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Ενεργοποίηση της αυτόματης ανίχνευσης των tokens" - }, "enabled": { "message": "Ενεργοποιημένο" }, + "enabledNetworks": { + "message": "Ενεργοποιημένα δίκτυα" + }, "encryptionPublicKeyNotice": { "message": "Το $1 θα ήθελε το δημόσιο σας κλειδί κρυπτογράφησης. Με τη συγκατάθεσή σας, αυτός ο ιστότοπος θα είναι σε θέση να συντάσσει κρυπτογραφημένα μηνύματα προς εσάς.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Εκτιμώμενη χρέωση" }, + "estimatedFeeTooltip": { + "message": "Ποσό που καταβλήθηκε για τη διεκπεραίωση της συναλλαγής στο δίκτυο." + }, "ethGasPriceFetchWarning": { "message": "Η εφεδρική τιμή του τέλους συναλλαγής παρέχεται καθώς η κύρια υπηρεσία εκτίμησης τελών συναλλαγής, δεν είναι διαθέσιμη αυτή τη στιγμή." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Προβολή στην Etherscan" }, + "existingChainId": { + "message": "Οι πληροφορίες που έχετε εισάγει συνδέονται με ένα υπάρχον αναγνωριστικό αλυσίδας." + }, + "existingRpcUrl": { + "message": "Αυτή η διεύθυνση URL συνδέεται με ένα άλλο αναγνωριστικό αλυσίδας." + }, "expandView": { "message": "Ανάπτυξη προβολής" }, @@ -1735,6 +1936,9 @@ "message": "Η εισαγωγή αρχείων δεν λειτουργεί; Κάντε κλικ εδώ!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Βρείτε το σωστό στο:" + }, "flaskWelcomeUninstall": { "message": "θα πρέπει να απεγκαταστήσετε αυτή την επέκταση", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Ξεχάσατε τον κωδικό πρόσβασής σας;" }, + "form": { + "message": "φόρμα" + }, "from": { "message": "Από" }, @@ -1904,7 +2111,7 @@ "message": "Δοκιμαστικό δίκτυο Goerli" }, "gotIt": { - "message": "Το κατάλαβα!" + "message": "Το κατάλαβα" }, "grantedToWithColon": { "message": "Χορηγήθηκε στο:" @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "υψηλό" }, + "highestCurrentBid": { + "message": "Υψηλότερη τρέχουσα προσφορά" + }, + "highestFloorPrice": { + "message": "Υψηλότερη κατώτατη τιμή" + }, "history": { "message": "Ιστορικό" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Αυτή η ενέργεια θα επεξεργαστεί τα tokens που είναι ήδη καταχωρημένα στο πορτοφόλι σας, τα οποία μπορούν να χρησιμοποιηθούν για να σας εξαπατήσουν. Εγκρίνετε μόνο αν είστε σίγουροι ότι θέλετε να αλλάξετε αυτό που αντιπροσωπεύουν αυτά τα tokens. Μάθετε περισσότερα στο $1" }, + "l1Fee": { + "message": "Τέλη L1" + }, + "l1FeeTooltip": { + "message": "Τέλη συναλλαγών L1" + }, + "l2Fee": { + "message": "Τέλη L2" + }, + "l2FeeTooltip": { + "message": "Τέλη συναλλαγών L2" + }, "lastConnected": { "message": "Τελευταία σύνδεση" }, - "lastPriceSold": { - "message": "Τελευταία τιμή πώλησης" - }, "lastSold": { "message": "Τελευταία πώληση" }, @@ -2498,6 +2720,12 @@ "message": "Βεβαιωθείτε ότι κανείς δεν κοιτάει", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Κεφαλαιοποίηση αγοράς" + }, + "marketDetails": { + "message": "Λεπτομέρειες της αγοράς" + }, "max": { "message": "Μέγ." }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Μέγιστη χρέωση" }, + "maxFeeTooltip": { + "message": "Τα μέγιστα τέλη που προβλέπονται για την πληρωμή της συναλλαγής." + }, "maxPriorityFee": { "message": "Μέγιστο τέλος προτεραιότητας" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Μέθοδος" }, - "methodDataTransactionDescription": { - "message": "Αυτή είναι η συγκεκριμένη ενέργεια που θα γίνει. Αυτά τα δεδομένα μπορεί να είναι ψεύτικα, οπότε βεβαιωθείτε ότι εμπιστεύεστε τον ιστότοπο στην άλλη πλευρά." + "methodDataTransactionDesc": { + "message": "Λειτουργία που εκτελείται με βάση τα αποκωδικοποιημένα δεδομένα εισόδου." }, "methodNotSupported": { "message": "Δεν υποστηρίζεται με αυτόν τον λογαριασμό." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Μετρήσεις" }, + "millionAbbreviation": { + "message": "Ε", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Ο λογαριασμός ($1) που επιλέξατε είναι διαφορετικός από τον λογαριασμό που προσπαθείτε να υπογράψετε ($2)" }, @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Βάση" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Το όνομα που συνδέεται με αυτό το δίκτυο." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Επιλογές δικτύου" + }, "networkProvider": { "message": "Πάροχος δικτύου" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "Το “$1” προστέθηκε με επιτυχία!" }, + "newNetworkEdited": { + "message": "Το “$1” έχει επεξεργασθεί με επιτυχία!" + }, "newNftAddedMessage": { "message": "Το NFT προστέθηκε με επιτυχία!" }, @@ -2872,6 +3116,9 @@ "nftAlreadyAdded": { "message": "Το NFT έχει ήδη προστεθεί." }, + "nftAutoDetectionEnabled": { + "message": "Ενεργοποιήθηκε η αυτόματη ανίχνευση NFT" + }, "nftDisclaimer": { "message": "Αποποίηση ευθυνών: Το MetaMask αντλεί το αρχείο πολυμέσων από το url της πηγής. Αυτό το url μερικές φορές αλλάζει ανάλογα με την αγορά στην οποία εκδόθηκε το NFT." }, @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Δεν παρέχεται ανάλυση για τον τομέα." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Τα Snaps και τα περισσότερα πορτοφόλια υλικού δεν θα λειτουργούν με την τρέχουσα έκδοση του προγράμματος περιήγησής σας." + }, "noNFTs": { "message": "Δεν υπάρχουν NFT ακόμα" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Προσαρμόστε τη συναλλαγή nonce" }, - "nonceFieldDescription": { - "message": "Ενεργοποιήστε την επιλογή αυτή για να αλλάξετε τον αριθμό Nonce (αριθμό συναλλαγής) στις οθόνες επιβεβαίωσης. Αυτή είναι μια προηγμένη λειτουργία, χρησιμοποιήστε την με προσοχή." + "nonceFieldDesc": { + "message": "Ενεργοποιήστε την επιλογή αυτή για να αλλάξετε το nonce (αριθμός συναλλαγής) κατά την αποστολή περιουσιακών στοιχείων. Αυτή είναι μια προηγμένη λειτουργία, χρησιμοποιήστε τη με προσοχή." }, "nonceFieldHeading": { "message": "Προσαρμοσμένο Nonce" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 νέο token βρέθηκε σε αυτόν τον λογαριασμό" }, + "numberOfTokens": { + "message": "Αριθμός tokens" + }, "ofTextNofM": { "message": "από" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Ενεργό" }, - "onboarding": { - "message": "Ενσωμάτωση" + "onboardedMetametricsAccept": { + "message": "Συμφωνώ" + }, + "onboardedMetametricsDisagree": { + "message": "Όχι, ευχαριστώ" + }, + "onboardedMetametricsKey1": { + "message": "Τελευταίες εξελίξεις" + }, + "onboardedMetametricsKey2": { + "message": "Χαρακτηριστικά προϊόντων" + }, + "onboardedMetametricsKey3": { + "message": "Άλλο σχετικό προωθητικό υλικό" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Εκτός από το $1, θα θέλαμε να χρησιμοποιήσουμε δεδομένα για να κατανοήσουμε πώς αλληλεπιδράτε με τις επικοινωνίες μάρκετινγκ.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Αυτό μας βοηθά να εξατομικεύσουμε αυτά που κοινοποιούμε σε εσάς, όπως:" + }, + "onboardedMetametricsParagraph3": { + "message": "Να θυμάστε ότι δεν πουλάμε ποτέ τα δεδομένα που μας παρέχετε και μπορείτε να εξαιρεθείτε ανά πάσα στιγμή." + }, + "onboardedMetametricsTitle": { + "message": "Βοηθήστε μας να βελτιώσουμε την εμπειρία σας" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Η πύλη IPFS επιτρέπει την πρόσβαση και την προβολή δεδομένων που φιλοξενούνται από τρίτους. Μπορείτε να προσθέσετε μια προσαρμοσμένη πύλη IPFS ή να συνεχίσετε να χρησιμοποιείτε την προεπιλεγμένη." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Όταν συγκεντρώνουμε μετρήσεις, θα είναι πάντα..." }, - "onboardingMetametricsDisagree": { - "message": "Όχι, ευχαριστώ" - }, "onboardingMetametricsInfuraTerms": { "message": "Θα σας ενημερώσουμε εάν αποφασίσουμε να χρησιμοποιήσουμε αυτά τα δεδομένα για άλλους σκοπούς. Για περισσότερες πληροφορίες, μπορείτε να ανατρέξετε στην $1. Να θυμάστε ότι μπορείτε να μεταβείτε στις ρυθμίσεις και να εξαιρεθείτε ανά πάσα στιγμή.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Βοηθήστε μας να βελτιώσουμε το MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Θα χρησιμοποιήσουμε αυτά τα δεδομένα για να μάθουμε πώς αλληλεπιδράτε με τις επικοινωνίες μάρκετινγκ. Ενδέχεται να κοινοποιήσουμε σχετικές πληροφορίες (όπως χαρακτηριστικά προϊόντων)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Πλήρης πρόσβαση" }, @@ -3289,6 +3570,22 @@ "message": "Οι ειδοποιήσεις ανίχνευσης για phishing βασίζονται στην επικοινωνία με το $1. Το jsDeliver θα έχει πρόσβαση στη διεύθυνση IP σας. Δείτε $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1Η", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1M", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1ΕΒ", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1Ε", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Εξερευνήστε τα Snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Οι ειδοποιήσεις ασφαλείας δεν είναι πλέον διαθέσιμες σε αυτό το δίκτυο. Η εγκατάσταση ενός Snap μπορεί να βελτιώσει την ασφάλειά σας." - }, - "openSeaToBlockaidTitle": { - "message": "Προσοχή!" - }, "operationFailed": { "message": "Η λειτουργία απέτυχε" }, @@ -3421,7 +3709,7 @@ "message": "Ζητήθηκε τώρα" }, "permissionRequestedForAccounts": { - "message": "Ζητείται τώρα για $1", + "message": "Ζητήθηκε τώρα για $1", "description": "Permission cell status for requested permission including accounts, rendered as AvatarGroup which is $1." }, "permissionRevoked": { @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Οι συνδεδεμένοι ιστότοποι είναι τώρα με άδειες χρήσης" }, + "permitSimulationDetailInfo": { + "message": "Δίνετε στον διαθέτη την άδεια να δαπανήσει τα tokens από τον λογαριασμό σας." + }, "personalAddressDetected": { "message": "Η προσωπική διεύθυνση εντοπίστηκε. Καταχωρίστε τη διεύθυνση συμβολαίου του token." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Δημοφιλή προσαρμοσμένα δίκτυα" }, + "popularNetworkAddToolTip": { + "message": "Ορισμένα από αυτά τα δίκτυα βασίζονται σε τρίτους. Οι συνδέσεις μπορεί να είναι λιγότερο αξιόπιστες ή να επιτρέπουν σε τρίτους να παρακολουθούν τη δραστηριότητα. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Χαρτοφυλάκιο" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Προηγούμενο" }, + "price": { + "message": "Τιμή" + }, + "priceUnavailable": { + "message": "μη διαθέσιμη τιμή" + }, "primaryCurrencySetting": { "message": "Κύριο νόμισμα" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Τιμή προσφοράς" }, + "rank": { + "message": "Κατάταξη" + }, "reAddAccounts": { "message": "προσθέστε εκ νέου τυχόν άλλους λογαριασμούς" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Λήψη" }, - "receiveTokensCamelCase": { - "message": "Λάβετε tokens" - }, "recipientAddressPlaceholder": { "message": "Εισάγετε τη δημόσια διεύθυνση (0x) ή το όνομα ENS" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Επαναφορά" }, - "resetStates": { - "message": "Επαναφορά καταστάσεων" - }, "resetWallet": { "message": "Επαναφορά πορτοφολιού" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Αναζήτηση λογαριασμών" }, + "searchNfts": { + "message": "Αναζήτηση για NFT" + }, "searchTokens": { "message": "Αναζήτηση tokens" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Προστατέψτε το πορτοφόλι μου (συνιστάται)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Γράψτε και αποθηκεύστε σε πολλά μυστικά μέρη" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Αποθήκευση σε ένα διαχειριστή κωδικών πρόσβασης" + "message": "Γράψτε και αποθηκεύστε σε πολλά μυστικά μέρη" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Αποθήκευση σε θυρίδα ασφαλείας" }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Επιλέξτε token" }, "selectNFTPrivacyPreference": { - "message": "Ενεργοποιήστε την ανίχνευση των NFT στις Ρυθμίσεις" + "message": "Ενεργοποιήστε την ανίχνευση των NFT" }, "selectPathHelp": { - "message": "Αν δεν βλέπετε τους λογαριασμούς που περιμένατε, δοκιμάστε να αλλάξετε τη διαδρομή HD." + "message": "Αν δεν βλέπετε τους λογαριασμούς που περιμένετε, δοκιμάστε να αλλάξετε τη διαδρομή HD ή το τρέχον επιλεγμένο δίκτυο." }, "selectType": { "message": "Επιλέξτε Τύπο" @@ -4287,9 +4585,6 @@ "send": { "message": "Αποστολή" }, - "sendAToken": { - "message": "Στείλτε ένα token" - }, "sendBugReport": { "message": "Στείλτε μας μια αναφορά σφάλματος." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Δίκτυο δοκιμών Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Διατηρήστε το Service Worker" - }, "setAdvancedPrivacySettingsDetails": { "message": "Το MetaMask χρησιμοποιεί αυτές τις αξιόπιστες υπηρεσίες τρίτων για να ενισχύσει τη χρηστικότητα και την ασφάλεια των προϊόντων." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Αυτό βασίζεται σε διαφορετικά API τρίτων για κάθε δίκτυο, τα οποία εκθέτουν τη διεύθυνση Ethereum και τη διεύθυνση IP σας." }, + "showLess": { + "message": "Εμφάνιση λιγότερων" + }, "showMore": { "message": "Εμφάνιση περισσότερων" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Υπογράψτε αυτό το μήνυμα μόνο εάν κατανοείτε πλήρως το περιεχόμενο και εμπιστεύεστε τον ιστότοπο που το ζητάει." }, - "signatureRequestWarning": { - "message": "Η υπογραφή αυτού του μηνύματος μπορεί να είναι επικίνδυνη. Μπορεί να δώσετε τον πλήρη έλεγχο του λογαριασμού και των περιουσιακών σας στοιχείων στο άτομο που βρίσκεται στην άλλη άκρη αυτού του μηνύματος. Αυτό σημαίνει ότι μπορεί να αδειάσει τον λογαριασμό σας ανά πάσα στιγμή. Προχωρήστε με προσοχή. $1." - }, "signed": { "message": "Υπογράφηκε" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Υπογραφή" }, + "signingInWith": { + "message": "Σύνδεση με" + }, "simulationDetailsFailed": { "message": "Υπήρξε σφάλμα στη φόρτωση της εκτίμησής σας." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Εκτίμηση μεταβολών υπολοίπου" }, + "siweIssued": { + "message": "Εκδόθηκε" + }, + "siweNetwork": { + "message": "Δίκτυο" + }, + "siweRequestId": { + "message": "Αναγνωριστικό αιτήματος" + }, + "siweResources": { + "message": "Πόροι" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Συνδέεστε σε έναν ιστότοπο και δεν προβλέπονται αλλαγές στον λογαριασμό σας." + }, + "siweURI": { + "message": "Διεύθυνση URL" + }, "skip": { "message": "Παράλειψη" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Λογαριασμοί που ελέγχονται από Snaps τρίτων." }, + "snapConnectTo": { + "message": "Σύνδεση με $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Επιτρέψτε στο $1 να συνδεθεί αυτόματα με το $2 χωρίς την έγκρισή σας.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "Το $1 θέλει να χρησιμοποιήσει $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Ιστότοπος" }, + "snapHomeMenu": { + "message": "Αρχικό Μενού του Snap" + }, "snapInstallRequest": { "message": "Η εγκατάσταση του $1 του δίνει τις ακόλουθες άδειες.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Πηγή" }, + "speed": { + "message": "Ταχύτητα" + }, "speedUp": { "message": "Επιτάχυνση" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Πολύ μεγάλο όριο δαπανών" }, + "spender": { + "message": "Διαθέτης" + }, "spendingCap": { "message": "Ανώτατο όριο δαπανών" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Τα αρχεία καταγραφής κατάστασης περιέχουν τις διευθύνσεις του δημόσιου λογαριασμού σας και τις συναλλαγές οι οποίες έχουν αποσταλεί." }, - "states": { - "message": "Καταστάσεις" - }, "status": { "message": "Κατάσταση" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Υποβλήθηκε" }, + "suggestedBySnap": { + "message": "Προτείνεται από $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Προτεινόμενο όνομα:" + }, "suggestedTokenSymbol": { "message": "Προτεινόμενο σύμβολο μετοχής:" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Λήψη προσφορών" + "message": "Λήψη προσφορών..." }, "swapFetchingQuotesErrorDescription": { "message": "Χμμμ... κάτι πήγε στραβά. Προσπαθήστε ξανά, ή αν τα σφάλματα επιμένουν, επικοινωνήστε με την υποστήριξη πελατών." @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Έχετε αλλάξει σε" + "message": "Τώρα χρησιμοποιείτε το" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Η αλλαγή δικτύων θα ακυρώσει όλες τις εκκρεμείς επιβεβαιώσεις" @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "αυτή η συλλογή" }, + "threeMonthsAbbreviation": { + "message": "3Μ", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Ώρα" }, @@ -5498,45 +5836,6 @@ "message": "Προς: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Κινδυνεύετε από επιθέσεις phishing. Προστατευτείτε απενεργοποιώντας το eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Αν ενεργοποιήσετε αυτή τη ρύθμιση, ενδέχεται να λάβετε αιτήματα υπογραφής που δεν είναι αναγνώσιμα. Υπογράφοντας ένα μήνυμα που δεν καταλαβαίνετε, μπορεί να συμφωνείτε να παραχωρήσετε τα κεφάλαια και τα NFT σας." - }, - "toggleEthSignField": { - "message": "Αιτήματα eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " μπορεί να σας εξαπατήσουν" - }, - "toggleEthSignModalBannerText": { - "message": "Αν σας ζητήθηκε να ενεργοποιήσετε αυτή τη ρύθμιση," - }, - "toggleEthSignModalCheckBox": { - "message": "Κατανοώ ότι μπορεί να χάσω όλα μου τα κεφάλαια και τα NFT αν ενεργοποιήσω τα αιτήματα eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Η αποδοχή αιτημάτων eth_sign μπορεί να σας καταστήσει ευάλωτους σε επιθέσεις phishing. Να ελέγχετε πάντα τη διεύθυνση URL και να είστε προσεκτικοί όταν υπογράφετε μηνύματα που περιέχουν κώδικα." - }, - "toggleEthSignModalFormError": { - "message": "Το κείμενο είναι λανθασμένο" - }, - "toggleEthSignModalFormLabel": { - "message": "Πληκτρολογήστε «Υπογράφω μόνο ό,τι κατανοώ» για να συνεχίσετε" - }, - "toggleEthSignModalFormValidation": { - "message": "Υπογράφω μόνο ό,τι κατανοώ" - }, - "toggleEthSignModalTitle": { - "message": "Χρησιμοποιήστε το με δική σας ευθύνη" - }, - "toggleEthSignOff": { - "message": "ΑΝΕΝΕΡΓΟ (συνιστάται)" - }, - "toggleEthSignOn": { - "message": "ΕΝΕΡΓΟ (δεν συνιστάται)" - }, "toggleRequestQueueDescription": { "message": "Αυτό σας επιτρέπει να επιλέξετε ένα δίκτυο για κάθε ιστότοπο αντί για ένα μόνο επιλεγμένο δίκτυο για όλους τους ιστότοπους. Αυτή η λειτουργία θα σας αποτρέψει από το να αλλάζετε δίκτυα χειροκίνητα, το οποίο μπορεί να διαταράξει την εμπειρία του χρήστη σε ορισμένους ιστότοπους." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Διεύθυνση συμβολαίου του token" }, + "tokenDecimal": { + "message": "Δεκαδικά ψηφία του token" + }, "tokenDecimalFetchFailed": { "message": "Απαιτείται δεκαδικό token. Βρείτε το σε: $1" }, @@ -5580,7 +5882,7 @@ "message": "Αναγνωριστικό του token" }, "tokenList": { - "message": "Λίστες με token:" + "message": "Λίστες με tokens" }, "tokenScamSecurityRisk": { "message": "απάτες με token και κίνδυνοι ασφάλειας" @@ -5588,6 +5890,9 @@ "tokenShowUp": { "message": "Τα tokens σας ενδέχεται να μην εμφανιστούν αυτόματα στο πορτοφόλι σας." }, + "tokenStandard": { + "message": "Πρότυπο Token" + }, "tokenSymbol": { "message": "Σύμβολο του token" }, @@ -5598,6 +5903,9 @@ "message": "Βρέθηκαν $1 νέα tokens", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Tokens στη συλλογή" + }, "tooltipApproveButton": { "message": "Κατανοώ" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Σύνολο" }, + "totalVolume": { + "message": "Συνολικός όγκος" + }, "transaction": { "message": "συναλλαγή" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "Η συναλλαγή δημιουργήθηκε με αξία $1 στις $2." }, + "transactionDataFunction": { + "message": "Λειτουργία" + }, "transactionDetailDappGasMoreInfo": { "message": "Προτεινόμενος ιστότοπος" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Μεταφορά από" }, + "trillionAbbreviation": { + "message": "Τ", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Έχουμε πρόβλημα με τη σύνδεσή σας στο Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Σύμφωνα με τα αρχεία μας, αυτή η διεύθυνση URL δεν ταιριάζει με γνωστό πάροχο για αυτό το αναγνωριστικό αλυσίδας." + }, "unapproved": { "message": "Μη εγκεκριμένο" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Ενημέρωση" }, + "updateOrEditNetworkInformations": { + "message": "Ενημερώστε τα στοιχεία σας ή" + }, "updateRequest": { "message": "Αίτημα ενημέρωσης" }, "updatedWithDate": { "message": "Ενημερώθηκε $1" }, + "uploadDropFile": { + "message": "Αφήστε το αρχείο σας εδώ" + }, + "uploadFile": { + "message": "Μεταφόρτωση αρχείου" + }, "urlErrorMsg": { "message": "Οι διευθύνσεις URL απαιτούν το κατάλληλο πρόθεμα HTTP/HTTPS." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Τι είναι αυτό;" }, + "wrongChainId": { + "message": "Αυτό το αναγνωριστικό αλυσίδας δεν ταιριάζει με το όνομα του δικτύου." + }, + "wrongNetworkName": { + "message": "Σύμφωνα με τα αρχεία μας, το όνομα του δικτύου ενδέχεται να μην αντιστοιχεί με αυτό το αναγνωριστικό αλυσίδας." + }, "xOfYPending": { "message": "$1 από $2 σε εκκρεμότητα", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Οι λογαριασμοί σας" }, - "yourFundsMayBeAtRisk": { - "message": "Τα κεφάλαιά σας μπορεί να κινδυνεύουν" + "yourActivity": { + "message": "Η δραστηριότητά σας" + }, + "yourBalance": { + "message": "Το υπόλοιπό σας" }, "yourNFTmayBeAtRisk": { "message": "Τα NFT μπορεί να κινδυνεύουν" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 549762cc1672..b179af364c9a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -42,7 +42,7 @@ "message": "Connect your QR hardware wallet" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (coming soon)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "The address in the sign-in request does not match the address of the account you are using to sign in." @@ -195,6 +195,9 @@ "addAccount": { "message": "Add account" }, + "addAccountToMetaMask": { + "message": "Add account to MetaMask" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, @@ -249,6 +252,9 @@ "addEthereumChainWarningModalTitle": { "message": "You are adding a new RPC provider for Ethereum Mainnet" }, + "addEthereumWatchOnlyAccount": { + "message": "Watch an Ethereum account (Beta)" + }, "addFriendsAndAddresses": { "message": "Add friends and addresses you trust" }, @@ -273,12 +279,22 @@ "addNetwork": { "message": "Add network" }, + "addNetworkConfirmationTitle": { + "message": "Add $1", + "description": "$1 represents network name" + }, "addNetworkTooltipWarning": { "message": "This network connection relies on third parties. This connection may be less reliable or enable third-parties to track activity. $1", "description": "$1 is Learn more link" }, "addNewAccount": { - "message": "Add a new account" + "message": "Add a new Ethereum account" + }, + "addNewBitcoinAccount": { + "message": "Add a new Bitcoin account (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Add a new Bitcoin account (Testnet)" }, "addNewToken": { "message": "Add new token" @@ -314,6 +330,9 @@ "addUrl": { "message": "Add URL" }, + "addingAccount": { + "message": "Adding account" + }, "addingCustomNetwork": { "message": "Adding Network" }, @@ -332,6 +351,17 @@ "addressCopied": { "message": "Address copied!" }, + "addressMismatch": { + "message": "Site address mismatch" + }, + "addressMismatchOriginal": { + "message": "Current URL: $1", + "description": "$1 replaced by origin URL in confirmation request" + }, + "addressMismatchPunycode": { + "message": "Punycode version: $1", + "description": "$1 replaced by punycode version of the URL in confirmation request" + }, "advanced": { "message": "Advanced" }, @@ -397,6 +427,9 @@ "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, + "alertMessageAddressMismatchWarning": { + "message": "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended site before you continue." + }, "alertMessageGasEstimateFailed": { "message": "We’re unable to provide an accurate fee and this estimate might be high. We suggest you to input a custom gas limit, but there’s a risk the transaction will still fail." }, @@ -421,6 +454,9 @@ "alertMessageSignInDomainMismatch": { "message": "The site making the request is not the site you’re signing into. This could be an attempt to steal your login credentials." }, + "alertMessageSignInWrongAccount": { + "message": "This site is asking you to sign in using the wrong account." + }, "alertMessageSigningOrSubmitting": { "message": "This transaction will only go through once your previous transaction is complete." }, @@ -457,6 +493,9 @@ "alertReasonSignIn": { "message": "Suspicious sign-in request" }, + "alertReasonWrongAccount": { + "message": "Wrong account" + }, "alertSettingsUnconnectedAccount": { "message": "Browsing a website with an unconnected account selected" }, @@ -501,12 +540,6 @@ "allow": { "message": "Allow" }, - "allowMetaMaskToDetectNFTs": { - "message": "Allow MetaMask to detect and display your NFTs with autodetection. You’ll be able to:" - }, - "allowMetaMaskToDetectTokens": { - "message": "Allow MetaMask to detect and display your tokens with autodetection. You’ll be able to:" - }, "allowMmiToConnectToCustodian": { "message": "This will allow MMI to connect to $1 to import your accounts." }, @@ -538,9 +571,6 @@ "message": "$1 and $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Announcements" - }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -626,6 +656,9 @@ "attemptToCancelSwapForFree": { "message": "Attempt to cancel swap for free" }, + "attributes": { + "message": "Attributes" + }, "attributions": { "message": "Attributions" }, @@ -747,6 +780,25 @@ "message": "B", "description": "Shortened form of 'billion'" }, + "bitcoinActivityNotSupported": { + "message": "Bitcoin activity is not supported" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Turning on this feature will give you the option to add a Bitcoin Account to your MetaMask Extension derived from your existing Secret Recovery Phrase. This is an experimental Beta feature, so you should use it at your own risk. To give us feedback on this new Bitcoin experience, please fill out this $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Enable \"Add a new Bitcoin account (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Turning on this feature will give you the option to add a Bitcoin Account for the test network." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Enable \"Add a new Bitcoin account (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Account", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -814,6 +866,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Bought for" + }, "bridge": { "message": "Bridge" }, @@ -935,6 +990,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Collection name" + }, "comboNoOptions": { "message": "No options found", "description": "Default text shown in the combo field dropdown if no options." @@ -996,8 +1054,17 @@ "confirmRpcUrlDeletionMessage": { "message": "Are you sure you want to delete the RPC URL? Your information will not be saved for this network." }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." + "confirmTitleApproveTransaction": { + "message": "Allowance request" + }, + "confirmTitleDeployContract": { + "message": "Deploy a contract" + }, + "confirmTitleDescApproveTransaction": { + "message": "This site wants permission to withdraw your NFTs" + }, + "confirmTitleDescDeployContract": { + "message": "This site wants you to deploy a contract" }, "confirmTitleDescPermitSignature": { "message": "This site wants permission to spend your tokens." @@ -1213,6 +1280,9 @@ "copyAddress": { "message": "Copy address to clipboard" }, + "copyAddressShort": { + "message": "Copy address" + }, "copyPrivateKey": { "message": "Copy private key" }, @@ -1240,6 +1310,9 @@ "createSnapAccountTitle": { "message": "Create account" }, + "creatorAddress": { + "message": "Creator address" + }, "crossChainSwapsLink": { "message": "Swap across networks with MetaMask Portfolio" }, @@ -1424,6 +1497,12 @@ "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "data unavailable" + }, + "dateCreated": { + "message": "Date created" + }, "dcent": { "message": "D'Cent" }, @@ -1503,39 +1582,6 @@ "developerOptions": { "message": "Developer Options" }, - "developerOptionsNetworkMenuRedesignDescription": { - "message": "Toggles the new design of the Networks menu" - }, - "developerOptionsNetworkMenuRedesignTitle": { - "message": "Network Menu Redesign" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Resets isShown boolean to false for all announcements. Announcements are the notifications shown in the What's New popup modal." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Resets various states related to onboarding and redirects to the \"Secure Your Wallet\" onboarding page." - }, - "developerOptionsSentryButtonGenerateBackgroundError": { - "message": "Generate Background Error" - }, - "developerOptionsSentryButtonGenerateTrace": { - "message": "Generate Trace" - }, - "developerOptionsSentryButtonGenerateUIError": { - "message": "Generate UI Error" - }, - "developerOptionsSentryDescriptionGenerateBackgroundError": { - "message": "Generate an unhandled $1 in the service worker." - }, - "developerOptionsSentryDescriptionGenerateTrace": { - "message": " Generate a $1 Sentry trace." - }, - "developerOptionsSentryDescriptionGenerateUIError": { - "message": "Generate an unhandled $1 in this window." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Results in a timestamp being continuously saved to session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” is disabled because it does not meet the minimum of a 10% increase from the original gas fee.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1596,12 +1642,6 @@ "displayNftMediaDescription": { "message": "Displaying NFT media and data exposes your IP address to OpenSea or other third parties. This can allow attackers to associate your IP address with your Ethereum address. NFT autodetection relies on this setting, and won't be available when this is turned off." }, - "diveStraightIntoUsingYourNFTs": { - "message": "Dive straight into using your NFTs" - }, - "diveStraightIntoUsingYourTokens": { - "message": "Dive straight into using your tokens" - }, "doNotShare": { "message": "Do not share this with anyone" }, @@ -1718,6 +1758,12 @@ "editGasTooLow": { "message": "Unknown processing time" }, + "editNetworkLink": { + "message": "edit the original network" + }, + "editNetworksTitle": { + "message": "Edit networks" + }, "editNonceField": { "message": "Edit nonce" }, @@ -1730,9 +1776,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Edit speed up gas fee" }, - "effortlesslyNavigateYourDigitalAssets": { - "message": "Effortlessly navigate your digital assets" - }, "enable": { "message": "Enable" }, @@ -1742,9 +1785,6 @@ "enableFromSettings": { "message": " Enable it from Settings." }, - "enableNftAutoDetection": { - "message": "Enable NFT autodetection" - }, "enableSnap": { "message": "Enable" }, @@ -1752,9 +1792,6 @@ "message": "enable $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Enable token autodetection" - }, "enabled": { "message": "Enabled" }, @@ -1865,6 +1902,9 @@ "estimatedFee": { "message": "Estimated fee" }, + "estimatedFeeTooltip": { + "message": "Amount paid to process the transaction on network." + }, "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, @@ -1884,6 +1924,15 @@ "etherscanViewOn": { "message": "View on Etherscan" }, + "existingChainId": { + "message": "The information you have entered is associated with an existing chain ID." + }, + "existingRequestsBannerAlertDesc": { + "message": "To view and confirm your most recent request, you'll need to approve or reject existing requests first." + }, + "existingRpcUrl": { + "message": "This URL is associated with another chain ID." + }, "expandView": { "message": "Expand view" }, @@ -1938,6 +1987,9 @@ "message": "File import not working? Click here!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Find the right one on:" + }, "flaskWelcomeUninstall": { "message": "you should uninstall this extension", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1977,6 +2029,9 @@ "forgotPassword": { "message": "Forgot password?" }, + "form": { + "message": "form" + }, "from": { "message": "From" }, @@ -2185,6 +2240,12 @@ "highLowercase": { "message": "high" }, + "highestCurrentBid": { + "message": "Highest current bid" + }, + "highestFloorPrice": { + "message": "Highest floor price" + }, "history": { "message": "History" }, @@ -2246,12 +2307,6 @@ "imToken": { "message": "imToken" }, - "immediateAccessToYourNFTs": { - "message": "Immediately access your NFTs" - }, - "immediateAccessToYourTokens": { - "message": "Immediate access to your tokens" - }, "import": { "message": "Import", "description": "Button to import an account from a selected file" @@ -2413,7 +2468,7 @@ "message": "Interacting with" }, "interactingWithTransactionDescription": { - "message": "This is the contract you're interacting with. Protect yourself from scammers by verifying the details." + "message": "This is the contract you're interacting with. Protect yourself from scammers by verifying the details." }, "invalidAddress": { "message": "Invalid address" @@ -2530,12 +2585,21 @@ "knownTokenWarning": { "message": "This action will edit tokens that are already listed in your wallet, which can be used to phish you. Only approve if you are certain that you mean to change what these tokens represent. Learn more about $1" }, + "l1Fee": { + "message": "L1 fee" + }, + "l1FeeTooltip": { + "message": "L1 gas fee" + }, + "l2Fee": { + "message": "L2 fee" + }, + "l2FeeTooltip": { + "message": "L2 gas fee" + }, "lastConnected": { "message": "Last connected" }, - "lastPriceSold": { - "message": "Last price sold" - }, "lastSold": { "message": "Last sold" }, @@ -2722,6 +2786,9 @@ "maxFee": { "message": "Max fee" }, + "maxFeeTooltip": { + "message": "A maximum fee provided to pay for the transaction." + }, "maxPriorityFee": { "message": "Max priority fee" }, @@ -2769,8 +2836,8 @@ "methodData": { "message": "Method" }, - "methodDataTransactionDescription": { - "message": "This is the specific action that will be taken. This data can be faked, so be sure you trust the site on the other end." + "methodDataTransactionDesc": { + "message": "Function executed based on decoded input data." }, "methodNotSupported": { "message": "Not supported with this account." @@ -2823,6 +2890,9 @@ "more": { "message": "more" }, + "multichainAddEthereumChainConfirmationDescription": { + "message": "You're adding this network to MetaMask and giving this site permission to use it." + }, "multipleSnapConnectionWarning": { "message": "$1 wants to use $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." @@ -2889,6 +2959,10 @@ "message": "Choose a nickname...", "description": "Placeholder text for name input field in name component modal." }, + "nativeNetworkPermissionRequestDescription": { + "message": "$1 is asking for your approval to:", + "description": "$1 represents dapp name" + }, "nativePermissionRequestDescription": { "message": "Do you want this site to do the following?", "description": "Description below header used on Permission Connect screen for native permissions." @@ -3017,12 +3091,20 @@ "message": "We can't connect to $1", "description": "$1 represents the network name" }, + "networkSwitchMessage": { + "message": "Network switched to $1", + "description": "$1 represents the network name" + }, "networkURL": { "message": "Network URL" }, "networkURLDefinition": { "message": "The URL used to access this network." }, + "networkUrlErrorWarning": { + "message": "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended site before you continue. Punycode version: $1", + "description": "$1 replaced by RPC URL for network" + }, "networks": { "message": "Networks" }, @@ -3186,8 +3268,8 @@ "nonceField": { "message": "Customize transaction nonce" }, - "nonceFieldDescription": { - "message": "Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously." + "nonceFieldDesc": { + "message": "Turn this on to change the nonce (transaction number) when sending assets. This is an advanced feature, use cautiously." }, "nonceFieldHeading": { "message": "Custom nonce" @@ -3204,9 +3286,6 @@ "notEnoughGas": { "message": "Not enough gas" }, - "notRightNow": { - "message": "Not right now" - }, "note": { "message": "Note" }, @@ -3392,6 +3471,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 new token found in this account" }, + "numberOfTokens": { + "message": "Number of tokens" + }, "ofTextNofM": { "message": "of" }, @@ -3438,9 +3520,6 @@ "onboardedMetametricsTitle": { "message": "Help us enhance your experience" }, - "onboarding": { - "message": "Onboarding" - }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "The IPFS gateway makes it possible to access and view data hosted by third parties. You can add a custom IPFS gateway or continue using the default." }, @@ -3477,9 +3556,6 @@ "onboardingMetametricsDescription2": { "message": "When we gather metrics, it will always be..." }, - "onboardingMetametricsDisagree": { - "message": "No thanks" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3603,15 +3679,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Explore Snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Security alerts are no longer available on this network. Installing a Snap may improve your security." - }, - "openSeaToBlockaidTitle": { - "message": "Heads up!" - }, "operationFailed": { "message": "Operation Failed" }, @@ -3785,6 +3852,14 @@ "message": "Let $1 access your preferred language from your MetaMask settings. This can be used to localize and display $1's content using your language.", "description": "An extended description for the `snap_getLocale` permission. $1 is the snap name." }, + "permission_getPreferences": { + "message": "See information like your preferred language and fiat currency.", + "description": "The description for the `snap_getPreferences` permission" + }, + "permission_getPreferencesDescription": { + "message": "Let $1 access information like your preferred language and fiat currency in your MetaMask settings. This helps $1 display content tailored to your preferences. ", + "description": "An extended description for the `snap_getPreferences` permission. $1 is the snap name." + }, "permission_homePage": { "message": "Display a custom screen", "description": "The description for the `endowment:page-home` permission" @@ -3914,7 +3989,7 @@ "description": "The description for the `snap_getBip32PublicKey` permission. $1 is a name for the derivation path, e.g., 'Ethereum accounts'." }, "permission_walletSwitchEthereumChain": { - "message": "Switch to and use the following network", + "message": "Use your enabled networks", "description": "The label for the `wallet_switchEthereumChain` permission" }, "permission_webAssembly": { @@ -3991,6 +4066,12 @@ "prev": { "message": "Prev" }, + "price": { + "message": "Price" + }, + "priceUnavailable": { + "message": "price unavailable" + }, "primaryCurrencySetting": { "message": "Primary currency" }, @@ -4143,6 +4224,9 @@ "quoteRate": { "message": "Quote rate" }, + "rank": { + "message": "Rank" + }, "reAddAccounts": { "message": "re-add any other accounts" }, @@ -4155,9 +4239,6 @@ "receive": { "message": "Receive" }, - "receiveTokensCamelCase": { - "message": "Receive tokens" - }, "recipientAddressPlaceholder": { "message": "Enter public address (0x) or ENS name" }, @@ -4197,6 +4278,12 @@ "redesignedConfirmationsToggleDescription": { "message": "Turn this on to see signature requests in an enhanced format." }, + "redesignedTransactionsEnabledToggle": { + "message": "Improved transaction requests" + }, + "redesignedTransactionsToggleDescription": { + "message": "Turn this on to see transactions requests in an enhanced format." + }, "refreshList": { "message": "Refresh list" }, @@ -4312,9 +4399,6 @@ "reset": { "message": "Reset" }, - "resetStates": { - "message": "Reset States" - }, "resetWallet": { "message": "Reset wallet" }, @@ -4400,6 +4484,9 @@ "reviewAlerts": { "message": "Review alerts" }, + "reviewPermissions": { + "message": "Review permissions" + }, "revokeAllTokensTitle": { "message": "Revoke permission to access and transfer all of your $1?", "description": "$1 is the symbol of the token for which the user is revoking approval" @@ -4482,6 +4569,10 @@ "message": "Powered by $1", "description": "The security provider that is providing data" }, + "seeAllPermissions": { + "message": "See all permissions", + "description": "Used for revealing more content (e.g. permission list, etc.)" + }, "seeDetails": { "message": "See details" }, @@ -4497,13 +4588,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Secure my wallet (recommended)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Write down and store in multiple secret places" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Save in a password manager" + "message": "Write down and store in multiple secret places" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Store in a safe deposit box" }, "seedPhraseIntroSidebarCopyOne": { @@ -4587,9 +4675,6 @@ "send": { "message": "Send" }, - "sendAToken": { - "message": "Send a token" - }, "sendBugReport": { "message": "Send us a bug report." }, @@ -4640,9 +4725,6 @@ "sepolia": { "message": "Sepolia test network" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Keep Alive" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask uses these trusted third-party services to enhance product usability and safety." }, @@ -4699,6 +4781,9 @@ "showIncomingTransactionsExplainer": { "message": "This relies on different third-party APIs for each network, which expose your Ethereum address and your IP address." }, + "showLess": { + "message": "Show less" + }, "showMore": { "message": "Show more" }, @@ -4729,9 +4814,6 @@ "signatureRequestGuidance": { "message": "Only sign this message if you fully understand the content and trust the requesting site." }, - "signatureRequestWarning": { - "message": "Signing this message could be dangerous. You may be giving total control of your account and assets to the party on the other end of this message. That means they could drain your account at any time. Proceed with caution. $1." - }, "signed": { "message": "Signed" }, @@ -4744,6 +4826,12 @@ "signingInWith": { "message": "Signing in with" }, + "simulationApproveHeading": { + "message": "Withdraw" + }, + "simulationDetailsApproveDesc": { + "message": "You're giving someone else permission to withdraw NFTs from your account." + }, "simulationDetailsFailed": { "message": "There was an error loading your estimation." }, @@ -4921,6 +5009,9 @@ "snapDetailWebsite": { "message": "Website" }, + "snapHomeMenu": { + "message": "Snap Home Menu" + }, "snapInstallRequest": { "message": "Installing $1 gives it the following permissions.", "description": "$1 is the snap name." @@ -5000,7 +5091,7 @@ "message": "Snaps connected" }, "snapsNoInsight": { - "message": "The snap didn't return any insight" + "message": "No insight to show" }, "snapsPrivacyWarningFirstMessage": { "message": "You acknowledge that any Snap that you install is a Third Party Service, unless otherwise identified, as defined in the Consensys $1. Your use of Third Party Services is governed by separate terms and conditions set forth by the Third Party Service provider. Consensys does not recommend the use of any Snap by any particular person for any particular reason. You access, rely upon or use the Third Party Service at your own risk. Consensys disclaims all responsibility and liability for any losses on account of your use of Third Party Services.", @@ -5043,6 +5134,9 @@ "source": { "message": "Source" }, + "speed": { + "message": "Speed" + }, "speedUp": { "message": "Speed up" }, @@ -5080,6 +5174,9 @@ "spender": { "message": "Spender" }, + "spenderTooltipDesc": { + "message": "This is the address that will be able to withdraw your NFTs." + }, "spendingCap": { "message": "Spending cap" }, @@ -5200,9 +5297,6 @@ "stateLogsDescription": { "message": "State logs contain your public account addresses and sent transactions." }, - "states": { - "message": "States" - }, "status": { "message": "Status" }, @@ -5808,6 +5902,9 @@ "testNetworks": { "message": "Test networks" }, + "testnets": { + "message": "Testnets" + }, "theme": { "message": "Theme" }, @@ -5841,45 +5938,6 @@ "message": "To: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "You’re at risk for phishing attacks. Protect yourself by turning off eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "If you enable this setting, you might get signature requests that aren’t readable. By signing a message you don't understand, you could be agreeing to give away your funds and NFTs." - }, - "toggleEthSignField": { - "message": "Eth_sign requests" - }, - "toggleEthSignModalBannerBoldText": { - "message": " you might be getting scammed" - }, - "toggleEthSignModalBannerText": { - "message": "If you've been asked to turn this setting on," - }, - "toggleEthSignModalCheckBox": { - "message": "I understand that I can lose all of my funds and NFTs if I enable eth_sign requests. " - }, - "toggleEthSignModalDescription": { - "message": "Allowing eth_sign requests can make you vulnerable to phishing attacks. Always review the URL and be careful when signing messages that contain code." - }, - "toggleEthSignModalFormError": { - "message": "The text is incorrect" - }, - "toggleEthSignModalFormLabel": { - "message": "Enter “I only sign what I understand” to continue" - }, - "toggleEthSignModalFormValidation": { - "message": "I only sign what I understand" - }, - "toggleEthSignModalTitle": { - "message": "Use at your own risk" - }, - "toggleEthSignOff": { - "message": "OFF (Recommended)" - }, - "toggleEthSignOn": { - "message": "ON (Not recommended)" - }, "toggleRequestQueueDescription": { "message": "This allows you to select a network for each site instead of a single selected network for all sites. This feature will prevent you from switching networks manually, which may break your user experience on certain sites." }, @@ -5934,6 +5992,9 @@ "tokenShowUp": { "message": "Your tokens may not automatically show up in your wallet. " }, + "tokenStandard": { + "message": "Token standard" + }, "tokenSymbol": { "message": "Token symbol" }, @@ -5944,6 +6005,9 @@ "message": "$1 new tokens found", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Tokens in collection" + }, "tooltipApproveButton": { "message": "I understand" }, @@ -5977,6 +6041,9 @@ "transactionCreated": { "message": "Transaction created with a value of $1 at $2." }, + "transactionDataFunction": { + "message": "Function" + }, "transactionDetailDappGasMoreInfo": { "message": "Site suggested" }, @@ -6149,6 +6216,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "According to our records, this URL does not match a known provider for this chain ID." + }, "unapproved": { "message": "Unapproved" }, @@ -6200,6 +6270,9 @@ "update": { "message": "Update" }, + "updateOrEditNetworkInformations": { + "message": "Update your information or" + }, "updateRequest": { "message": "Update request" }, @@ -6381,6 +6454,13 @@ "message": "$1 The third party could spend your entire token balance without further notice or consent. Protect yourself by customizing a lower spending cap.", "description": "$1 is a warning icon with text 'Be careful' in 'warning' colour" }, + "watchEthereumAccountsDescription": { + "message": "Turning this option on will give you the ability to watch Ethereum accounts via a public address or ENS name. For feedback on this Beta feature please complete this $1.", + "description": "$1 is the link to a product feedback form" + }, + "watchEthereumAccountsToggle": { + "message": "Watch Ethereum Accounts (Beta)" + }, "weak": { "message": "Weak" }, @@ -6427,6 +6507,9 @@ "whatsThis": { "message": "What's this?" }, + "wrongChainId": { + "message": "This chain ID doesn’t match the network name." + }, "wrongNetworkName": { "message": "According to our records, the network name may not correctly match this chain ID." }, @@ -6459,9 +6542,6 @@ "yourBalance": { "message": "Your balance" }, - "yourFundsMayBeAtRisk": { - "message": "Your funds may be at risk" - }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 1787f059bea9..b7e15bc86b55 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -556,9 +556,6 @@ "message": "$1 and $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Announcements" - }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -1042,9 +1039,6 @@ "confirmRpcUrlDeletionMessage": { "message": "Are you sure you want to delete the RPC URL? Your information will not be saved for this network." }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." - }, "confirmTitleDescPermitSignature": { "message": "This site wants permission to spend your tokens." }, @@ -1555,30 +1549,6 @@ "details": { "message": "Details" }, - "developerOptions": { - "message": "Developer Options" - }, - "developerOptionsEnableConfirmationsRedesignDescription": { - "message": "Enables or disables the confirmations redesign feature currently in development" - }, - "developerOptionsEnableConfirmationsRedesignTitle": { - "message": "Confirmations Redesign" - }, - "developerOptionsNetworkMenuRedesignDescription": { - "message": "Toggles the new design of the Networks menu" - }, - "developerOptionsNetworkMenuRedesignTitle": { - "message": "Network Menu Redesign" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Resets isShown boolean to false for all announcements. Announcements are the notifications shown in the What's New popup modal." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Resets various states related to onboarding and redirects to the \"Secure Your Wallet\" onboarding page." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Results in a timestamp being continuously saved to session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” is disabled because it does not meet the minimum of a 10% increase from the original gas fee.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -3526,9 +3496,6 @@ "onboardedMetametricsTitle": { "message": "Help us enhance your experience" }, - "onboarding": { - "message": "Onboarding" - }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "The IPFS gateway makes it possible to access and view data hosted by third parties. You can add a custom IPFS gateway or continue using the default." }, @@ -4240,9 +4207,6 @@ "receive": { "message": "Receive" }, - "receiveTokensCamelCase": { - "message": "Receive tokens" - }, "recipientAddressPlaceholder": { "message": "Enter public address (0x) or ENS name" }, @@ -4397,9 +4361,6 @@ "reset": { "message": "Reset" }, - "resetStates": { - "message": "Reset States" - }, "resetWallet": { "message": "Reset wallet" }, @@ -4722,9 +4683,6 @@ "sepolia": { "message": "Sepolia test network" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Keep Alive" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask uses these trusted third-party services to enhance product usability and safety." }, @@ -4814,9 +4772,6 @@ "signatureRequestGuidance": { "message": "Only sign this message if you fully understand the content and trust the requesting site." }, - "signatureRequestWarning": { - "message": "Signing this message could be dangerous. You may be giving total control of your account and assets to the party on the other end of this message. That means they could drain your account at any time. Proceed with caution. $1." - }, "signed": { "message": "Signed" }, @@ -5291,9 +5246,6 @@ "stateLogsDescription": { "message": "State logs contain your public account addresses and sent transactions." }, - "states": { - "message": "States" - }, "status": { "message": "Status" }, @@ -6568,9 +6520,6 @@ "yourBalance": { "message": "Your balance" }, - "yourFundsMayBeAtRisk": { - "message": "Your funds may be at risk" - }, "yourNFTmayBeAtRisk": { "message": "Your NFT may be at risk" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index cb367513eff5..a20b831091a0 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -42,7 +42,7 @@ "message": "Conecte su monedero físico QR" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (próximamente)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "La dirección de la solicitud de inicio de sesión no coincide con la dirección de la cuenta que está utilizando para iniciar sesión." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Debe seleccionar una cuenta." }, + "accountTypeNotSupported": { + "message": "Tipo de cuenta no admitido" + }, "accounts": { "message": "Cuentas" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Añadir una cuenta nueva de Ethereum" }, + "addNewBitcoinAccount": { + "message": "Añadir una nueva cuenta de Bitcoin (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Añadir una nueva cuenta de Bitcoin (Testnet)" + }, "addNewToken": { "message": "Agregar nuevo token" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Añadir NFT" }, + "addRpcUrl": { + "message": "Agregar URL RPC" + }, "addSnapAccountToggle": { "message": "Activar \"Añadir una cuenta Snap (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "¿No encuentra un token? Puede agregar cualquier token si copia su dirección. Puede encontrar la dirección de contrato del token en $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Agregar URL" + }, "addingCustomNetwork": { "message": "Agregando red" }, "addingTokens": { "message": "Agregando tokens" }, + "additionalNetworks": { + "message": "Redes adicionales" + }, + "additionalRpcUrl": { + "message": "URL RPC adicional" + }, "address": { "message": "Dirección" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Configuración avanzada" }, + "advancedDetailsDataDesc": { + "message": "Datos" + }, + "advancedDetailsHexDesc": { + "message": "Hex" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Este es el número de transacción de una cuenta. El nonce para la primera transacción es 0 y aumenta en orden secuencial." + }, "advancedGasFeeDefaultOptIn": { "message": "Guarde estos valores como mis valores por defecto para la red de $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Alerta" }, + "alertActionBuy": { + "message": "Comprar ETH" + }, + "alertActionUpdateGas": { + "message": "Actualizar el límite de gas" + }, + "alertActionUpdateGasFee": { + "message": "Actualizar tarifa" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Actualizar opciones de gas" + }, "alertBannerMultipleAlertsDescription": { "message": "Si aprueba esta solicitud, un tercero conocido por estafas podría quedarse con todos sus activos." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, + "alertMessageGasEstimateFailed": { + "message": "No podemos proporcionar una tarifa exacta y esta estimación podría ser alta. Le sugerimos que ingrese un límite de gas personalizado, pero existe el riesgo de que la transacción aún falle." + }, + "alertMessageGasFeeLow": { + "message": "Al elegir una tarifa baja, tenga en cuenta que las transacciones serán más lentas y los tiempos de espera más largos. Para transacciones más rápidas, elija las opciones de tarifa de mercado o agresiva." + }, + "alertMessageGasTooLow": { + "message": "Para continuar con esta transacción, deberá aumentar el límite de gas a 21000 o más." + }, + "alertMessageInsufficientBalance": { + "message": "No tiene suficiente ETH en su cuenta para pagar las tarifas de transacción." + }, + "alertMessageNetworkBusy": { + "message": "Los precios del gas son altos y las estimaciones son menos precisas." + }, + "alertMessageNoGasPrice": { + "message": "No podemos seguir adelante con esta transacción hasta que actualice manualmente la tarifa." + }, + "alertMessagePendingTransactions": { + "message": "Esta transacción no se realizará hasta que se complete una transacción anterior. Aprenda cómo cancelar o acelerar una transacción." + }, + "alertMessageSignInDomainMismatch": { + "message": "El sitio que realiza la solicitud no es el sitio en el que está iniciando sesión. Esto podría ser un intento de robar sus credenciales de inicio de sesión." + }, + "alertMessageSignInWrongAccount": { + "message": "Este sitio le pide que inicie sesión con la cuenta incorrecta." + }, + "alertMessageSigningOrSubmitting": { + "message": "Esta transacción solo se realizará una vez que se complete la transacción anterior." + }, "alertModalAcknowledge": { "message": "Soy consciente del riesgo y aun así deseo continuar" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Revisar todas las alertas" }, + "alertReasonGasEstimateFailed": { + "message": "Tarifa inexacta" + }, + "alertReasonGasFeeLow": { + "message": "Velocidad baja" + }, + "alertReasonGasTooLow": { + "message": "Límite de gas bajo" + }, + "alertReasonInsufficientBalance": { + "message": "Fondos insuficientes" + }, + "alertReasonNetworkBusy": { + "message": "La red está ocupada" + }, + "alertReasonNoGasPrice": { + "message": "Estimación de tarifa no disponible" + }, + "alertReasonPendingTransactions": { + "message": "Transacción pendiente" + }, + "alertReasonSignIn": { + "message": "Solicitud de inicio de sesión sospechosa" + }, + "alertReasonWrongAccount": { + "message": "Cuenta incorrecta" + }, "alertSettingsUnconnectedAccount": { "message": "Explorando un sitio web con una cuenta no conectada seleccionada" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Todos los permisos" }, + "allTimeHigh": { + "message": "Punto más alto" + }, + "allTimeLow": { + "message": "Punto más bajo" + }, "allYourNFTsOf": { "message": "Todos sus NFT de $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 y $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Anuncios" - }, "appDescription": { "message": "Un monedero de Ethereum en el explorador", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Opciones de activos" }, "attemptSendingAssets": { - "message": "Si intenta enviar activos directamente de una red a otra, esto puede provocar la pérdida permanente de activos. Asegúrese de utilizar un puente." + "message": "Puede perder sus activos si intenta enviarlos desde otra red. Transfiera fondos de forma segura entre redes mediante el uso de un puente." }, "attemptSendingAssetsWithPortfolio": { "message": "Puede perder sus activos si intenta enviarlos desde otra red. Transfiera fondos de forma segura entre redes usando un puente, como $1" @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "Intente cancelar el intercambio de forma gratuita" }, + "attributes": { + "message": "Atributos" + }, "attributions": { "message": "Atribuciones" }, + "auroraRpcDeprecationMessage": { + "message": "La URL de RPC de Infura ya no es compatible con Aurora." + }, "authorizedPermissions": { "message": "Ha autorizado los siguientes permisos" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "La funcionalidad básica está desactivada" }, + "basicConfigurationDescription": { + "message": "MetaMask ofrece funciones básicas como los detalles del token y la configuración de gas mediante servicios de Internet. Al utilizar los servicios de Internet, su dirección IP es compartida, en este caso con MetaMask. Es lo mismo que cuando visita cualquier página web. MetaMask utiliza estos datos temporalmente y nunca los vende. Puede utilizar una VPN o desactivar estos servicios, pero esto podría afectar su experiencia con MetaMask. Para saber más, consulte nuestra $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Funcionalidad básica" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask beta nunca le pedirá su frase secreta de recuperación." }, + "billionAbbreviation": { + "message": "mm", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "La actividad de Bitcoin no es compatible" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Activar esta función le dará la opción de añadir una cuenta de Bitcoin a su extensión MetaMask derivada de su frase secreta de recuperación existente. Esta es una característica Beta experimental, por lo que debe utilizarla bajo su propio riesgo. Para darnos su opinión sobre esta nueva experiencia Bitcoin, llene este $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Activar \"Añadir una nueva cuenta Bitcoin (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "La activación de esta función le dará la opción de añadir una cuenta de Bitcoin para la red de prueba." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Activar \"Añadir una nueva cuenta Bitcoin (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Cuenta", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "No le recomendamos que siga adelante con esta solicitud." + }, "blockaidDescriptionApproveFarming": { "message": "Si aprueba esta solicitud, un tercero conocido por realizar estafas podría tomar todos sus activos." }, @@ -666,7 +807,7 @@ "message": "Si aprueba esta solicitud, alguien puede robar sus activos enlistados en Blur." }, "blockaidDescriptionErrored": { - "message": "Debido a un error, el proveedor de seguridad no verificó esta solicitud. Proceda con precaución." + "message": "Debido a un error, no pudimos comprobar si hay alertas de seguridad. Continúe solo si confía en todas las direcciones involucradas." }, "blockaidDescriptionMaliciousDomain": { "message": "Está interactuando con un dominio malicioso. Si aprueba esta solicitud, podría perder sus activos." @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Si aprueba esta solicitud, un tercero conocido por estafas tomará todos sus activos." }, + "blockaidDescriptionWarning": { + "message": "Podría tratarse de una solicitud engañosa. Continúe solamente si confía en todas las direcciones implicadas." + }, "blockaidMessage": { "message": "Preservación de la privacidad: no se comparten datos con terceros. Disponible en Arbitrum, Avalanche, BNB Chain, la red principal de Ethereum, Linea, Optimism, Polygon, Base y Sepolia." }, @@ -687,7 +831,7 @@ "message": "Esta es una solicitud engañosa" }, "blockaidTitleMayNotBeSafe": { - "message": "La solicitud puede no ser segura" + "message": "Sea cuidadoso" }, "blockaidTitleSuspicious": { "message": "Esta es una solicitud sospechosa" @@ -695,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Comprado para" + }, "bridge": { "message": "Puente" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Debe usar MetaMask en Google Chrome para poder conectarse a su monedero físico." }, + "circulatingSupply": { + "message": "Suministro circulante" + }, "clear": { "message": "Borrar" }, @@ -799,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Haga clic aquí para agregar manualmente los tokens." + "message": "Siempre puede agregar tókenes manualmente." }, "close": { "message": "Cerrar" @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Nombre de la colección" + }, "comboNoOptions": { "message": "No se encontraron opciones", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Soy consciente de las alertas y aun así deseo continuar" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Soy consciente de la alerta y aun así deseo continuar" + }, "confirmAlertModalDetails": { "message": "Si inicia sesión, un tercero conocido por estafas podría quedarse con todos sus activos. Revise las alertas antes de continuar." }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Confirmar conexión a $1" }, + "confirmDeletion": { + "message": "Confirmar la eliminación" + }, + "confirmFieldPaymaster": { + "message": "Tarifa pagada por" + }, + "confirmFieldTooltipPaymaster": { + "message": "La tarifa de esta transacción la pagará el contrato inteligente del pagador." + }, "confirmPassword": { "message": "Confirmar contraseña" }, "confirmRecoveryPhrase": { "message": "Confirmar frase secreta de recuperación" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Solo confirme esta transacción si entiende completamente el contenido y confía en el sitio solicitante." + "confirmRpcUrlDeletionMessage": { + "message": "¿Está seguro de que desea eliminar la URL RPC? Su información no se guardará para esta red." + }, + "confirmTitleDescPermitSignature": { + "message": "Este sitio solicita permiso para gastar sus tokens." + }, + "confirmTitleDescSIWESignature": { + "message": "Un sitio quiere que inicie sesión para demostrar que es el propietario de esta cuenta." }, "confirmTitleDescSignature": { "message": "Solo confirme este mensaje si aprueba el contenido y confía en el sitio solicitante." }, + "confirmTitlePermitSignature": { + "message": "Solicitud de límite de gasto" + }, + "confirmTitleSIWESignature": { + "message": "Solicitud de inicio de sesión" + }, "confirmTitleSignature": { "message": "Solicitud de firma" }, @@ -958,7 +1135,7 @@ "message": "Conectado con" }, "connecting": { - "message": "Estableciendo conexión…" + "message": "Conectando" }, "connectingTo": { "message": "Estableciendo conexión a $1" @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Crear cuenta" }, + "creatorAddress": { + "message": "Dirección del creador" + }, "crossChainSwapsLink": { "message": "Intercambie entre redes con MetaMask Portfolio" }, @@ -1260,9 +1440,27 @@ "data": { "message": "Datos" }, + "dataCollectionForMarketing": { + "message": "Recopilación de datos para marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "Usaremos MetaMetrics para saber cómo interactúa con nuestras comunicaciones de marketing. Es posible que compartamos noticias relevantes (como características del producto y otros materiales)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Bien" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Desactivó la recopilación de datos para nuestros fines de marketing. Esto solo se aplica a este dispositivo. Si utiliza MetaMask en otros dispositivos, asegúrese de desactivarlo allí también." + }, "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "datos no disponibles" + }, + "dateCreated": { + "message": "Fecha de creación" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "Descifrar solicitud" }, + "defaultRpcUrl": { + "message": "URL RPC por defecto" + }, "delete": { "message": "Eliminar" }, @@ -1308,6 +1509,9 @@ "message": "¿Eliminar la red de $1?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Eliminar URL RPC" + }, "deposit": { "message": "Depositar" }, @@ -1333,18 +1537,6 @@ "details": { "message": "Detalles" }, - "developerOptions": { - "message": "Opciones de desarrollador" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Restablece el valor booleano isShown a falso para todos los anuncios. Los anuncios son las notificaciones que se muestran en la ventana emergente Novedades." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Restablece varios estados relacionados con la incorporación y redirige a la página de incorporación \"Asegure su monedero\"." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "El resultado es una marca de tiempo que se guarda continuamente en session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” está desactivado porque no cumple el mínimo de un aumento del 10 % respecto a la tarifa de gas original.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "Se desconoce el tiempo de procesamiento" }, + "editNetworkLink": { + "message": "editar la red original" + }, "editNonceField": { "message": "Editar nonce" }, @@ -1549,12 +1744,12 @@ "message": "activar $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Activar la autodetección de tokens" - }, "enabled": { "message": "Activado" }, + "enabledNetworks": { + "message": "Redes habilitadas" + }, "encryptionPublicKeyNotice": { "message": "$1 quisiera su clave pública de cifrado. Al aceptar, este sitio podrá redactar mensajes cifrados para usted.", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "Tarifa estimada" }, + "estimatedFeeTooltip": { + "message": "Monto pagado para procesar la transacción en la red." + }, "ethGasPriceFetchWarning": { "message": "Se muestra el precio del gas de respaldo, ya que el servicio para calcular el precio del gas principal no se encuentra disponible en este momento." }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Ver en Etherscan" }, + "existingChainId": { + "message": "La información que ha ingresado está asociada con un ID de cadena existente." + }, + "existingRpcUrl": { + "message": "Esta URL está asociada a otro ID de cadena." + }, "expandView": { "message": "Expandir vista" }, @@ -1732,6 +1936,9 @@ "message": "¿No funciona la importación del archivo? Haga clic aquí.", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Encuentre el correcto en:" + }, "flaskWelcomeUninstall": { "message": "le recomendamos que desinstale esta extensión", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "¿Olvidó su contraseña?" }, + "form": { + "message": "formulario" + }, "from": { "message": "De" }, @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "alto" }, + "highestCurrentBid": { + "message": "Oferta actual más alta" + }, + "highestFloorPrice": { + "message": "Precio mínimo más alto" + }, "history": { "message": "Historial" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "Esta acción editará tokens que ya estén enumerados en su monedero y que se pueden usar para engañarlo. Apruebe solo si está seguro de que quiere cambiar lo que representan estos tokens. Más información sobre $1" }, + "l1Fee": { + "message": "Tarifa L1" + }, + "l1FeeTooltip": { + "message": "Tarifa de gas L1" + }, + "l2Fee": { + "message": "Tarifa L2" + }, + "l2FeeTooltip": { + "message": "Tarifa de gas L2" + }, "lastConnected": { "message": "Última conexión" }, - "lastPriceSold": { - "message": "Precio de la última venta" - }, "lastSold": { "message": "Última venta" }, @@ -2495,6 +2720,12 @@ "message": "Asegúrese de que nadie esté mirando", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Capitalización bursátil" + }, + "marketDetails": { + "message": "Detalles del mercado" + }, "max": { "message": "Máx." }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "Tarifa máxima" }, + "maxFeeTooltip": { + "message": "Una tarifa máxima proporcionada para pagar la transacción." + }, "maxPriorityFee": { "message": "Tarifa de prioridad máxima" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "Método" }, - "methodDataTransactionDescription": { - "message": "Esta es la acción específica que se llevará a cabo. Estos datos pueden falsificarse, así que asegúrese de que confía en el otro sitio." + "methodDataTransactionDesc": { + "message": "Función ejecutada en base a datos de entrada decodificados." }, "methodNotSupported": { "message": "No compatible con esta cuenta." @@ -2560,6 +2794,10 @@ "metrics": { "message": "Indicadores" }, + "millionAbbreviation": { + "message": "m", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Su cuenta seleccionada ($1) es diferente a la cuenta que intenta firmar ($2)" }, @@ -2672,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "El token nativo en esta red es de $1. Es el token utilizado para las tarifas de gas.", + "message": "El token nativo en esta red es de $1. Es el token utilizado para las tarifas de gas. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "El nombre asociado a esta red." }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Opciones de red" + }, "networkProvider": { "message": "Proveedor de red" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "¡\"$1\" se añadió con éxito!" }, + "newNetworkEdited": { + "message": "¡\"$1\" se editó con éxito!" + }, "newNftAddedMessage": { "message": "¡NFT se agregó correctamente!" }, @@ -2869,8 +3116,11 @@ "nftAlreadyAdded": { "message": "NFT ya ha sido añadido." }, + "nftAutoDetectionEnabled": { + "message": "Autodetección de NFT habilitada" + }, "nftDisclaimer": { - "message": "Descargo de responsabilidad: MetaMask extrae el archivo multimedia de la URL de origen. Esta URL a veces es modificada por el mercado en el que se acuñó el NFT." + "message": "Descargo de responsabilidad: MetaMask extrae el archivo multimedia de la URL de origen. A veces, el mercado en el que se acuñó el NFT cambia esta URL." }, "nftOptions": { "message": "Opciones de NFT" @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "No se proporcionó resolución para el dominio." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snaps, y la mayoría de los monederos físicos, no funcionarán con la versión actual de su navegador." + }, "noNFTs": { "message": "No hay ningún NFT aún" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "Personalizar nonce de transacción" }, - "nonceFieldDescription": { - "message": "Active esta opción para cambiar el nonce (número de transacción) en las pantallas de confirmación. Esta es una función avanzada, úsela con precaución." + "nonceFieldDesc": { + "message": "Active esta opción para cambiar el nonce (número de transacción) al enviar activos. Esta es una función avanzada, úsela con precaución." }, "nonceFieldHeading": { "message": "Nonce personalizado" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 nuevo token encontrado en esta cuenta" }, + "numberOfTokens": { + "message": "Número de tokens" + }, "ofTextNofM": { "message": "de" }, @@ -3167,8 +3423,36 @@ "on": { "message": "Activado" }, - "onboarding": { - "message": "Incorporación" + "onboardedMetametricsAccept": { + "message": "Acepto" + }, + "onboardedMetametricsDisagree": { + "message": "No, gracias" + }, + "onboardedMetametricsKey1": { + "message": "Últimas novedades" + }, + "onboardedMetametricsKey2": { + "message": "Características de productos" + }, + "onboardedMetametricsKey3": { + "message": "Otros materiales promocionales relevantes" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Además de $1, nos gustaría utilizar datos para comprender cómo interactúa con las comunicaciones de marketing.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Esto nos ayuda a personalizar lo que compartimos con usted, como:" + }, + "onboardedMetametricsParagraph3": { + "message": "Recuerde, nunca vendemos los datos que usted proporciona y puede optar por no participar en cualquier momento." + }, + "onboardedMetametricsTitle": { + "message": "Ayúdenos a mejorar su experiencia" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "La puerta de enlace de IPFS permite acceder y visualizar datos alojados por terceros. Puede agregar una puerta de enlace de IPFS personalizada o continuar usando la predeterminada." @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Al recopilar métricas, siempre será..." }, - "onboardingMetametricsDisagree": { - "message": "No, gracias" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Ayúdenos a mejorar MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Utilizaremos estos datos para saber cómo interactúa con nuestras comunicaciones de marketing. Es posible que compartamos noticias relevantes (como características del producto)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Acceso completo" }, @@ -3286,6 +3570,22 @@ "message": "Las alertas de detección de phishing se basan en la comunicación con $1. jsDeliver tendrá acceso a su dirección IP. Ver 2$.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 d", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 m", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 s", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 a", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Explorar Snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Las alertas de seguridad ya no están disponibles en esta red. Instalar un Snap podría mejorar su seguridad." - }, - "openSeaToBlockaidTitle": { - "message": "¡Atención!" - }, "operationFailed": { "message": "Operación fallida" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Los sitios conectados ahora tienen permisos" }, + "permitSimulationDetailInfo": { + "message": "Le está dando permiso al gastador para gastar esta cantidad de tokens de su cuenta." + }, "personalAddressDetected": { "message": "Se detectó una dirección personal. Ingrese la dirección de contrato del token." }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "Redes populares personalizadas" }, + "popularNetworkAddToolTip": { + "message": "Algunas de estas redes dependen de terceros. Las conexiones pueden ser menos confiables o permitir que terceros realicen un seguimiento de la actividad. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portafolio" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "Ant." }, + "price": { + "message": "Precio" + }, + "priceUnavailable": { + "message": "precio no disponible" + }, "primaryCurrencySetting": { "message": "Moneda principal" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "Tarifa de cotización" }, + "rank": { + "message": "Rango" + }, "reAddAccounts": { "message": "volver a agregar cualquier otra cuenta" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "Recibir" }, - "receiveTokensCamelCase": { - "message": "Recibir tokens" - }, "recipientAddressPlaceholder": { "message": "Ingrese la dirección pública (0x) o el nombre de ENS" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "Restablecer" }, - "resetStates": { - "message": "Restablecer estados" - }, "resetWallet": { "message": "Restablecer monedero" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "Buscar cuentas" }, + "searchNfts": { + "message": "Buscar NFT" + }, "searchTokens": { "message": "Buscar tokens" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Proteger mi monedero (recomendado)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Escríbala y guárdela en varios lugares secretos." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Guárdela en un gestor de contraseñas" + "message": "Escríbala y guárdela en varios lugares secretos." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Guárdela en una caja fuerte." }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,10 +4571,10 @@ "message": "Seleccionar token" }, "selectNFTPrivacyPreference": { - "message": "Active la detección de NFT en Configuraciones" + "message": "Habilite la autodetección de NFT" }, "selectPathHelp": { - "message": "Si no ve las cuentas previstas, intente cambiar la ruta HD." + "message": "Si no ve las cuentas previstas, intente cambiar la ruta HD o la red seleccionada actualmente." }, "selectType": { "message": "Seleccionar tipo" @@ -4284,9 +4585,6 @@ "send": { "message": "Enviar" }, - "sendAToken": { - "message": "Enviar un token" - }, "sendBugReport": { "message": "Envíenos un informe de error." }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Red de prueba Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Mantener activo el Service Worker" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask utiliza estos servicios de terceros de confianza para mejorar la usabilidad y la seguridad de los productos." }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Esto se basa en diferentes API de terceros para cada red, que exponen su dirección Ethereum y su dirección IP." }, + "showLess": { + "message": "Mostrar menos" + }, "showMore": { "message": "Mostrar más" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Solo firme este mensaje si comprende completamente el contenido y confía en el sitio solicitante." }, - "signatureRequestWarning": { - "message": "Firmar este mensaje podría ser peligroso. Es posible que le esté otorgando el control total de su cuenta y activos a la contraparte de este mensaje. Eso significa que podrían vaciar su cuenta en cualquier momento. Proceda con precaución. $1." - }, "signed": { "message": "Firmado" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "Firmando" }, + "signingInWith": { + "message": "Iniciar sesión con" + }, "simulationDetailsFailed": { "message": "Se produjo un error al cargar su estimación." }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Estimar cambios de saldo" }, + "siweIssued": { + "message": "Emitido" + }, + "siweNetwork": { + "message": "Red" + }, + "siweRequestId": { + "message": "Solicitar ID" + }, + "siweResources": { + "message": "Recursos" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Está iniciando sesión en un sitio y no se prevén cambios en su cuenta." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Omitir" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "Cuentas controladas por Snaps de terceros." }, + "snapConnectTo": { + "message": "Conectarse a $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Permita que $1 se conecte automáticamente a $2 sin su aprobación.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 quiere conectarse a $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "Sitio web" }, + "snapHomeMenu": { + "message": "Menú de inicio de Snap" + }, "snapInstallRequest": { "message": "Instalar $1 le otorga los siguientes permisos.", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "Fuente" }, + "speed": { + "message": "Velocidad" + }, "speedUp": { "message": "Acelerar" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "El límite de gastos es demasiado alto" }, + "spender": { + "message": "Gastador" + }, "spendingCap": { "message": "Límite de gasto" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "Los registros de estado contienen sus direcciones de cuentas públicas y las transacciones enviadas." }, - "states": { - "message": "Estados" - }, "status": { "message": "Estado" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "Enviado" }, + "suggestedBySnap": { + "message": "Sugerido por $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Nombre sugerido:" + }, "suggestedTokenSymbol": { "message": "Símbolo de cotización sugerido:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Obtener cotizaciones" + "message": "Obteniendo cotizaciones..." }, "swapFetchingQuotesErrorDescription": { "message": "Se produjo un error. Vuelva a intentarlo o, si el error persiste, póngase en contacto con el soporte al cliente." @@ -5434,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Ha cambiado a" + "message": "Ahora está usando" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Cambiar de red cancelará todas las confirmaciones pendientes" @@ -5473,7 +5810,7 @@ "message": "Elija su tema MetaMask preferido." }, "thingsToKeep": { - "message": "Cosas a tener en cuenta:" + "message": "Tenga en cuenta:" }, "thirdPartySoftware": { "message": "Aviso de software de terceros", @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "esta colección" }, + "threeMonthsAbbreviation": { + "message": "3 m", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Tiempo" }, @@ -5495,45 +5836,6 @@ "message": "Para: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Está en riesgo de sufrir ataques de phishing. Protéjase desactivando eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Si activa esta opción, es posible que reciba solicitudes de firma que no sean legibles. Al firmar un mensaje que no entiende, podría estar dando su consentimiento para ceder sus fondos y NFT." - }, - "toggleEthSignField": { - "message": "Solicitudes de eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " usted podría estar siendo estafado" - }, - "toggleEthSignModalBannerText": { - "message": "Si se le ha pedido que active esta configuración," - }, - "toggleEthSignModalCheckBox": { - "message": "Entiendo que puedo perder todos mis fondos y mis NFT si activo las solicitudes de eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Permitir solicitudes eth_sign puede hacerlo vulnerable a ataques de phishing. Siempre revise la URL y tenga cuidado al firmar mensajes que contengan código." - }, - "toggleEthSignModalFormError": { - "message": "El texto es incorrecto" - }, - "toggleEthSignModalFormLabel": { - "message": "Ingrese “Firmo solo lo que entiendo” para continuar" - }, - "toggleEthSignModalFormValidation": { - "message": "Firmo solo lo que entiendo" - }, - "toggleEthSignModalTitle": { - "message": "Úselo bajo su propio riesgo" - }, - "toggleEthSignOff": { - "message": "DESACTIVADO (Recomendado)" - }, - "toggleEthSignOn": { - "message": "ACTIVADO (No recomendado)" - }, "toggleRequestQueueDescription": { "message": "Esto le permite seleccionar una red para cada sitio en lugar de una única red seleccionada para todos los sitios. Esta función evitará que cambie de red manualmente, lo que puede afectar su experiencia de usuario en ciertos sitios." }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "Dirección de contrato de token" }, + "tokenDecimal": { + "message": "Decimales del token" + }, "tokenDecimalFetchFailed": { "message": "Se requiere decimal del token. Encuéntrelo en: $1" }, @@ -5577,13 +5882,16 @@ "message": "ID de token" }, "tokenList": { - "message": "Listas de tokens:" + "message": "Lista de tókenes" }, "tokenScamSecurityRisk": { "message": "estafas de tokens y riesgos de seguridad" }, "tokenShowUp": { - "message": "Es posible que sus tokens no aparezcan automáticamente en su monedero." + "message": "Es posible que sus tókenes no aparezcan automáticamente en su monedero. " + }, + "tokenStandard": { + "message": "Estándar de tokenes" }, "tokenSymbol": { "message": "Símbolo del token" @@ -5595,6 +5903,9 @@ "message": "$1 nuevos tokens encontrados", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Tokens en la colección" + }, "tooltipApproveButton": { "message": "Comprendo" }, @@ -5610,6 +5921,9 @@ "total": { "message": "Total" }, + "totalVolume": { + "message": "Volúmen total" + }, "transaction": { "message": "transacción" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "La transacción se creó con un valor de $1 en $2." }, + "transactionDataFunction": { + "message": "Función" + }, "transactionDetailDappGasMoreInfo": { "message": "Sitio sugerido" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "Transferir desde" }, + "trillionAbbreviation": { + "message": "b", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Tenemos problemas para conectarnos con su Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Según nuestros registros, esta URL no coincide con un proveedor conocido para este ID de cadena." + }, "unapproved": { "message": "No aprobado" }, @@ -5844,12 +6168,21 @@ "update": { "message": "Actualizar" }, + "updateOrEditNetworkInformations": { + "message": "Actualice su información o" + }, "updateRequest": { "message": "Solicitud de actualización" }, "updatedWithDate": { "message": "$1 actualizado" }, + "uploadDropFile": { + "message": "Ingrese su archivo aquí" + }, + "uploadFile": { + "message": "Cargar archivo" + }, "urlErrorMsg": { "message": "Las direcciones URL requieren el prefijo HTTP/HTTPS adecuado." }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "¿Qué es esto?" }, + "wrongChainId": { + "message": "Este ID de cadena no coincide con el nombre de la red." + }, + "wrongNetworkName": { + "message": "Según nuestros registros, es posible que el nombre de la red no coincida correctamente con este ID de cadena." + }, "xOfYPending": { "message": "$1 de $2 están pendientes", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "Sus cuentas" }, - "yourFundsMayBeAtRisk": { - "message": "Sus fondos podrían estar en riesgo" + "yourActivity": { + "message": "Su actividad" + }, + "yourBalance": { + "message": "Su saldo" }, "yourNFTmayBeAtRisk": { "message": "Sus NFT podrían estar en riesgo" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 662a7f3ede27..c406955c36c9 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -42,7 +42,7 @@ "message": "Cartera HW con QR" }, "QRHardwareWalletSteps2Description": { - "message": "AirGap Vault y Ngrave (próximamente)" + "message": "Ngrave Zero" }, "about": { "message": "Acerca de" @@ -1363,9 +1363,6 @@ "nonceField": { "message": "Personalizar nonce de transacción" }, - "nonceFieldDescription": { - "message": "Active esta opción para cambiar el nonce (número de transacción) en las pantallas de confirmación. Esta es una función avanzada, úsela con precaución." - }, "nonceFieldHeading": { "message": "Nonce personalizado" }, @@ -1698,13 +1695,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Asegurar mi cartera (recomendado)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Escríbala y guárdela en varios lugares secretos." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Guárdela en un administrador de contraseñas" + "message": "Escríbala y guárdela en varios lugares secretos." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Guárdela en una caja fuerte." }, "seedPhraseIntroSidebarCopyOne": { diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 011ebc4fa589..28d08810ae82 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -42,7 +42,7 @@ "message": "Connectez votre portefeuille électronique QR" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (bientôt disponible)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "L’adresse figurant dans la demande de connexion ne correspond pas à l’adresse du compte que vous utilisez pour vous connecter." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Vous devez sélectionner un compte !" }, + "accountTypeNotSupported": { + "message": "Ce type de compte n’est pas pris en charge" + }, "accounts": { "message": "Comptes" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Ajouter un nouveau compte Ethereum" }, + "addNewBitcoinAccount": { + "message": "Ajouter un nouveau compte Bitcoin (Bêta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Ajouter un nouveau compte Bitcoin (Testnet)" + }, "addNewToken": { "message": "Ajouter un nouveau jeton" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Ajouter des NFT" }, + "addRpcUrl": { + "message": "Ajouter l’URL du RPC" + }, "addSnapAccountToggle": { "message": "Activer « Ajouter un Snap de compte (bêta) »" }, @@ -305,12 +317,21 @@ "message": "Vous n’arrivez pas à trouver un jeton ? Vous pouvez ajouter manuellement n’importe quel jeton en copiant et collant son adresse. Les adresses des contrats de jetons sont disponibles sur $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Ajouter l'URL" + }, "addingCustomNetwork": { "message": "Ajout de réseau" }, "addingTokens": { "message": "Ajouter des jetons" }, + "additionalNetworks": { + "message": "Réseaux supplémentaires" + }, + "additionalRpcUrl": { + "message": "URL supplémentaire de RPC" + }, "address": { "message": "Adresse" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Configuration avancée" }, + "advancedDetailsDataDesc": { + "message": "Données" + }, + "advancedDetailsHexDesc": { + "message": "Hexa" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Il s’agit du nombre de transactions d’un compte. Le nonce de la première transaction est 0 et il augmente d’une manière séquentielle." + }, "advancedGasFeeDefaultOptIn": { "message": "Enregistrer ces valeurs comme valeurs par défaut pour le réseau $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Alerte" }, + "alertActionBuy": { + "message": "Acheter de l’ETH" + }, + "alertActionUpdateGas": { + "message": "Mettre à jour la limite de gaz" + }, + "alertActionUpdateGasFee": { + "message": "Actualiser les frais" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Mettre à jour les options de gaz" + }, "alertBannerMultipleAlertsDescription": { "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Vous pouvez modifier ceci dans « Paramètres > Alertes »" }, + "alertMessageGasEstimateFailed": { + "message": "Nous ne sommes pas en mesure de déterminer le montant exact des frais et cette estimation peut être élevée. Nous vous suggérons d’entrer une limite de gaz personnalisée, mais la transaction pourrait quand même échouer." + }, + "alertMessageGasFeeLow": { + "message": "Si vous choisissez des frais peu élevés, attendez-vous à des transactions plus lentes et à des temps d’attente plus longs. Pour des transactions plus rapides, choisissez les options « Ordre au marché » ou « Agressif »." + }, + "alertMessageGasTooLow": { + "message": "Pour effectuer cette transaction, vous devez augmenter la limite de gaz à 21 000 ou plus." + }, + "alertMessageInsufficientBalance": { + "message": "Vous n’avez pas assez d’ETH sur votre compte pour payer les frais de transaction." + }, + "alertMessageNetworkBusy": { + "message": "Les prix du gaz sont élevés et les estimations sont moins précises." + }, + "alertMessageNoGasPrice": { + "message": "Nous ne pouvons pas valider cette transaction tant que vous n’avez pas mis à jour manuellement les frais." + }, + "alertMessagePendingTransactions": { + "message": "La transaction précédente doit être finalisée avant que celle-ci ne soit traitée. Découvrez comment vous pouvez annuler ou accélérer une transaction." + }, + "alertMessageSignInDomainMismatch": { + "message": "Le site auquel vous êtes en train de vous connecter n’est pas le site à l’origine de la demande. Il pourrait s’agir d’une tentative de vol de vos identifiants de connexion." + }, + "alertMessageSignInWrongAccount": { + "message": "Ce site vous demande de vous connecter en utilisant le mauvais compte." + }, + "alertMessageSigningOrSubmitting": { + "message": "La transaction précédente doit être finalisée avant que celle-ci ne soit traitée." + }, "alertModalAcknowledge": { "message": "Je suis conscient du risque et je souhaite quand même continuer" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Examiner toutes les alertes" }, + "alertReasonGasEstimateFailed": { + "message": "Frais inexacts" + }, + "alertReasonGasFeeLow": { + "message": "Vitesse lente" + }, + "alertReasonGasTooLow": { + "message": "Limite de gaz trop basse" + }, + "alertReasonInsufficientBalance": { + "message": "Fonds insuffisants" + }, + "alertReasonNetworkBusy": { + "message": "Le réseau est occupé" + }, + "alertReasonNoGasPrice": { + "message": "Estimation des frais non disponible" + }, + "alertReasonPendingTransactions": { + "message": "Transaction en attente" + }, + "alertReasonSignIn": { + "message": "Demande de connexion suspecte" + }, + "alertReasonWrongAccount": { + "message": "Mauvais compte" + }, "alertSettingsUnconnectedAccount": { "message": "Navigation sur un site Web avec un compte non connecté sélectionné" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Toutes les autorisations" }, + "allTimeHigh": { + "message": "Le plus haut niveau jamais atteint" + }, + "allTimeLow": { + "message": "Le plus bas niveau jamais atteint" + }, "allYourNFTsOf": { "message": "Tous vos NFT via $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 et $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Annonces" - }, "appDescription": { "message": "Extension Ethereum pour navigateur", "description": "The description of the application" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Tentative d’annuler gratuitement le swap" }, + "attributes": { + "message": "Attributs" + }, "attributions": { "message": "Attributions" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "La fonctionnalité de base est désactivée" }, + "basicConfigurationDescription": { + "message": "MetaMask offre des fonctionnalités de base telles que l’affichage des détails des jetons et des paramètres de gaz par le biais de services Internet. Lorsque vous utilisez des services Internet, votre adresse IP est partagée avec le fournisseur de ces services, dans ce cas MetaMask. C’est la même chose que lorsque vous visitez un site web. MetaMask conserve ces données temporairement et ne les vend jamais. Vous pouvez utiliser un VPN ou désactiver ces services, mais cela peut affecter votre expérience avec MetaMask. Pour en savoir plus, lisez notre $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Fonctionnalité de base" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "La version bêta de MetaMask ne vous demandera jamais votre phrase secrète de récupération." }, + "billionAbbreviation": { + "message": "Mrd", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "L’activité Bitcoin n’est pas prise en charge" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "En activant cette fonctionnalité, vous aurez la possibilité d’ajouter un compte Bitcoin à votre extension MetaMask dérivée de votre phrase secrète de récupération existante. Toute utilisation de cette fonctionnalité bêta expérimentale se fait à vos risques et périls. Pour nous faire part de vos commentaires sur cette nouvelle expérience Bitcoin, veuillez remplir ce $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Activer « Ajouter un nouveau compte Bitcoin (Bêta) »" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "En activant cette fonctionnalité, vous aurez la possibilité d’ajouter un compte Bitcoin pour le réseau de test." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Activer « Ajouter un nouveau compte Bitcoin (Testnet) »" + }, "blockExplorerAccountAction": { "message": "Compte", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "BlockAid" }, + "blockaidAlertInfo": { + "message": "Nous ne vous recommandons pas de donner suite à cette demande." + }, "blockaidDescriptionApproveFarming": { "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." }, @@ -669,7 +807,7 @@ "message": "Si vous approuvez cette demande, quelqu’un pourrait s'emparer de vos actifs répertoriés sur Blur." }, "blockaidDescriptionErrored": { - "message": "À la suite d’une erreur, cette demande n’a pas été vérifiée par le fournisseur de services de sécurité. Veuillez agir avec prudence." + "message": "En raison d’une erreur, nous n’avons pas pu vérifier les alertes de sécurité. Ne continuez que si vous faites confiance à toutes les adresses concernées." }, "blockaidDescriptionMaliciousDomain": { "message": "Vous interagissez avec un domaine malveillant. Si vous approuvez cette demande, vous risquez de perdre vos actifs." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." }, + "blockaidDescriptionWarning": { + "message": "Il pourrait s’agir d’une demande trompeuse. Ne continuez que si vous faites confiance à toutes les adresses concernées." + }, "blockaidMessage": { "message": "Protection de la vie privée : aucune donnée n’est partagée avec des tiers. Disponible sur Arbitrum, Avalanche, BNB chain, Linea, Optimism, Polygon, Base, Sepolia et le réseau principal Ethereum." }, @@ -690,7 +831,7 @@ "message": "Cette demande trompeuse" }, "blockaidTitleMayNotBeSafe": { - "message": "Cette demande peut présenter des risques" + "message": "Soyez prudent" }, "blockaidTitleSuspicious": { "message": "Cette demande suspecte" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Acheté pour" + }, "bridge": { "message": "Pont" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Pour connecter votre portefeuille matériel, vous devez utiliser MetaMask pour Google Chrome." }, + "circulatingSupply": { + "message": "Offre en circulation" + }, "clear": { "message": "Effacer" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Cliquez ici pour ajouter les jetons manuellement." + "message": "Vous pouvez ajouter des jetons manuellement." }, "close": { "message": "Fermer" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Nom de la collection" + }, "comboNoOptions": { "message": "Aucune option trouvée", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "J’ai pris connaissance des alertes, mais je souhaite quand même continuer" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "J’ai pris connaissance de l’alerte, mais je souhaite quand même continuer" + }, "confirmAlertModalDetails": { "message": "Si vous vous connectez, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs. Veuillez examiner les alertes avant de continuer." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Confirmer la connexion à $1" }, + "confirmDeletion": { + "message": "Confirmer la suppression" + }, + "confirmFieldPaymaster": { + "message": "Frais payés par" + }, + "confirmFieldTooltipPaymaster": { + "message": "Les frais de cette transaction seront payés par le contrat « Paymaster » intelligent." + }, "confirmPassword": { "message": "Confirmer le mot de passe" }, "confirmRecoveryPhrase": { "message": "Confirmer la phrase secrète de récupération" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Ne confirmez cette transaction que si vous comprenez parfaitement son contenu et si vous faites confiance au site demandeur." + "confirmRpcUrlDeletionMessage": { + "message": "Voulez-vous vraiment supprimer l’URL du RPC ? Vos informations ne seront pas sauvegardées pour ce réseau." + }, + "confirmTitleDescPermitSignature": { + "message": "Ce site demande que vous lui accordiez l'autorisation de dépenser vos jetons." + }, + "confirmTitleDescSIWESignature": { + "message": "Un site vous demande de vous connecter pour prouver que vous êtes le titulaire de ce compte." }, "confirmTitleDescSignature": { "message": "Ne confirmez ce message que si vous approuvez son contenu et faites confiance au site demandeur." }, + "confirmTitlePermitSignature": { + "message": "Demande de plafonnement des dépenses" + }, + "confirmTitleSIWESignature": { + "message": "Demande de connexion" + }, "confirmTitleSignature": { "message": "Demande de signature" }, @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Créer un compte" }, + "creatorAddress": { + "message": "Adresse du créateur" + }, "crossChainSwapsLink": { "message": "Échanges inter-réseaux avec MetaMask Portfolio" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Données" }, + "dataCollectionForMarketing": { + "message": "Collecte de données à des fins de marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "Nous utiliserons MetaMetrics pour savoir comment vous interagissez avec nos communications commerciales et pour partager avec vous des informations pertinentes (comme les caractéristiques des produits et d’autres contenus)." + }, + "dataCollectionWarningPopoverButton": { + "message": "OK" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Vous avez désactivé la collecte de données à des fins de marketing. Cela ne s’applique qu’à cet appareil. Si vous utilisez MetaMask sur d’autres appareils, n’oubliez pas de désactiver cette fonctionnalité sur ces appareils aussi." + }, "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "données non disponibles" + }, + "dateCreated": { + "message": "Date de création" + }, "dcent": { "message": "D’Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Décrypter la demande" }, + "defaultRpcUrl": { + "message": "URL par défaut du RPC" + }, "delete": { "message": "Supprimer" }, @@ -1311,6 +1509,9 @@ "message": "Supprimer le réseau $1 ?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Supprimer l’URL du RPC" + }, "deposit": { "message": "Effectuez un dépôt" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Détails" }, - "developerOptions": { - "message": "Options pour les développeurs" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Règle la valeur de « isShown boolean » sur faux (false) pour toutes les annonces. Les annonces sont les notifications affichées dans la fenêtre contextuelle « Nouveautés »." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Réinitialise divers états liés au processus d’intégration et redirige vers la page d’accueil « Sécuriser votre portefeuille »." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Les résultats d’un horodatage sont continuellement sauvegardés dans « session.storage »" - }, "disabledGasOptionToolTipMessage": { "message": "« $1 » est désactivé parce qu’il ne correspond pas au minimum d’augmentation de 10 % par rapport aux gas fees initiaux.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Délai de traitement inconnu" }, + "editNetworkLink": { + "message": "modifier le réseau d’origine" + }, "editNonceField": { "message": "Modifier le nonce" }, @@ -1552,12 +1744,12 @@ "message": "activer $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Activer la détection automatique des jetons" - }, "enabled": { "message": "Activé" }, + "enabledNetworks": { + "message": "Réseaux activés" + }, "encryptionPublicKeyNotice": { "message": "$1 aimerait avoir votre clé publique de cryptage. En y consentant, ce site sera en mesure de vous composer des messages cryptés.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Frais estimés" }, + "estimatedFeeTooltip": { + "message": "Montant payé pour traiter la transaction sur le réseau." + }, "ethGasPriceFetchWarning": { "message": "Le prix de carburant de sauvegarde est fourni, car le service principal d’estimation du carburant est momentanément indisponible." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Afficher sur Etherscan" }, + "existingChainId": { + "message": "Les informations que vous avez saisies sont associées à un ID de chaîne existant." + }, + "existingRpcUrl": { + "message": "Cette URL est associée à un autre ID de chaîne." + }, "expandView": { "message": "Agrandir la vue" }, @@ -1735,6 +1936,9 @@ "message": "L’importation de fichier ne fonctionne pas ? Cliquez ici !", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Trouvez le bon ID de chaîne sur :" + }, "flaskWelcomeUninstall": { "message": "vous devriez désinstaller cette extension", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Mot de passe oublié ?" }, + "form": { + "message": "formulaire" + }, "from": { "message": "de" }, @@ -1884,7 +2091,7 @@ "message": "Quelque chose a mal tourné…" }, "genericExplorerView": { - "message": "Afficher le compte sur $1" + "message": "Voir le compte sur $1" }, "getStartedWithNFTs": { "message": "Obtenez des $1 pour acheter des NFT", @@ -1904,7 +2111,7 @@ "message": "Testnet Goerli" }, "gotIt": { - "message": "J’ai compris !" + "message": "D’accord" }, "grantedToWithColon": { "message": "Accordé à :" @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "élevé" }, + "highestCurrentBid": { + "message": "Offre actuelle la plus élevée" + }, + "highestFloorPrice": { + "message": "Prix plancher le plus élevé" + }, "history": { "message": "Historique" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Cette action modifiera les jetons déjà présents dans votre portefeuille, et risque de favoriser les tentatives d’hameçonnage. N’approuvez que si vous êtes certain·e de vouloir modifier ce que ces jetons représentent. En savoir plus sur $1" }, + "l1Fee": { + "message": "Frais L1" + }, + "l1FeeTooltip": { + "message": "Frais de gaz L1" + }, + "l2Fee": { + "message": "Frais L2" + }, + "l2FeeTooltip": { + "message": "Frais de gaz L2" + }, "lastConnected": { "message": "Dernière connexion" }, - "lastPriceSold": { - "message": "Prix de la dernière vente" - }, "lastSold": { "message": "Dernière vente" }, @@ -2498,6 +2720,12 @@ "message": "Assurez-vous que personne ne regarde votre écran", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Capitalisation boursière" + }, + "marketDetails": { + "message": "Détails du marché" + }, "max": { "message": "Max." }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Frais maximaux" }, + "maxFeeTooltip": { + "message": "Frais maximums prévus pour le traitement de la transaction." + }, "maxPriorityFee": { "message": "Frais de priorité maximaux" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Méthode" }, - "methodDataTransactionDescription": { - "message": "Il s’agit de l’action spécifique qui sera entreprise. Ces données peuvent être falsifiées. Alors veillez à ce que le site qui se trouve à l’autre bout soit digne de confiance." + "methodDataTransactionDesc": { + "message": "Fonction exécutée en fonction des données d’entrée décodées." }, "methodNotSupported": { "message": "Non pris en charge par ce compte." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Indicateurs" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Le compte sélectionné ($1) est différent du compte que vous essayez de signer ($2)" }, @@ -2675,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Le jeton natif de ce réseau est $1. C’est le jeton utilisé pour les frais de gaz.\n", + "message": "Le jeton natif de ce réseau est $1. C’est le jeton utilisé pour les frais de gaz. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Le nom associé à ce réseau." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Options du réseau" + }, "networkProvider": { "message": "Fournisseur de réseau" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "« $1 » a été ajouté avec succès !" }, + "newNetworkEdited": { + "message": "« $1 » a été modifié !" + }, "newNftAddedMessage": { "message": "Le NFT a été ajouté avec succès !" }, @@ -2872,8 +3116,11 @@ "nftAlreadyAdded": { "message": "Le NFT a déjà été ajouté." }, + "nftAutoDetectionEnabled": { + "message": "Détection automatique des NFT activée" + }, "nftDisclaimer": { - "message": "Avertissement : MetaMask importe le fichier multimédia de l'URL source. Cette URL est parfois modifiée par la place de marché sur laquelle le NFT a été frappé." + "message": "Avertissement : MetaMask importe le fichier multimédia de l'URL source. Cette URL est parfois modifiée par la place de marché sur laquelle le NFT a été minté." }, "nftOptions": { "message": "Options NFT" @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Aucune résolution n’a été fournie pour le domaine." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Les Snaps et la plupart des portefeuilles matériels ne fonctionneront pas avec la version actuelle de votre navigateur." + }, "noNFTs": { "message": "Aucun NFT pour le moment" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Personnaliser le nonce de transaction" }, - "nonceFieldDescription": { - "message": "Activez cette option pour modifier le nonce (numéro de transaction) sur les écrans de confirmation. Il s’agit d’une fonctionnalité avancée, à utiliser avec précaution." + "nonceFieldDesc": { + "message": "Activez cette option pour modifier le nonce (numéro de transaction) lors de l’envoi d’actifs. Il s’agit d’une fonctionnalité avancée que vous devez utiliser avec prudence." }, "nonceFieldHeading": { "message": "Nonce personnalisé" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 nouveau jeton trouvé dans ce compte" }, + "numberOfTokens": { + "message": "Nombre de jetons" + }, "ofTextNofM": { "message": "de" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Activé" }, - "onboarding": { - "message": "Intégration" + "onboardedMetametricsAccept": { + "message": "J’accepte" + }, + "onboardedMetametricsDisagree": { + "message": "Non, merci" + }, + "onboardedMetametricsKey1": { + "message": "Dernières avancées" + }, + "onboardedMetametricsKey2": { + "message": "Caractéristiques du produit" + }, + "onboardedMetametricsKey3": { + "message": "Autres matériels promotionnels pertinents" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "En plus de $1, nous aimerions utiliser les données pour comprendre comment vous interagissez avec les communications commerciales.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Cela nous aide à personnaliser les informations que nous partageons avec vous, comme :" + }, + "onboardedMetametricsParagraph3": { + "message": "N’oubliez pas que nous ne vendons jamais les données que vous nous fournissez et que vous pouvez vous désinscrire à tout moment." + }, + "onboardedMetametricsTitle": { + "message": "Aidez-nous à améliorer votre expérience utilisateur" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "La passerelle IPFS vous permet d’accéder aux données hébergées par des tiers et de les visualiser. Vous pouvez ajouter une passerelle IPFS personnalisée ou continuer à utiliser la passerelle par défaut." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Lorsque nous recueillons des données, elles sont toujours…" }, - "onboardingMetametricsDisagree": { - "message": "Non merci" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Aidez-nous à améliorer MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Nous utiliserons ces données pour savoir comment vous interagissez avec nos communications commerciales et pour partager avec vous des informations pertinentes (comme les caractéristiques des produits)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Accès complet" }, @@ -3289,6 +3570,22 @@ "message": "Les alertes de détection d’hameçonnage reposent sur la communication avec $1. jsDeliver aura accès à votre adresse IP. Voir $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 j", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 mois", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 sem.", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 an", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Explorer les Snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Les alertes de sécurité ne sont plus disponibles sur ce réseau. L’installation d’un Snap peut améliorer la sécurité." - }, - "openSeaToBlockaidTitle": { - "message": "Attention !" - }, "operationFailed": { "message": "L’opération a échoué" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Les sites connectés sont maintenant des autorisations" }, + "permitSimulationDetailInfo": { + "message": "Vous autorisez la dépenseur à dépenser ce nombre de jetons de votre compte." + }, "personalAddressDetected": { "message": "Votre adresse personnelle a été détectée. Veuillez saisir à la place l’adresse du contrat du jeton." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Réseaux personnalisés populaires" }, + "popularNetworkAddToolTip": { + "message": "Certains de ces réseaux dépendent de services tiers. Ils peuvent être moins fiables ou permettre à des tiers de suivre l’activité des utilisateurs. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portefeuille" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Préc." }, + "price": { + "message": "Prix" + }, + "priceUnavailable": { + "message": "prix non disponible" + }, "primaryCurrencySetting": { "message": "Devise principale" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Cotation" }, + "rank": { + "message": "Rang" + }, "reAddAccounts": { "message": "ajouter de nouveau tous les autres comptes" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Recevoir" }, - "receiveTokensCamelCase": { - "message": "Recevez des jetons" - }, "recipientAddressPlaceholder": { "message": "Saisissez l’adresse publique (0x) ou le nom de domaine ENS" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Reinitialiser" }, - "resetStates": { - "message": "Réinitialiser les états" - }, "resetWallet": { "message": "Réinitialiser le portefeuille" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Rechercher des comptes" }, + "searchNfts": { + "message": "Recherche de NFT" + }, "searchTokens": { "message": "Rechercher des jetons" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Sécuriser mon portefeuille (recommandé)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Notez-la et conservez-la dans plusieurs endroits secrets." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Sauvegarder dans un gestionnaire de mots de passe" + "message": "Notez-la et conservez-la dans plusieurs endroits secrets." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Stocker dans un coffre-fort." }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Sélectionnez un jeton" }, "selectNFTPrivacyPreference": { - "message": "Activez la détection de NFT dans les Paramètres" + "message": "Activez la détection automatique des NFT" }, "selectPathHelp": { - "message": "Si vos comptes n’apparaissent pas ci-dessous, essayez de sélectionner le chemin HD." + "message": "Si vous ne voyez pas les comptes auxquels vous vous attendez, essayez de changer le chemin d’accès au portefeuille HD ou le réseau sélectionné." }, "selectType": { "message": "Sélectionner le type" @@ -4287,9 +4585,6 @@ "send": { "message": "Envoyer" }, - "sendAToken": { - "message": "Envoyer un jeton" - }, "sendBugReport": { "message": "Envoyez-nous un rapport de bogue." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Réseau de test Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Garder le « Service Worker » activé" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask utilise ces services tiers de confiance pour améliorer la convivialité et la sécurité des produits." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Ce processus fait appel à différentes API tierces pour chaque réseau et expose ainsi votre adresse Ethereum et votre adresse IP." }, + "showLess": { + "message": "Afficher moins" + }, "showMore": { "message": "Afficher plus" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Ne signez ce message que si vous comprenez parfaitement son contenu et si le site demandeur vous inspire confiance." }, - "signatureRequestWarning": { - "message": "Signer ce message peut être dangereux. Vous risquez de céder le contrôle de tous vos actifs et de votre compte à la personne qui vous a envoyé ce message et si cette personne est malintentionnée, elle pourra vider votre compte à tout moment, alors agissez avec prudence. $1." - }, "signed": { "message": "Signé" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Signature" }, + "signingInWith": { + "message": "Se connecter avec" + }, "simulationDetailsFailed": { "message": "Une erreur s’est produite lors du chargement de l’estimation." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Estimer les changements de solde" }, + "siweIssued": { + "message": "Émis" + }, + "siweNetwork": { + "message": "Réseau" + }, + "siweRequestId": { + "message": "Demander un ID" + }, + "siweResources": { + "message": "Ressources" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Vous êtes en train de vous connecter à un site, aucun changement ne devrait être apporté à votre compte." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Ignorer" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Comptes contrôlés par des Snaps tiers." }, + "snapConnectTo": { + "message": "Connexion à $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Laissez $1 se connecter automatiquement à $2 sans votre approbation.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 veut utiliser $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Site web" }, + "snapHomeMenu": { + "message": "Menu d’accueil du Snap" + }, "snapInstallRequest": { "message": "L’installation de $1 lui accorde les autorisations suivantes.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Source" }, + "speed": { + "message": "Vitesse" + }, "speedUp": { "message": "Accélérer" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Limite de dépenses trop élevée" }, + "spender": { + "message": "Dépenseur" + }, "spendingCap": { "message": "Plafond de dépenses" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Les journaux d’état contiennent les adresses publiques de vos comptes et vos transactions envoyées." }, - "states": { - "message": "États" - }, "status": { "message": "État" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Envoyé" }, + "suggestedBySnap": { + "message": "Suggéré par $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Nom suggéré :" + }, "suggestedTokenSymbol": { "message": "Symbole boursier suggéré :" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Récupération des cotations" + "message": "Récupération des cotations…" }, "swapFetchingQuotesErrorDescription": { "message": "Hum… un problème est survenu. Réessayez et si les erreurs persistent, contactez le service client." @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Vous êtes passé à" + "message": "Vous êtes en train d’utiliser" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Le changement de réseau annulera toutes les confirmations en attente" @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "cette collection" }, + "threeMonthsAbbreviation": { + "message": "3 mois", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Temps" }, @@ -5498,45 +5836,6 @@ "message": "Vers : $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Vous êtes vulnérable aux attaques par hameçonnage. Protégez-vous en désactivant eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Si vous activez ce paramètre, vous risquez de recevoir des demandes de signature illisibles. En signant un message que vous ne comprenez pas, vous pourriez accepter de céder vos fonds et vos NFTs." - }, - "toggleEthSignField": { - "message": "Requêtes Eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " il se peut qu’il essaie de vous arnaquer" - }, - "toggleEthSignModalBannerText": { - "message": "Si quelqu’un vous a demandé d’activer ce paramètre," - }, - "toggleEthSignModalCheckBox": { - "message": "Je sais que je peux perdre tous mes fonds et mes NFT si j’active les demandes eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "L’autorisation des demandes eth_sign peut vous rendre vulnérable aux attaques par hameçonnage. Assurez-vous de l’adresse URL et faites preuve de vigilance avant de signer des messages qui contiennent du code." - }, - "toggleEthSignModalFormError": { - "message": "Le texte est incorrect" - }, - "toggleEthSignModalFormLabel": { - "message": "Tapez « Je ne signe que ce que je comprends » pour continuer" - }, - "toggleEthSignModalFormValidation": { - "message": "Je ne signe que ce que je comprends" - }, - "toggleEthSignModalTitle": { - "message": "Utilisez cette fonctionnalité à vos risques et périls" - }, - "toggleEthSignOff": { - "message": "Désactiver (recommandé)" - }, - "toggleEthSignOn": { - "message": "Activer (recommandé)" - }, "toggleRequestQueueDescription": { "message": "Cette fonction vous permet de sélectionner un réseau pour chaque site au lieu d’un seul réseau pour tous les sites. Vous n’aurez donc pas à changer manuellement de réseau, ce qui pourrait nuire à l’expérience utilisateur sur certains sites." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Adresse du contrat de jeton" }, + "tokenDecimal": { + "message": "Nombre de décimales du jeton" + }, "tokenDecimalFetchFailed": { "message": "La décimale du jeton est requise. Trouvez-la sur : $1" }, @@ -5580,13 +5882,16 @@ "message": "ID de token" }, "tokenList": { - "message": "Listes de tokens :" + "message": "Listes de jetons" }, "tokenScamSecurityRisk": { "message": "les arnaques et les risques de piratage informatique" }, "tokenShowUp": { - "message": "Vos tokens n’apparaîtront peut-être pas automatiquement dans votre portefeuille." + "message": "Il se peut que vos jetons n’apparaissent pas automatiquement dans votre portefeuille. " + }, + "tokenStandard": { + "message": "Jeton standard" }, "tokenSymbol": { "message": "Symbole du jeton" @@ -5598,6 +5903,9 @@ "message": "$1 nouveaux jetons trouvés", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Jetons dans la collection" + }, "tooltipApproveButton": { "message": "Je comprends" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Total" }, + "totalVolume": { + "message": "Volume total" + }, "transaction": { "message": "transaction" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "Transaction créée avec une valeur de $1 sur $2." }, + "transactionDataFunction": { + "message": "Fonction" + }, "transactionDetailDappGasMoreInfo": { "message": "Site suggéré" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Transfert depuis" }, + "trillionAbbreviation": { + "message": "B", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Nous avons des difficultés à connecter votre Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Selon nos informations, cette URL ne correspond pas à celle d’un fournisseur connu pour cet ID de chaîne." + }, "unapproved": { "message": "Non autorisé" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Mise à jour" }, + "updateOrEditNetworkInformations": { + "message": "Mettez à jour vos informations ou" + }, "updateRequest": { "message": "Demande de mise à jour" }, "updatedWithDate": { "message": "Mis à jour $1" }, + "uploadDropFile": { + "message": "Déposez votre fichier ici" + }, + "uploadFile": { + "message": "Télécharger le fichier" + }, "urlErrorMsg": { "message": "Les URLs requièrent un préfixe HTTP/HTTPS approprié." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Qu’est-ce que c’est ?" }, + "wrongChainId": { + "message": "Cet ID de chaîne ne correspond pas au nom du réseau." + }, + "wrongNetworkName": { + "message": "Selon nos informations, il se peut que le nom du réseau ne corresponde pas exactement à l’ID de chaîne." + }, "xOfYPending": { "message": "$1 sur $2 en attente", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Vos comptes" }, - "yourFundsMayBeAtRisk": { - "message": "Vous risquez de perdre une partie ou la totalité du capital investi" + "yourActivity": { + "message": "Votre activité" + }, + "yourBalance": { + "message": "Votre solde" }, "yourNFTmayBeAtRisk": { "message": "Il peut y avoir des risques associés à votre NFT" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 480a412fb108..2371df612b77 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -42,7 +42,7 @@ "message": "अपने QR hardware wallet को कनेक्ट करें" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (जल्द आ रहा है)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "साइन-इन रिक्वेस्ट का एड्रेस उस अकाउंट के एड्रेस से मेल नहीं खाता जिसका इस्तेमाल आप साइन इन करने के लिए कर रहे हैं।" @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "आपको एक अकाउंट चुनना होगा!" }, + "accountTypeNotSupported": { + "message": "खाता प्रकार सपोर्ट नहीं करता" + }, "accounts": { "message": "अकाउंट्स" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "एक नया Ethereum अकाउंट जोड़ें" }, + "addNewBitcoinAccount": { + "message": "एक नया Bitcoin अकाउंट जोड़ें (बीटा)" + }, + "addNewBitcoinTestnetAccount": { + "message": "एक नया Bitcoin अकाउंट जोड़ें (टैस्टनेट (testnet))" + }, "addNewToken": { "message": "नया टोकन जोड़ें" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "NFTs जोड़ें" }, + "addRpcUrl": { + "message": "RPC URL जोड़ें" + }, "addSnapAccountToggle": { "message": "\"अकाउंट Snap जोड़ें (बीटा)\" को चालू करें" }, @@ -305,12 +317,21 @@ "message": "टोकन नहीं मिल रहा है? आप किसी भी टोकन का एड्रेस पेस्ट करके उसे मैन्युअल रूप से भी जोड़ सकते हैं। टोकन कॉन्ट्रैक्ट एड्रेस $1 पर मिल सकते हैं।", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "URL जोड़ें" + }, "addingCustomNetwork": { "message": "नेटवर्क जोड़ रहे हैं" }, "addingTokens": { "message": "टोकन जोड़ना" }, + "additionalNetworks": { + "message": "अतिरिक्त नेटवर्क" + }, + "additionalRpcUrl": { + "message": "अतिरिक्त RPC URL" + }, "address": { "message": "एड्रेस" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "एडवांस्ड कॉन्फ़िगरेशन" }, + "advancedDetailsDataDesc": { + "message": "डेटा" + }, + "advancedDetailsHexDesc": { + "message": "Hex" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "यह किसी अकाउंट का ट्रांसेक्शन नंबर है। पहले ट्रांसेक्शन के लिए nonce 0 है और यह क्रमिक क्रम में बढ़ता है।" + }, "advancedGasFeeDefaultOptIn": { "message": "$1 नेटवर्क के लिए इन मूल्यों को मेरे डिफॉल्ट के रूप में सहेजें।", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "एलर्ट" }, + "alertActionBuy": { + "message": "ETH खरीदें" + }, + "alertActionUpdateGas": { + "message": "गैस लिमिट को अपग्रेड करें" + }, + "alertActionUpdateGasFee": { + "message": "शुल्क अपडेट करें" + }, + "alertActionUpdateGasFeeLevel": { + "message": "गैस के विकल्प को अपडेट करें" + }, "alertBannerMultipleAlertsDescription": { "message": "यदि आप इस रिक्वेस्ट को एप्रूव करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "इसे \"सेटिंग > अलर्ट\" में बदला जा सकता है" }, + "alertMessageGasEstimateFailed": { + "message": "हम सटीक शुल्क प्रदान करने में असमर्थ हैं और यह अनुमान अधिक हो सकता है। हम आपको एक कस्टम गैस लिमिट दर्ज करने का सुझाव देते हैं, लेकिन जोखिम है कि ट्रांसेक्शन अभी भी विफल हो जाएगा।" + }, + "alertMessageGasFeeLow": { + "message": "कम शुल्क चुनते समय, धीमे ट्रांसेक्शन और लंबे समय तक प्रतीक्षा करने की अपेक्षा करें। तेज़ ट्रांसेक्शन के लिए, मार्केट या एग्रेसिव शुल्क के विकल्प चुनें।" + }, + "alertMessageGasTooLow": { + "message": "इस ट्रांसेक्शन को जारी रखने के लिए, आपको गैस लिमिट को 21000 या अधिक तक बढ़ाना होगा।" + }, + "alertMessageInsufficientBalance": { + "message": "ट्रांसेक्शन फीस का भुगतान करने के लिए आपके अकाउंट में पर्याप्त ETH नहीं है।" + }, + "alertMessageNetworkBusy": { + "message": "गैस प्राइसें अधिक हैं और अनुमान कम सटीक हैं।" + }, + "alertMessageNoGasPrice": { + "message": "जब तक आप शुल्क को मैन्युअल रूप से अपडेट नहीं करते, हम इस ट्रांसेक्शन को आगे नहीं बढ़ा सकते।" + }, + "alertMessagePendingTransactions": { + "message": "यह ट्रांसेक्शन तब तक नहीं होगा जब तक पिछला ट्रांसेक्शन पूरा न हो जाए। किसी ट्रांसेक्शन को रद्द करने या तेज़ करने का तरीका जानें।" + }, + "alertMessageSignInDomainMismatch": { + "message": "अनुरोध करने वाली साइट वह साइट नहीं है जिस पर आप साइन इन कर रहे हैं। यह आपके लॉगिन क्रेडेंशियल चुराने का प्रयास हो सकता है।" + }, + "alertMessageSignInWrongAccount": { + "message": "यह साइट आपसे गलत अकाउंट का उपयोग करके साइन इन करने के लिए कह रही है।" + }, + "alertMessageSigningOrSubmitting": { + "message": "यह ट्रांसेक्शन तभी पूरा होगा जब आपका पिछला ट्रांसेक्शन पूरा हो जाएगा।" + }, "alertModalAcknowledge": { "message": "मैंने जोखिम को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "सभी एलर्ट की समीक्षा करें" }, + "alertReasonGasEstimateFailed": { + "message": "गलत शुल्क" + }, + "alertReasonGasFeeLow": { + "message": "धीमी गति" + }, + "alertReasonGasTooLow": { + "message": "कम गैस लिमिट" + }, + "alertReasonInsufficientBalance": { + "message": "अपर्याप्त फंड" + }, + "alertReasonNetworkBusy": { + "message": "नेटवर्क व्यस्त है" + }, + "alertReasonNoGasPrice": { + "message": "शुल्क अनुमान अनुपलब्ध है" + }, + "alertReasonPendingTransactions": { + "message": "अभी तक पूरा नहीं हुआ ट्रांसेक्शन" + }, + "alertReasonSignIn": { + "message": "संदिग्ध साइन-इन अनुरोध" + }, + "alertReasonWrongAccount": { + "message": "गलत अकाउंट" + }, "alertSettingsUnconnectedAccount": { "message": "जो कनेक्टेड नहीं है वह अकाउंट चुनकर कोई वेबसाइट ब्राउज़ करना" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "सभी अनुमतियां" }, + "allTimeHigh": { + "message": "अब तक के सबसे ऊँचे स्तर पर" + }, + "allTimeLow": { + "message": "अब तक के सबसे निचले स्तर पर" + }, "allYourNFTsOf": { "message": "आपके सभी NFTs $1 से शुरू", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 और $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "घोषणाएं" - }, "appDescription": { "message": "आपके ब्राउज़र में एक Ethereum वॉलेट", "description": "The description of the application" @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "स्वैप को मुफ्त में कैंसिल करने की कोशिश करें" }, + "attributes": { + "message": "विशेषताएं" + }, "attributions": { "message": "एट्रीब्यूशन्स" }, + "auroraRpcDeprecationMessage": { + "message": "Infura RPC URL अब Aurora को सपोर्ट नहीं कर रहा है।" + }, "authorizedPermissions": { "message": "आपने निम्नलिखित अनुमतियों को अधिकृत किया है" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "बेसिक फंक्शनलिटी बंद है" }, + "basicConfigurationDescription": { + "message": "MetaMask इंटरनेट सेवाओं के माध्यम से टोकन विवरण और गैस सेटिंग्स जैसी बुनियादी सुविधाएं प्रदान करता है। जब आप इंटरनेट सेवाओं का उपयोग करते हैं, तो आपका आईपी ​​एड्रेस साझा किया जाता है, इस मामले में MetaMask के साथ। यह बिल्कुल वैसा ही है जैसे आप किसी वेबसाइट पर जाते हैं। MetaMask इस डेटा का अस्थायी रूप से उपयोग करता है और कभी भी आपका डेटा नहीं बेचता है। आप वीपीएन का उपयोग कर सकते हैं या इन सेवाओं को बंद कर सकते हैं, लेकिन यह आपके MetaMask अनुभव को प्रभावित कर सकता है। अधिक जानने के लिए हमारी $1 पढ़ें।", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "बेसिक फंक्शनलिटी" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask बीटा आपसे आपका गुप्त रिकवरी वाक्यांश कभी नहीं मांगेगा।" }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Bitcoin गतिविधि सपोर्ट नहीं करती है" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "इस सुविधा को चालू करने से आपको अपने मौजूदा सीक्रेट रिकवरी फ्रेज़ से प्राप्त MetaMask एक्सटेंशन में एक Bitcoin अकाउंट जोड़ने का विकल्प मिलेगा। यह एक एक्सपेरिमेंटल बीटा सुविधा है, इसलिए आपको इसका उपयोग अपने जोखिम पर करना होगा। इस नए Bitcoin अनुभव पर हमें प्रतिक्रिया देने के लिए, कृपया यह $1 भरें।", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "\"एक नया Bitcoin अकाउंट जोड़ें (बीटा)\" को चालू करें" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "इस सुविधा को चालू करने से आपको परीक्षण नेटवर्क के लिए एक Bitcoin अकाउंट जोड़ने का विकल्प मिलेगा।" + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "\"एक नया Bitcoin अकाउंट जोड़ें (टैस्टनेट (testnet))\" को चालू करें" + }, "blockExplorerAccountAction": { "message": "अकाउंट", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "हम इस अनुरोध के साथ आगे बढ़ने का सुझाव नहीं देते।" + }, "blockaidDescriptionApproveFarming": { "message": "यदि आप इस रिक्वेस्ट को स्वीकार करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" }, @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "यदि आप इस रिक्वेस्ट को स्वीकार करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" }, + "blockaidDescriptionWarning": { + "message": "यह एक भ्रामक अनुरोध हो सकता है। केवल तभी जारी रखें जब आपको इसमें शामिल प्रत्येक एड्रेस पर भरोसा हो।" + }, "blockaidMessage": { "message": "गोपनीयता को सुरक्षित रखना - कोई भी डेटा थर्ड पार्टी के साथ साझा नहीं किया जाता है। Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Base और Sepolia पर उपलब्ध है।" }, @@ -695,6 +839,9 @@ "blockies": { "message": "ब्लॉकीज़" }, + "boughtFor": { + "message": "के लिए खरीदा गया" + }, "bridge": { "message": "ब्रिज" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "अपने hardware wallet से कनेक्ट करने के लिए आपको Google Chrome पर MetaMask का इस्तेमाल करने की आवश्यकता है।" }, + "circulatingSupply": { + "message": "सप्लाई सर्कुलेट किया जा रहा है" + }, "clear": { "message": "हटाएं" }, @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "संग्रह का नाम" + }, "comboNoOptions": { "message": "कोई विकल्प नहीं मिला", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "मैंने एलर्ट को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "मैंने एलर्ट को स्वीकार कर लिया है और इसके बावजूद आगे बढ़ना चाहता/चाहती हूं" + }, "confirmAlertModalDetails": { "message": "यदि आप साइन इन करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है। कृपया आगे बढ़ने से पहले एलर्ट की समीक्षा करें।" }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "$1 से कनेक्शन को कन्फर्म करें" }, + "confirmDeletion": { + "message": "हटाना कन्फर्म करें" + }, + "confirmFieldPaymaster": { + "message": "के द्वारा शुल्क का भुगतान किया गया" + }, + "confirmFieldTooltipPaymaster": { + "message": "इस ट्रांसेक्शन के लिए शुल्क का भुगतान पेमास्टर स्मार्ट कॉन्ट्रैक्ट द्वारा किया जाएगा।" + }, "confirmPassword": { "message": "पासवर्ड कन्फर्म करें" }, "confirmRecoveryPhrase": { "message": "सीक्रेट रिकवरी फ्रेज कन्फर्म करें" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "यदि आप कंटेंट को पूरी तरह से समझते हैं और अनुरोध करने वाली साइट पर भरोसा करते हैं तो ही इस ट्रांसेक्शन को कन्फर्म करें" + "confirmRpcUrlDeletionMessage": { + "message": "क्या आप वाकई RPC URL को हटाना चाहते हैं? आपकी जानकारी इस नेटवर्क के लिए सेव नहीं की जाएगी।" + }, + "confirmTitleDescPermitSignature": { + "message": "यह साइट आपके टोकन खर्च करने की अनुमति चाहती है।" + }, + "confirmTitleDescSIWESignature": { + "message": "एक साइट चाहती है कि आप यह साबित करने के लिए साइन इन करें कि यह आपका अकाउंट है।" }, "confirmTitleDescSignature": { "message": "इस संदेश को केवल तभी कन्फर्म करें जब आप कंटेंट को एप्रूव करते हैं और अनुरोध करने वाली साइट पर भरोसा करते हैं।" }, + "confirmTitlePermitSignature": { + "message": "खर्च करने की सीमा का अनुरोध" + }, + "confirmTitleSIWESignature": { + "message": "साइन-इन अनुरोध" + }, "confirmTitleSignature": { "message": "सिग्नेचर अनुरोध" }, @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "अकाउंट बनाएं" }, + "creatorAddress": { + "message": "निर्माता का एड्रेस" + }, "crossChainSwapsLink": { "message": "MetaMask पोर्टफोलियो के साथ पूरे नेटवर्क में कहीं भी स्वैप करें" }, @@ -1260,9 +1440,27 @@ "data": { "message": "डेटा" }, + "dataCollectionForMarketing": { + "message": "विपणन के लिए डेटा संग्रह" + }, + "dataCollectionForMarketingDescription": { + "message": "आप हमारे मार्केटिंग कम्यूनिकेशन्स के साथ कैसे इंटरैक्ट करते हैं, यह जानने के लिए हम MetaMetrics का उपयोग करेंगे। हम प्रासंगिक समाचार (जैसे प्रॉडक्ट फीचर्स और अन्य सामग्री) साझा कर सकते हैं।" + }, + "dataCollectionWarningPopoverButton": { + "message": "ठीक है" + }, + "dataCollectionWarningPopoverDescription": { + "message": "आपने हमारे मार्केटिंग उद्देश्यों के लिए डेटा संग्रहण बंद कर दिया है।  यह केवल इस डिवाइस पर लागू होता है। यदि आप अन्य डिवाइसों पर MetaMask का उपयोग करते हैं, तो वहां भी ऑप्ट आउट करना सुनिश्चित करें।" + }, "dataHex": { "message": "हेक्स" }, + "dataUnavailable": { + "message": "डेटा अनुपलब्ध है" + }, + "dateCreated": { + "message": "बनाने की तारीख" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "रिक्वेस्ट डिक्रिप्ट करें" }, + "defaultRpcUrl": { + "message": "डिफॉल्ट RPC URL" + }, "delete": { "message": "मिटाएं" }, @@ -1308,6 +1509,9 @@ "message": "$1 नेटवर्क को हटाएं?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "RPC URL को हटाएं" + }, "deposit": { "message": "डिपॉज़िट करें" }, @@ -1333,18 +1537,6 @@ "details": { "message": "विस्तृत जानकारी" }, - "developerOptions": { - "message": "डेवलपर विकल्प" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "सभी घोषणाओं के लिए isShown boolean को गलत पर रीसेट करता है। घोषणाएं व्हाट्स न्यू पॉपअप मोडल में दिखाई जाने वाली सूचनाएं हैं।" - }, - "developerOptionsResetStatesOnboarding": { - "message": "ऑनबोर्डिंग से संबंधित विभिन्न स्टेट को रीसेट करता है और \"सिक्योर योर वॉलेट\" ऑनबोर्डिंग पेज पर रीडायरेक्ट करता है।" - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "हर टाइमस्टैम्प के परिणाम को लगातार session.storage में सेव किया जा रहा है" - }, "disabledGasOptionToolTipMessage": { "message": "\"$1\" डिसेबल किया गया है क्योंकि यह ओरिजिनल गैस फ़ीस से कम-से-कम 10% वृद्धि को पूरा नहीं करता है।", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "अनजाना प्रोसेसिंग टाइम" }, + "editNetworkLink": { + "message": "मूल नेटवर्क को संपादित करें" + }, "editNonceField": { "message": "Nonce बदलें" }, @@ -1549,12 +1744,12 @@ "message": "$1 इनेबल करें", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "टोकन ऑटोडिटेक्शन चालू करें" - }, "enabled": { "message": "इनेबल किया गया" }, + "enabledNetworks": { + "message": "चालू किए गए नेटवर्क" + }, "encryptionPublicKeyNotice": { "message": "$1 आपकी पब्लिक एन्क्रिप्शन की (key) चाहता है। सहमति देने पर, यह साइट आपके लिए एन्क्रिप्ट किए गए मैसेज लिख पाएगी।", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "अनुमानित फ़ीस" }, + "estimatedFeeTooltip": { + "message": "नेटवर्क पर ट्रांसेक्शन को प्रोसेस करने के लिए भुगतान की गई राशि।" + }, "ethGasPriceFetchWarning": { "message": "बैकअप गैस प्राइस दिया गया है क्योंकि मेन गैस एस्टीमेशन सर्विस अभी उपलब्ध नहीं है।" }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Etherscan पर देखें" }, + "existingChainId": { + "message": "आपके द्वारा दर्ज की गई जानकारी मौजूदा चेन ID से जुड़ी है।" + }, + "existingRpcUrl": { + "message": "यह URL किसी अन्य चेन ID से जुड़ा है।" + }, "expandView": { "message": "व्यू को बड़ा करें" }, @@ -1701,7 +1905,7 @@ "message": "प्रस्तावित उपनाम" }, "externalNameSourcesSettingDescription": { - "message": "हम Etherscan, Infura और लेंस प्रोटोकॉल जैसे थर्ड पार्टी स्रोतों से उन एड्रेसों के लिए प्रस्तावित उपनाम लाएंगे जिनके साथ आप इंटरैक्ट करते हैं। ये स्रोत उन एड्रेसों और आपके आईपी एड्रेस को देख सकेंगे। आपके अकाउंट का एड्रेस थर्ड पार्टी के सामने नहीं आएगा।" + "message": "हम Etherscan, Infura और Lens Protocol जैसे थर्ड पार्टी सोर्सों से उन एड्रेसों के लिए प्रस्तावित उपनाम लाएंगे जिनके साथ आप इंटरैक्ट करते हैं। ये सोर्स उन एड्रेसों और आपके आईपी एड्रेस को देख सकेंगे। आपके अकाउंट का एड्रेस थर्ड पार्टी के सामने नहीं आएगा।" }, "failed": { "message": "नहीं हो पाया" @@ -1732,6 +1936,9 @@ "message": "फाइल इम्पोर्ट काम नहीं कर रहा है? यहां क्लिक करें!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "यहां पर सही खोजें:" + }, "flaskWelcomeUninstall": { "message": "आपको इस एक्सटेन्शन को अनइंस्टाल करना चाहिए", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "पासवर्ड भूल गए?" }, + "form": { + "message": "फॉर्म" + }, "from": { "message": "भेजने वाले" }, @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "हाई" }, + "highestCurrentBid": { + "message": "सबसे बड़ी वर्तमान बिड" + }, + "highestFloorPrice": { + "message": "सबसे बड़ी फ्लोर प्राइस" + }, "history": { "message": "इतिहास" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "यह कार्रवाई उन टोकन को संपादित करेगी, जो पहले से ही आपके वॉलेट में लिस्टेड हैं, जिसका इस्तेमाल आपको फ़िश करने के लिए किया जा सकता है। केवल तभी एप्रूव करें, जब आप इस बात को लेकर सुनिश्चित हों कि आप इन टोकन का प्रतिनिधित्व बदलना चाहते हैं। $1 के बारे में और अधिक जानें" }, + "l1Fee": { + "message": "L1 शुल्क" + }, + "l1FeeTooltip": { + "message": "L1 गैस फीस" + }, + "l2Fee": { + "message": "L2 शुल्क" + }, + "l2FeeTooltip": { + "message": "L2 गैस फीस" + }, "lastConnected": { "message": "अंतिम बार जुड़ा" }, - "lastPriceSold": { - "message": "पिछली बार की बिक्री दर" - }, "lastSold": { "message": "पिछली बार बेचा गया" }, @@ -2495,6 +2720,12 @@ "message": "पक्का करें कि इसे कोई भी नहीं देख रहा है", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "मार्केट कैप" + }, + "marketDetails": { + "message": "मार्केट का ब्यौरा" + }, "max": { "message": "अधिकतम" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "अधिकतम फ़ीस" }, + "maxFeeTooltip": { + "message": "ट्रांसेक्शन के भुगतान के लिए अधिकतम शुल्क प्रदान किया गया।" + }, "maxPriorityFee": { "message": "अधिकतम प्रायोरिटी फी" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "तरीका" }, - "methodDataTransactionDescription": { - "message": "यह विशिष्ट कार्रवाई की जाएगी। इस डेटा को जाली बनाया जा सकता है, इसलिए तभी आगे बढ़ें जब आप दूसरी तरफ की साइट पर भरोसा करते हों।" + "methodDataTransactionDesc": { + "message": "डिकोड किए गए इनपुट डेटा के आधार पर फ़ंक्शन पूरा किया गया।" }, "methodNotSupported": { "message": "इस अकाउंट के साथ सपोर्ट नहीं करता है।" @@ -2560,6 +2794,10 @@ "metrics": { "message": "मेट्रिक्स" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "आपका चुना गया अकाउंट ($1) साइन-इन करने की कोशिश करने वाले अकाउंट ($2) से अलग है" }, @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "बेस" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "इस नेटवर्क के साथ जुड़ा नाम।" }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "नेटवर्क के विकल्प" + }, "networkProvider": { "message": "नेटवर्क प्रोवाइडर" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "\"$1\" सफलतापूर्वक जोड़ा गया था!" }, + "newNetworkEdited": { + "message": "\"$1\" सफलतापूर्वक बदला गया!" + }, "newNftAddedMessage": { "message": "NFT सफलतापूर्वक जोड़ा गया!" }, @@ -2869,6 +3116,9 @@ "nftAlreadyAdded": { "message": "NFT पहले ही जोड़ा जा चुका है।" }, + "nftAutoDetectionEnabled": { + "message": "NFT ऑटोडिटेक्शन चालू किया गया" + }, "nftDisclaimer": { "message": "अस्वीकरण: MetaMask मीडिया फ़ाइल को सोर्स url से खींचता है। यह url कभी-कभी मार्केटप्लेस द्वारा बदल दिया जाता है जिस पर NFT को मिंट किया गया था।" }, @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "डोमेन के लिए कोई रिज़ॉल्यूशन प्रदान नहीं किया गया।" }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snap और अधिकांश हार्डवेयर वॉलेट, आपके वर्तमान ब्राउज़र वर्जन के साथ काम नहीं करेंगे।" + }, "noNFTs": { "message": "अभी तक कोई NFT नहीं" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "ट्रांसेक्शन Nonce कस्टमाइज़ करें" }, - "nonceFieldDescription": { - "message": "कन्फर्मेशन स्क्रीन पर Nonce (ट्रांसेक्शन संख्या) को बदलने के लिए इसे चालू करें। यह एक एडवांस्ड सुविधा है, सावधानी से इस्तेमाल करें।" + "nonceFieldDesc": { + "message": "एसेट भेजते समय Nonce (ट्रांसेक्शन संख्या) बदलने के लिए इसे चालू करें। यह एक एडवांस्ड सुविधा है, सावधानी से उपयोग करें।" }, "nonceFieldHeading": { "message": "कस्टम Nonce" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "इस अकाउंट में 1 नया टोकन पाया गया" }, + "numberOfTokens": { + "message": "टोकन की संख्या" + }, "ofTextNofM": { "message": "का" }, @@ -3167,8 +3423,36 @@ "on": { "message": "चालू" }, - "onboarding": { - "message": "ऑनबोर्ड हो रहा है" + "onboardedMetametricsAccept": { + "message": "मैं सहमत हूं" + }, + "onboardedMetametricsDisagree": { + "message": "जी नहीं, धन्यवाद" + }, + "onboardedMetametricsKey1": { + "message": "लेटेस्ट डेवलप्मेंट्स" + }, + "onboardedMetametricsKey2": { + "message": "प्रॉडक्ट फीचर्स" + }, + "onboardedMetametricsKey3": { + "message": "अन्य प्रासंगिक प्रमोशनल सामग्री" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "$1 के अलावा, हम यह समझने के लिए डेटा का उपयोग करना चाहेंगे कि आप मार्केटिंग कम्यूनिकेशन्स के साथ कैसे इंटरैक्ट करते हैं।", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "इससे हमें आपके साथ साझा की जाने वाली चीज़ों को निजीकृत करने में मदद मिलती है, जैसे:" + }, + "onboardedMetametricsParagraph3": { + "message": "याद रखें, हम आपके द्वारा प्रदान किया गया डेटा कभी नहीं बेचते हैं और आप किसी भी समय इससे बाहर निकल सकते हैं।" + }, + "onboardedMetametricsTitle": { + "message": "आपके अनुभव को बेहतर करने में हमारी सहायता करें" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "IPFS (आईपीएफएस) गेटवे थर्ड पार्टियों द्वारा होस्ट किए गए डेटा को एक्सेस करना और देखना संभव बनाता है। आप एक कस्टम IPFS गेटवे जोड़ सकते हैं या डिफॉल्ट का इस्तेमाल जारी रख सकते हैं।" @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "जब हम मेट्रिक्स इकट्ठा करते हैं, तो यह हमेशा... रहेगा" }, - "onboardingMetametricsDisagree": { - "message": "जी नहीं, धन्यवाद" - }, "onboardingMetametricsInfuraTerms": { "message": "यदि हम इस डेटा का उपयोग अन्य उद्देश्यों के लिए करने का निर्णय लेते हैं तो हम आपको बताएंगे। अधिक जानकारी के लिए आप हमारे $1 की समीक्षा कर सकते हैं। याद रखें, आप किसी भी समय सेटिंग्स में जाकर ऑप्ट आउट कर सकते हैं।", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "MetaMask को बेहतर बनाने में हमारी मदद करें" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "हम इस डेटा का उपयोग यह जानने के लिए करेंगे कि आप हमारे मार्केटिंग कम्यूनिकेशन्स के साथ कैसे इंटरैक्ट करते हैं। हम प्रासंगिक समाचार (जैसे प्रॉडक्ट फीचर्स) साझा कर सकते हैं।" + }, "onboardingPinExtensionBillboardAccess": { "message": "पूरी एक्सेस" }, @@ -3286,6 +3570,22 @@ "message": "फिशिंग डिटेक्शन अलर्ट $1 के साथ संचार पर निर्भर करते हैं। jsDeliver की पहुंच आपके IP एड्रेस तक होगी। $2 देखें।", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1D", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1M", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1W", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1Y", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "ओपनसी" }, - "openSeaToBlockaidBtnLabel": { - "message": "Snaps को एक्सप्लोर करें" - }, - "openSeaToBlockaidDescription": { - "message": "सुरक्षा एलर्ट अब इस नेटवर्क पर उपलब्ध नहीं हैं। Snap इंस्टॉल करने से आपकी सुरक्षा में सुधार हो सकता है।" - }, - "openSeaToBlockaidTitle": { - "message": "सतर्क रहें!" - }, "operationFailed": { "message": "प्रचालन नहीं हो पाया" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "कनेक्टेड साइटें अब अनुमतियां हैं" }, + "permitSimulationDetailInfo": { + "message": "आप खर्च करने वाले को अपने अकाउंट से इतने सारे टोकन खर्च करने की अनुमति दे रहे हैं।" + }, "personalAddressDetected": { "message": "व्यक्तिगत एड्रेस का एड्रेस चला। टोकन कॉन्ट्रैक्ट एड्रेस डालें।" }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "पॉपुलर कस्टम नेटवर्क" }, + "popularNetworkAddToolTip": { + "message": "इनमें से कुछ नेटवर्क थर्ड पार्टीज़ पर निर्भर हैं। कनेक्शन कम विश्वसनीय हो सकते हैं या गतिविधि को ट्रैक करने के लिए थर्ड पार्टीज़ को चालू कर सकते हैं। $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "पोर्टफोलियो" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "पिछला" }, + "price": { + "message": "प्राइस" + }, + "priceUnavailable": { + "message": "प्राइस अनुपलब्ध है" + }, "primaryCurrencySetting": { "message": "प्राथमिक मुद्रा" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "उद्धरण का दर" }, + "rank": { + "message": "रैंक" + }, "reAddAccounts": { "message": "किसी अन्य अकाउंट को फिर से जोड़ें" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "प्राप्त करें" }, - "receiveTokensCamelCase": { - "message": "टोकन प्राप्त करें" - }, "recipientAddressPlaceholder": { "message": "सार्वजनिक एड्रेस (0x) या ENS नाम डालें" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "रीसेट करें" }, - "resetStates": { - "message": "स्टेट रीसेट करें" - }, "resetWallet": { "message": "वॉलेट रीसेट करें" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "अकाउंट्स खोजें" }, + "searchNfts": { + "message": "NFT ढूंढें" + }, "searchTokens": { "message": "टोकनों को ढूंढें" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "मेरा वॉलेट सुरक्षित करें (अनुशंसित)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "लिखें और कई गुप्त स्थानों में स्टोर करें।" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "पासवर्ड मैनेजर में सेव करें" + "message": "लिखें और कई गुप्त स्थानों में स्टोर करें।" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "सेफ डिपॉजिट बॉक्स में स्टोर करें।" }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,7 +4571,7 @@ "message": "टोकन चुनें" }, "selectNFTPrivacyPreference": { - "message": "सेटिंग्स में NFT डिटेक्शन चालू करें" + "message": "NFT ऑटोडिटेक्शन चालू करें" }, "selectPathHelp": { "message": "यदि आपको अपनी पसंद के हिसाब से अकाउंट दिखाई नहीं देते हैं, तो HD पाथ या मौजूदा चुना गया नेटवर्क बदलने की कोशिश करें।" @@ -4284,9 +4585,6 @@ "send": { "message": "भेजें" }, - "sendAToken": { - "message": "एक टोकन भेजें" - }, "sendBugReport": { "message": "हमें एक बग रिपोर्ट भेजें।" }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Sepolia टेस्ट नेटवर्क" }, - "serviceWorkerKeepAlive": { - "message": "सेवाकर्मी जीवित रहें" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask उत्पाद की उपयोगिता और सुरक्षा को बढ़ाने के लिए इन विश्वसनीय तीसरे-पक्ष की सेवाओं का इस्तेमाल करता है।" }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "यह हरेक नेटवर्क के लिए अलग-अलग थर्ड-पार्टी API पर निर्भर करता है, जो आपके Ethereum एड्रेस और आपके आईपी ​​एड्रेस को एक्स्पोज़ करता है।" }, + "showLess": { + "message": "कम दिखाएं" + }, "showMore": { "message": "अधिक दिखाएं" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "यदि आप कंटेंट को पूरी तरह से समझते हैं और रिक्वेस्ट करने वाली साइट पर भरोसा करते हैं तो ही इस मैसेज पर हस्ताक्षर करें।" }, - "signatureRequestWarning": { - "message": "इस मैसेज पर हस्ताक्षर करना खतरनाक हो सकता है। हो सकता है कि आप इस मैसेज के दूसरे सिरे पर पार्टी को अपने अकाउंट और संपत्ति का पूर्ण नियंत्रण दे रहे हों। इसका मतलब है कि वे किसी भी समय आपका अकाउंट खाली कर सकते हैं। सावधानी के साथ आगे बढ़ें। $1." - }, "signed": { "message": "हस्ताक्षर किया गया" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "साइन हो रहा है" }, + "signingInWith": { + "message": "के साथ साइन इन करना" + }, "simulationDetailsFailed": { "message": "आपका एस्टीमेशन लोड करने में गड़बड़ी हुई।" }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "बैलेंस अमाउंट में बदलावों का अनुमान लगाएं" }, + "siweIssued": { + "message": "जारी किया गया" + }, + "siweNetwork": { + "message": "नेटवर्क" + }, + "siweRequestId": { + "message": "रिक्वेस्ट ID" + }, + "siweResources": { + "message": "संसाधन" + }, + "siweSignatureSimulationDetailInfo": { + "message": "आप किसी साइट पर साइन इन कर रहे हैं और आपके अकाउंट में कोई अनुमानित परिवर्तन नहीं हैं।" + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "छोड़ें" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "थर्ड-पार्टी Snaps द्वारा कंट्रोल किए गए अकाउंट।" }, + "snapConnectTo": { + "message": "$1 से कनेक्ट करें", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "आपके एप्रूवल के बिना $1 को स्वचालित रूप से $2 से कनेक्ट होने दें।", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 $2 का उपयोग करना चाहता है", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "वेबसाइट" }, + "snapHomeMenu": { + "message": "Snap होम मेन्यू" + }, "snapInstallRequest": { "message": "$1 इंस्टॉल करने से इसे निम्नलिखित अनुमतियां मिलती हैं।", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "स्त्रोत" }, + "speed": { + "message": "गति" + }, "speedUp": { "message": "जल्दी करें" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "खर्च की लिमिट बहुत अधिक है" }, + "spender": { + "message": "खर्च करने वाला" + }, "spendingCap": { "message": "खर्च करने की लिमिट" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "स्टेट लॉग में आपके सार्वजनिक अकाउंट के एड्रेस और भेजे गए ट्रांसेक्शन शामिल होते हैं।" }, - "states": { - "message": "स्टेट" - }, "status": { "message": "स्टेटस" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "सबमिट किया गया" }, + "suggestedBySnap": { + "message": "$1 के द्वारा सुझाव दिया गया", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "सुझाया गया नाम:" + }, "suggestedTokenSymbol": { "message": "सुझाया गया टिकर सिंबल:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "उद्धरण प्राप्त कर रहे हैं" + "message": "कोटेशन प्राप्त कर रहे हैं.." }, "swapFetchingQuotesErrorDescription": { "message": "हम्म्म... कुछ गलत हो गया। फिर से कोशिश करें या यदि गड़बड़ीयां बनी रहती हैं, तो ग्राहक सहायता से कॉन्टेक्ट करें।" @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "यह संग्रह" }, + "threeMonthsAbbreviation": { + "message": "3M", + "description": "Shortened form of '3 months'" + }, "time": { "message": "समय" }, @@ -5495,45 +5836,6 @@ "message": "प्रति: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "आपको फ़िशिंग हमलों का जोखिम है। Eth_sign को बंद करके अपनी सुरक्षा करें।" - }, - "toggleEthSignDescriptionField": { - "message": "यदि आप इस सेटिंग को एनेबल करते हैं, तो आपको हस्ताक्षर रिक्वेस्ट प्राप्त हो सकते हैं जो पढ़ने योग्य नहीं हैं। एक ऐसे मैसेज पर हस्ताक्षर करके जिसे आप समझ नहीं पा रहे हैं, आप अपने फंड और NFT दे देने के लिए सहमत हो सकते हैं।" - }, - "toggleEthSignField": { - "message": "Eth_sign अनुरोध" - }, - "toggleEthSignModalBannerBoldText": { - "message": " आपके साथ धोखा हो सकता है" - }, - "toggleEthSignModalBannerText": { - "message": "यदि आपसे यह सेटिंग चालू करने के लिए कहा गया है," - }, - "toggleEthSignModalCheckBox": { - "message": "मैं समझता हूं कि अगर मैं eth_sign रिक्वेस्ट्स को इनेबल करता हूं, तो मैं अपने सभी फंड और NFT's खो सकता हूं। " - }, - "toggleEthSignModalDescription": { - "message": "eth_sign रिक्वेस्ट्स को अनुमति देने से आप फ़िशिंग हमलों के प्रति असुरक्षित हो सकते हैं। URL की हमेशा समीक्षा करें और कोड वाले मैसेज पर हस्ताक्षर करते समय सावधान रहें।" - }, - "toggleEthSignModalFormError": { - "message": "टेक्स्ट गलत है" - }, - "toggleEthSignModalFormLabel": { - "message": "जारी रखने के लिए \"मैं केवल वही हस्ताक्षर करता हूं जो मुझे समझ में आता है\" डालें" - }, - "toggleEthSignModalFormValidation": { - "message": "मैं केवल वही हस्ताक्षर करता हूं जो मुझे समझ में आता है" - }, - "toggleEthSignModalTitle": { - "message": "अपने जोखिम पर इस्तेमाल करें" - }, - "toggleEthSignOff": { - "message": "बंद (अनुशंसित)" - }, - "toggleEthSignOn": { - "message": "चालू (अनुशंसित नहीं)" - }, "toggleRequestQueueDescription": { "message": "ऐसा करके, आप सभी साइटों के लिए कोई सिंगल नेटवर्क चुनने के बजाय हरेक साइट के लिए एक नेटवर्क चुन सकते हैं। यह फीचर आपको मैन्युअल तरीके से नेटवर्क स्विच करने से रोकता है, इस वजह से कुछ साइटों पर आपका यूज़र अनुभव ख़राब हो सकता है।" }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "टोकन कॉन्ट्रैक्ट एड्रेस" }, + "tokenDecimal": { + "message": "टोकन डेसिमल" + }, "tokenDecimalFetchFailed": { "message": "टोकन डेसीमल की आवश्यकता है। इसे: $1 पर पाएं" }, @@ -5577,7 +5882,7 @@ "message": "टोकन आइडी" }, "tokenList": { - "message": "टोकन लिस्ट्स:" + "message": "टोकन की सूचियां" }, "tokenScamSecurityRisk": { "message": "टोकन घोटाले और सुरक्षा जोखिम" @@ -5585,6 +5890,9 @@ "tokenShowUp": { "message": "हो सकता है आपके वॉलेट में आपके टोकन ऑटोमेटिकली दिखाई नहीं दें। " }, + "tokenStandard": { + "message": "टोकन स्टैंडर्ड" + }, "tokenSymbol": { "message": "टोकन का प्रतीक" }, @@ -5595,6 +5903,9 @@ "message": "$1 नए टोकन मिले", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "संग्रह में टोकन" + }, "tooltipApproveButton": { "message": "मैं समझता हूं" }, @@ -5610,6 +5921,9 @@ "total": { "message": "कुलयोग" }, + "totalVolume": { + "message": "टोटल वॉल्यूम" + }, "transaction": { "message": "ट्रांसेक्शन" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "$2 पर $1 के मूल्य के साथ ट्रांसेक्शन बनाया गया।" }, + "transactionDataFunction": { + "message": "फंक्शन" + }, "transactionDetailDappGasMoreInfo": { "message": "साइट का सुझाव दिया गया" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "इससे ट्रांसफ़र करें" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "हमें आपके Ledger को जोड़ने में समस्या आ रही है। $1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "हमारे रिकॉर्ड के अनुसार, यह URL इस चेन ID के लिए ज्ञात प्रदाता से मेल नहीं खाता है।" + }, "unapproved": { "message": "रिजेक्ट" }, @@ -5844,12 +6168,21 @@ "update": { "message": "अपडेट करें" }, + "updateOrEditNetworkInformations": { + "message": "अपनी जानकारी अपडेट करें या" + }, "updateRequest": { "message": "अपडेट का अनुरोध" }, "updatedWithDate": { "message": "अपडेट किया गया $1" }, + "uploadDropFile": { + "message": "अपनी फ़ाइल यहां छोड़ें" + }, + "uploadFile": { + "message": "फाइल अपलोड करें" + }, "urlErrorMsg": { "message": "URL को उपयुक्त HTTP/HTTPS प्रीफिक्स्ड की आवश्यकता होती है।" }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "यह क्या है?" }, + "wrongChainId": { + "message": "यह चेन ID नेटवर्क के नाम से मेल नहीं खाती।" + }, + "wrongNetworkName": { + "message": "हमारे रिकॉर्ड के अनुसार, नेटवर्क का नाम इस चेन ID से ठीक से मेल नहीं खा सकता है।" + }, "xOfYPending": { "message": "$2 में से $1 लंबित", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "आपके अकाउंट" }, - "yourFundsMayBeAtRisk": { - "message": "आपके फंड खतरे में हो सकते हैं" + "yourActivity": { + "message": "आपकी एक्टिविटी" + }, + "yourBalance": { + "message": "आपका बैलेंस" }, "yourNFTmayBeAtRisk": { "message": "आपका NFT खतरे में हो सकता है" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 43f09805d3f8..0f8ceacc9ea7 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -42,7 +42,7 @@ "message": "Hubungkan dompet perangkat keras QR Anda" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (segera hadir)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Alamat pada permintaan masuk tidak sesuai dengan alamat akun yang Anda gunakan untuk masuk." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Anda harus memilih satu akun!" }, + "accountTypeNotSupported": { + "message": "Jenis akun tidak didukung" + }, "accounts": { "message": "Akun" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Tambahkan akun Ethereum baru" }, + "addNewBitcoinAccount": { + "message": "Tambahkan akun Bitcoin baru (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Tambahkan akun Bitcoin baru (Testnet)" + }, "addNewToken": { "message": "Tambahkan token baru" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Tambahkan NFT" }, + "addRpcUrl": { + "message": "Tambahkan URL RPC" + }, "addSnapAccountToggle": { "message": "Aktifkan \"Tambahkan akun Snap (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "Tidak dapat menemukan token? Tambahkan token secara manual dengan menempelkan alamatnya. Alamat kontrak token dapat ditemukan di $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Tambahkan URL" + }, "addingCustomNetwork": { "message": "Menambahkan Jaringan" }, "addingTokens": { "message": "Menambahkan token" }, + "additionalNetworks": { + "message": "Jaringan tambahan" + }, + "additionalRpcUrl": { + "message": "URL RPC Tambahan" + }, "address": { "message": "Alamat" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Konfigurasi lanjutan" }, + "advancedDetailsDataDesc": { + "message": "Data" + }, + "advancedDetailsHexDesc": { + "message": "Hex" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Ini adalah nomor transaksi suatu akun. Nonce untuk transaksi pertama adalah 0 dan meningkat secara berurutan." + }, "advancedGasFeeDefaultOptIn": { "message": "Simpan nilai ini sebagai default saya untuk jaringan $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Peringatan" }, + "alertActionBuy": { + "message": "Beli ETH" + }, + "alertActionUpdateGas": { + "message": "Perbarui batas gas" + }, + "alertActionUpdateGasFee": { + "message": "Perbarui biaya" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Perbarui opsi gas" + }, "alertBannerMultipleAlertsDescription": { "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Ini dapat diubah dalam \"Pengaturan > Peringatan\"" }, + "alertMessageGasEstimateFailed": { + "message": "Kami tidak dapat memberikan biaya akurat dan estimasi ini mungkin tinggi. Kami menyarankan Anda untuk memasukkan batas gas kustom, tetapi ada risiko transaksi tetap gagal." + }, + "alertMessageGasFeeLow": { + "message": "Saat memilih biaya rendah, perkirakan transaksi lebih lambat dan waktu tunggu lebih lama. Untuk transaksi lebih cepat, pilih opsi biaya Pasar atau Agresif." + }, + "alertMessageGasTooLow": { + "message": "Untuk melanjutkan transaksi ini, Anda perlu meningkatkan batas gas menjadi 21000 atau lebih tinggi." + }, + "alertMessageInsufficientBalance": { + "message": "Anda tidak memiliki cukup ETH di akun untuk membayar biaya transaksi." + }, + "alertMessageNetworkBusy": { + "message": "Harga gas tinggi dan estimasinya kurang akurat." + }, + "alertMessageNoGasPrice": { + "message": "Kami tidak dapat melanjutkan transaksi ini hingga Anda memperbarui biayanya secara manual." + }, + "alertMessagePendingTransactions": { + "message": "Transaksi ini tidak akan dilanjutkan hingga transaksi sebelumnya selesai. Pelajari cara membatalkan atau mempercepat transaksi." + }, + "alertMessageSignInDomainMismatch": { + "message": "Situs yang membuat permintaan bukanlah situs yang Anda masuki. Ini dapat merupakan upaya untuk mencuri kredensial login Anda." + }, + "alertMessageSignInWrongAccount": { + "message": "Situs ini meminta Anda masuk menggunakan akun yang salah." + }, + "alertMessageSigningOrSubmitting": { + "message": "Transaksi ini hanya akan dilanjutkan setelah transaksi Anda sebelumnya selesai." + }, "alertModalAcknowledge": { "message": "Saya telah mengetahui risikonya dan tetap ingin melanjutkan" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Tinjau semua peringatan" }, + "alertReasonGasEstimateFailed": { + "message": "Biaya tidak akurat" + }, + "alertReasonGasFeeLow": { + "message": "Kecepatan lambat" + }, + "alertReasonGasTooLow": { + "message": "Batas gas rendah" + }, + "alertReasonInsufficientBalance": { + "message": "Dana tidak cukup" + }, + "alertReasonNetworkBusy": { + "message": "Jaringan sibuk" + }, + "alertReasonNoGasPrice": { + "message": "Estimasi biaya tidak tersedia" + }, + "alertReasonPendingTransactions": { + "message": "Transaksi menunggu" + }, + "alertReasonSignIn": { + "message": "Permintaan masuk mencurigakan" + }, + "alertReasonWrongAccount": { + "message": "Akun salah" + }, "alertSettingsUnconnectedAccount": { "message": "Memilih untuk menjelajahi situs web dengan akun yang tidak terhubung" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Semua Izin" }, + "allTimeHigh": { + "message": "Tertinggi sepanjang waktu" + }, + "allTimeLow": { + "message": "Terendah sepanjang waktu" + }, "allYourNFTsOf": { "message": "Seluruh NFT Anda dari $1 ", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 dan $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Pengumuman" - }, "appDescription": { "message": "Dompet Ethereum pada Browser Anda", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Opsi aset" }, "attemptSendingAssets": { - "message": "Jika Anda mencoba untuk mengirim aset secara langsung dari satu jaringan ke jaringan lain, aset Anda berpotensi hilang secara permanen. Pastikan untuk menggunakan bridge." + "message": "Aset Anda berpotensi hilang jika mencoba mengirimnya dari jaringan lain. Transfer dana secara aman antar jaringan menggunakan bridge." }, "attemptSendingAssetsWithPortfolio": { "message": "Aset Anda berpotensi hilang jika mencoba mengirimnya melalui jaringan lain. Transfer dana antar jaringan dengan aman menggunakan bridge, seperti $1" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Mencoba membatalkan pertukaran secara gratis" }, + "attributes": { + "message": "Atribut" + }, "attributions": { "message": "Atribusi" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Fungsionalitas dasar tidak aktif" }, + "basicConfigurationDescription": { + "message": "MetaMask menawarkan fitur dasar seperti detail token dan pengaturan gas melalui layanan internet. Alamat IP dibagikan saat menggunakan layanan internet, dalam hal ini kepada MetaMask. Ini sama seperti saat Anda mengunjungi situs web mana pun. MetaMask menggunakan data ini untuk sementara dan tidak akan pernah menjual data Anda. Anda dapat menggunakan VPN atau menonaktifkan layanan ini, akan tetapi hal ini dapat memengaruhi pengalaman dalam menggunakan MetaMask. Untuk selengkapnya, baca $1 kami.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Fungsionalitas dasar" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask Beta tidak akan pernah menanyakan Frasa Pemulihan Rahasia Anda." }, + "billionAbbreviation": { + "message": "M", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Aktivitas Bitcoin tidak didukung" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Mengaktifkan fitur ini akan memberi Anda opsi untuk menambahkan Akun Bitcoin ke Ekstensi MetaMask yang berasal dari Frasa Pemulihan Rahasia yang ada. Ini merupakan fitur Beta eksperimental, jadi Anda harus menggunakannya dengan risiko yang ditanggung sendiri. Untuk memberikan masukan seputar pengalaman Bitcoin baru ini, isi $1 ini.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Aktifkan \"Tambahkan akun Bitcoin baru (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Mengaktifkan fitur ini akan memberi Anda opsi untuk menambahkan Akun Bitcoin bagi jaringan uji." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Aktifkan \"Tambahkan akun Bitcoin baru (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Akun", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Sebaiknya jangan lanjutkan permintaan ini." + }, "blockaidDescriptionApproveFarming": { "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda." }, @@ -669,7 +807,7 @@ "message": "Jika Anda menyetujui permintaan ini, seseorang dapat mencuri aset Anda yang terdaftar di Blur." }, "blockaidDescriptionErrored": { - "message": "Karena terjadi kesalahan, permintaan ini tidak diverifikasi oleh penyedia keamanan. Lanjutkan dengan hati-hati." + "message": "Karena terjadi kesalahan, kami tidak dapat memeriksa peringatan keamanan. Lanjutkan hanya jika Anda memercayai setiap alamat yang terlibat." }, "blockaidDescriptionMaliciousDomain": { "message": "Anda berinteraksi dengan domain berbahaya. Jika Anda menyetujui permintaan ini, aset Anda kemungkinan akan hilang." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan akan mengambil semua aset Anda." }, + "blockaidDescriptionWarning": { + "message": "Ini mungkin merupakan permintaan tipuan. Lanjutkan hanya jika Anda memercayai setiap alamat yang terlibat." + }, "blockaidMessage": { "message": "Menjaga privasi - tidak ada data yang dibagikan kepada pihak ketiga. Tersedia di Arbitrum, Avalanche, BNB chain, Mainnet Ethereum, Linea, Optimism, Polygon, Base, dan Sepolia." }, @@ -690,7 +831,7 @@ "message": "Ini adalah permintaan tipuan" }, "blockaidTitleMayNotBeSafe": { - "message": "Permintaan mungkin tidak aman" + "message": "Berhati-hatilah" }, "blockaidTitleSuspicious": { "message": "Ini adalah permintaan yang mencurigakan" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Dibeli senilai" + }, "bridge": { "message": "Bridge" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Anda perlu menggunakan MetaMask di Google Chrome untuk terhubung ke Dompet Perangkat Keras Anda." }, + "circulatingSupply": { + "message": "Suplai yang beredar" + }, "clear": { "message": "Hapus" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Klik di sini untuk menambahkan token secara manual." + "message": "Anda dapat menambahkan token secara manual setiap saat." }, "close": { "message": "Tutup" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Nama koleksi" + }, "comboNoOptions": { "message": "Opsi tidak ditemukan", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Saya telah mengetahui peringatannya dan tetap ingin melanjutkan" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Saya telah mengetahui peringatannya dan tetap ingin melanjutkan" + }, "confirmAlertModalDetails": { "message": "Jika masuk, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda. Tinjau peringatannya sebelum melanjutkan." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Konfirmasikan koneksi ke $1" }, + "confirmDeletion": { + "message": "Konfirmasikan penghapusan" + }, + "confirmFieldPaymaster": { + "message": "Biaya dibayar oleh" + }, + "confirmFieldTooltipPaymaster": { + "message": "Biaya untuk transaksi ini akan dibayar oleh kontrak cerdas paymaster." + }, "confirmPassword": { "message": "Konfirmasikan kata sandi" }, "confirmRecoveryPhrase": { "message": "Konfirmasikan Frasa Pemulihan Rahasia" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Konfirmasikan transaksi ini hanya jika Anda benar-benar memahami isinya dan memercayai situs yang memintanya." + "confirmRpcUrlDeletionMessage": { + "message": "Yakin ingin menghapus URL RPC? Informasi Anda tidak akan disimpan untuk jaringan ini." + }, + "confirmTitleDescPermitSignature": { + "message": "Situs ini meminta izin untuk menggunakan token Anda." + }, + "confirmTitleDescSIWESignature": { + "message": "Sebuah situs ingin Anda masuk untuk membuktikan Anda pemilik akun ini." }, "confirmTitleDescSignature": { "message": "Konfirmasikan pesan ini hanya jika Anda menyetujui isinya dan memercayai situs yang memintanya." }, + "confirmTitlePermitSignature": { + "message": "Permintaan batas penggunaan" + }, + "confirmTitleSIWESignature": { + "message": "Permintaan masuk" + }, "confirmTitleSignature": { "message": "Permintaan tanda tangan" }, @@ -961,7 +1135,7 @@ "message": "Terhubung dengan" }, "connecting": { - "message": "Menghubungkan..." + "message": "Menghubungkan" }, "connectingTo": { "message": "Menghubungkan ke $1" @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Buat akun" }, + "creatorAddress": { + "message": "Alamat pembuat" + }, "crossChainSwapsLink": { "message": "Bertukar antar jaringan dengan Portfolio MetaMask" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Data" }, + "dataCollectionForMarketing": { + "message": "Pengumpulan data untuk pemasaran" + }, + "dataCollectionForMarketingDescription": { + "message": "Kami akan menggunakan MetaMetrics untuk mempelajari cara Anda berinteraksi dengan komunikasi pemasaran. Kami mungkin akan membagikan berita yang relevan (seperti fitur produk dan materi lainnya)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Oke" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Anda menonaktifkan pengumpulan data untuk tujuan pemasaran kami. Ini hanya berlaku untuk perangkat ini. Jika Anda menggunakan MetaMask di perangkat lain, pastikan untuk keluar terlebih dahulu." + }, "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "data tidak tersedia" + }, + "dateCreated": { + "message": "Tanggal dibuat" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Dekrip permintaan" }, + "defaultRpcUrl": { + "message": "URL RPC Default" + }, "delete": { "message": "Hapus" }, @@ -1311,6 +1509,9 @@ "message": "Hapus jaringan $1?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Hapus URL RPC" + }, "deposit": { "message": "Deposit" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Detail" }, - "developerOptions": { - "message": "Opsi Pengembang" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Reset boolean isShown bernilai salah untuk semua pengumuman. Pengumuman adalah notifikasi yang ditampilkan di modal popup Yang Baru." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Reset berbagai state terkait orientasi dan alihkan ke halaman orientasi \"Amankan Dompet Anda\"." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Menjadikan stempel waktu disimpan ke session.storage secara kontinu" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” dinonaktifkan karena tidak memenuhi kenaikan minimum 10% dari biaya gas asli.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Waktu pemrosesan tak diketahui" }, + "editNetworkLink": { + "message": "edit jaringan asli" + }, "editNonceField": { "message": "Edit nonce" }, @@ -1552,12 +1744,12 @@ "message": "aktifkan $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Aktifkan autodeteksi token" - }, "enabled": { "message": "Diaktifkan" }, + "enabledNetworks": { + "message": "Jaringan yang diaktifkan" + }, "encryptionPublicKeyNotice": { "message": "$1 menginginkan kunci enkripsi publik Anda. Dengan menyetujui, situs ini akan dapat membuat pesan terenkripsi untuk Anda.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Estimasi biaya" }, + "estimatedFeeTooltip": { + "message": "Jumlah yang dibayarkan untuk memproses transaksi di jaringan." + }, "ethGasPriceFetchWarning": { "message": "Biaya gas cadangan diberikan karena layanan estimasi gas utama saat ini tidak tersedia." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Lihat di Etherscan" }, + "existingChainId": { + "message": "Informasi yang Anda masukkan terhubung dengan ID chain yang ada." + }, + "existingRpcUrl": { + "message": "URL ini terhubung dengan ID chain lain." + }, "expandView": { "message": "Perluas tampilan" }, @@ -1704,7 +1905,7 @@ "message": "Nama panggilan yang diusulkan" }, "externalNameSourcesSettingDescription": { - "message": "Kami akan mengambil nama panggilan yang diusulkan untuk alamat yang berinteraksi dengan Anda dari sumber pihak ketiga seperti Etherscan, Infura, dan Lens Protocol. Sumber-sumber ini akan dapat melihat alamat-alamat tersebut dan alamat IP Anda. Alamat akun Anda tidak akan diketahui oleh pihak ketiga." + "message": "Kami akan mengambil nama panggilan yang diusulkan untuk alamat yang berinteraksi dengan Anda dari sumber pihak ketiga seperti Etherscan, Infura, dan Lens Protocol. Sumber-sumber ini dapat melihat alamat-alamat tersebut dan alamat IP Anda. Alamat akun Anda tidak akan diketahui oleh pihak ketiga." }, "failed": { "message": "Gagal" @@ -1735,6 +1936,9 @@ "message": "Impor file tidak bekerja? Klik di sini!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Temukan yang tepat di:" + }, "flaskWelcomeUninstall": { "message": "Anda harus menghapus ekstensi ini", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Lupa kata sandi?" }, + "form": { + "message": "formulir" + }, "from": { "message": "Dari" }, @@ -1904,7 +2111,7 @@ "message": "Jaringan uji Goerli" }, "gotIt": { - "message": "Mengerti!" + "message": "Mengerti" }, "grantedToWithColon": { "message": "Diberikan kepada:" @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "tinggi" }, + "highestCurrentBid": { + "message": "Penawaran tertinggi saat ini" + }, + "highestFloorPrice": { + "message": "Harga dasar tertinggi" + }, "history": { "message": "Riwayat" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Tindakan ini akan mengedit token yang telah terdaftar dalam dompet Anda, yang dapat digunakan untuk menipu Anda. Setujui hanya jika Anda yakin bahwa Anda ingin mengubah apa yang diwakili token ini. Pelajari selengkapnya seputar $1" }, + "l1Fee": { + "message": "Biaya L1" + }, + "l1FeeTooltip": { + "message": "Biaya gas L1" + }, + "l2Fee": { + "message": "Biaya L2" + }, + "l2FeeTooltip": { + "message": "Biaya gas L2" + }, "lastConnected": { "message": "Terakhir terhubung" }, - "lastPriceSold": { - "message": "Harga terakhir terjual" - }, "lastSold": { "message": "Terakhir terjual" }, @@ -2498,6 +2720,12 @@ "message": "Pastikan tidak ada yang melihat layar Anda", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Kap pasar" + }, + "marketDetails": { + "message": "Detail pasar" + }, "max": { "message": "Maks" }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Biaya maks" }, + "maxFeeTooltip": { + "message": "Biaya maksimum yang diberikan untuk membayar transaksi." + }, "maxPriorityFee": { "message": "Biaya prioritas maks" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Metode" }, - "methodDataTransactionDescription": { - "message": "Ini merupakan tindakan khusus yang akan diambil. Data ini dapat dipalsukan, jadi pastikan Anda memercayai situs tersebut." + "methodDataTransactionDesc": { + "message": "Fungsi dijalankan berdasarkan data masukan yang didekodekan." }, "methodNotSupported": { "message": "Akun ini tidak mendukung." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Metrik" }, + "millionAbbreviation": { + "message": "Jt", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Akun yang Anda pilih ($1) berbeda dengan akun yang mencoba ditandatangani ($2)" }, @@ -2675,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Token asli di jaringan ini adalah $1. Ini merupakan token yang digunakan untuk biaya gas.", + "message": "Token asli di jaringan ini adalah $1. Ini merupakan token yang digunakan untuk biaya gas. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Dasar" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Nama yang dikaitkan dengan jaringan ini." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Opsi jaringan" + }, "networkProvider": { "message": "Penyedia jaringan" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "“$1” berhasil ditambahkan!" }, + "newNetworkEdited": { + "message": "“$1” berhasil diedit!" + }, "newNftAddedMessage": { "message": "NFT berhasil ditambahkan!" }, @@ -2872,8 +3116,11 @@ "nftAlreadyAdded": { "message": "NFT telah ditambahkan." }, + "nftAutoDetectionEnabled": { + "message": "Autodeteksi NFT diaktifkan" + }, "nftDisclaimer": { - "message": "Penafian: MetaMask mengambil file media dari url sumber. URL ini terkadang diubah oleh pasar tempat NFT dicetak." + "message": "Penafian: MetaMask mengambil file media dari url sumber. Url ini terkadang diubah oleh pasar tempat NFT dicetak." }, "nftOptions": { "message": "Opsi NFT" @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Tidak ada resolusi untuk domain yang tersedia." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snap, dan sebagian besar dompet perangkat keras, tidak akan berfungsi dengan versi browser saat ini." + }, "noNFTs": { "message": "Belum ada NFT" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Sesuaikan nonce transaksi" }, - "nonceFieldDescription": { - "message": "Aktifkan ini untuk mengubah nonce (nomor transaksi) di layar konfirmasi. Ini merupakan fitur lanjutan, gunakan dengan hati-hati." + "nonceFieldDesc": { + "message": "Aktifkan ini untuk mengubah nonce (nomor transaksi) saat mengirim aset. Ini merupakan fitur lanjutan, gunakan dengan hati-hati." }, "nonceFieldHeading": { "message": "Nonce kustom" @@ -3001,7 +3251,7 @@ "message": "Biaya prioritas (GWEI)" }, "notificationItemCheckBlockExplorer": { - "message": "Periksa di BlockExplorer" + "message": "Periksa di Block Explorer" }, "notificationItemCollection": { "message": "Koleksi" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 token baru ditemukan di akun ini" }, + "numberOfTokens": { + "message": "Jumlah token" + }, "ofTextNofM": { "message": "dari" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Nyala" }, - "onboarding": { - "message": "Orientasi" + "onboardedMetametricsAccept": { + "message": "Saya setuju" + }, + "onboardedMetametricsDisagree": { + "message": "Tidak, terima kasih" + }, + "onboardedMetametricsKey1": { + "message": "Perkembangan terkini" + }, + "onboardedMetametricsKey2": { + "message": "Fitur produk" + }, + "onboardedMetametricsKey3": { + "message": "Materi promosi lain yang relevan" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Selain $1, kami ingin menggunakan data untuk memahami cara Anda berinteraksi dengan komunikasi pemasaran.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Ini membantu kami mempersonalisasi hal yang kami bagikan kepada Anda, seperti:" + }, + "onboardedMetametricsParagraph3": { + "message": "Ingat, kami tidak pernah menjual data yang Anda berikan dan Anda dapat memilih untuk keluar setiap saat." + }, + "onboardedMetametricsTitle": { + "message": "Bantu kami meningkatkan pengalaman Anda" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Gateway IPFS memungkinkan untuk mengakses dan melihat data yang disimpan oleh pihak ketiga. Anda dapat menambahkan gateway IPFS khusus atau melanjutkan menggunakan default." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Saat kami mengumpulkan metrik, maka akan selalu..." }, - "onboardingMetametricsDisagree": { - "message": "Tidak, terima kasih" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Bantu kami meningkatkan MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Kami akan menggunakan data ini untuk mempelajari cara Anda berinteraksi dengan komunikasi pemasaran. Kami mungkin akan membagikan berita yang relevan (seperti fitur produk)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Akses penuh" }, @@ -3289,6 +3570,22 @@ "message": "Peringatan deteksi pengelabuan bergantung pada komunikasi dengan $1. jsDeliver akan mendapat akses ke alamat IP Anda. Lihat $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1H", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1B", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1M", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1T", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Jelajahi Snap" - }, - "openSeaToBlockaidDescription": { - "message": "Peringatan keamanan tidak lagi tersedia di jaringan ini. Menginstal Snap dapat meningkatkan keamanan Anda." - }, - "openSeaToBlockaidTitle": { - "message": "Perhatian!" - }, "operationFailed": { "message": "Pengoperasian Gagal" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Situs yang terhubung kini memiliki izin" }, + "permitSimulationDetailInfo": { + "message": "Anda memberikan izin kepada pengguna untuk menggunakan token sebanyak ini dari akun." + }, "personalAddressDetected": { "message": "Alamat pribadi terdeteksi. Masukkan alamat kontrak token." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Jaringan khusus populer" }, + "popularNetworkAddToolTip": { + "message": "Beberapa jaringan ini mengandalkan pihak ketiga. Koneksi ini kurang dapat diandalkan atau memungkinkan pihak ketiga melacak aktivitas. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portofolio" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Sebelumnya" }, + "price": { + "message": "Harga" + }, + "priceUnavailable": { + "message": "harga tidak tersedia" + }, "primaryCurrencySetting": { "message": "Mata uang primer" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Tingkat kuotasi" }, + "rank": { + "message": "Peringkat" + }, "reAddAccounts": { "message": "tambahkan kembali akun lain" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Terima" }, - "receiveTokensCamelCase": { - "message": "Terima token" - }, "recipientAddressPlaceholder": { "message": "Masukkan alamat publik (0x) atau nama ENS" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Atur ulang" }, - "resetStates": { - "message": "Reset State" - }, "resetWallet": { "message": "Reset dompet" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Cari akun" }, + "searchNfts": { + "message": "Cari NFT" + }, "searchTokens": { "message": "Cari token" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Amankan dompet saya (direkomendasikan)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Tuliskan dan simpan di beberapa tempat rahasia." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Simpan di pengelola kata sandi" + "message": "Tuliskan dan simpan di beberapa tempat rahasia." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Simpan di deposit box yang aman." }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Pilih token" }, "selectNFTPrivacyPreference": { - "message": "Aktifkan deteksi NFT pada Pengaturan" + "message": "Aktifkan Autodeteksi NFT" }, "selectPathHelp": { - "message": "Jika Anda tidak menemukan akun yang diharapkan, coba alihkan jalur HD." + "message": "Jika Anda tidak menemukan akun yang diharapkan, coba alihkan jalur HD atau jaringan yang dipilih saat ini." }, "selectType": { "message": "Pilih Jenis" @@ -4287,9 +4585,6 @@ "send": { "message": "Kirim" }, - "sendAToken": { - "message": "Kirimkan token" - }, "sendBugReport": { "message": "Kirimi kami laporan bug." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Jaringan uji Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Tetap Menyala" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask menggunakan layanan pihak ketiga tepercaya ini untuk meningkatkan kegunaan dan keamanan produk." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Ini bergantung pada API pihak ketiga yang berbeda untuk setiap jaringan, yang mengungkap alamat Ethereum dan alamat IP Anda." }, + "showLess": { + "message": "Ciutkan" + }, "showMore": { "message": "Tampilkan selengkapnya" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Tandatangani pesan ini hanya jika Anda benar-benar memahami isinya dan memercayai situs yang memintanya." }, - "signatureRequestWarning": { - "message": "Berhati-hatilah sebelum menandatangani pesan ini. Anda mungkin memberikan kendali penuh atas akun dan aset Anda kepada pihak lain pesan ini. Artinya, pihak tersebut dapat menguras akun Anda kapan saja. Lanjutkan dengan hati-hati. $1." - }, "signed": { "message": "Ditandatangani" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Penandatanganan" }, + "signingInWith": { + "message": "Masuk dengan" + }, "simulationDetailsFailed": { "message": "Terjadi kesalahan saat memuat estimasi Anda." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Estimasikan perubahan saldo" }, + "siweIssued": { + "message": "Dikeluarkan" + }, + "siweNetwork": { + "message": "Jaringan" + }, + "siweRequestId": { + "message": "Permintaan ID" + }, + "siweResources": { + "message": "Sumber daya" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Anda masuk ke sebuah situs dan tidak ada perkiraan perubahan pada akun Anda." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Lewati" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Akun yang dikontrol oleh Snap pihak ketiga." }, + "snapConnectTo": { + "message": "Hubungkan ke $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Izinkan $1 terhubung secara otomatis ke $2 tanpa persetujuan Anda.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 ingin menggunakan $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Situs web" }, + "snapHomeMenu": { + "message": "Menu Beranda Snap" + }, "snapInstallRequest": { "message": "Anda memberikan izin berikut dengan menginstal $1.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Sumber" }, + "speed": { + "message": "Kecepatan" + }, "speedUp": { "message": "Percepat" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Batas penggunaan terlalu besar" }, + "spender": { + "message": "Pengguna" + }, "spendingCap": { "message": "Batas penggunaan" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Log status berisi alamat akun publik Anda dan transaksi terkirim." }, - "states": { - "message": "State" - }, "status": { "message": "Status" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Terkirim" }, + "suggestedBySnap": { + "message": "Disarankan oleh $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Nama yang disarankan:" + }, "suggestedTokenSymbol": { "message": "Simbol ticker yang disarankan:" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Mengambil kuotasi" + "message": "Mengambil kuotasi..." }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm... terjadi kesalahan. Coba lagi, atau jika masalah terus berlanjut, hubungi dukungan pelanggan." @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Anda telah beralih ke" + "message": "Saat ini Anda menggunakan" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Mengalihkan jaringan akan membatalkan semua konfirmasi yang berstatus menunggu" @@ -5476,7 +5810,7 @@ "message": "Pilih tema MetaMask yang Anda sukai." }, "thingsToKeep": { - "message": "Hal-hal yang perlu diingat:" + "message": "Ingatlah:" }, "thirdPartySoftware": { "message": "Pemberitahuan perangkat lunak pihak ketiga", @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "koleksi ini" }, + "threeMonthsAbbreviation": { + "message": "3B", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Waktu" }, @@ -5498,45 +5836,6 @@ "message": "Ke: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Anda berisiko terkena serangan phishing. Lindungi diri dengan menonaktifkan eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Jika mengaktifkan pengaturan ini, Anda akan mendapatkan permintaan tanda tangan yang tidak terbaca. Dengan menandatangani pesan yang tidak Anda pahami, Anda mungkin setuju untuk memberikan dana dan NFT Anda." - }, - "toggleEthSignField": { - "message": "Permintaan et_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " Anda mungkin ditipu" - }, - "toggleEthSignModalBannerText": { - "message": "Jika Anda diminta mengaktifkan pengaturan ini," - }, - "toggleEthSignModalCheckBox": { - "message": "Saya memahami bahwa saya dapat kehilangan semua dana dan NFT jika mengaktifkan permintaan eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Mengizinkan permintaan eth_sign dapat membuat Anda rentan terhadap serangan phishing. Selalu tinjau URL dan berhati-hatilah saat menandatangani pesan yang berisi kode." - }, - "toggleEthSignModalFormError": { - "message": "Teks salah" - }, - "toggleEthSignModalFormLabel": { - "message": "Masukkan \"Saya hanya menandatangani yang saya pahami\" untuk melanjutkan" - }, - "toggleEthSignModalFormValidation": { - "message": "Saya hanya menandatangani yang saya pahami" - }, - "toggleEthSignModalTitle": { - "message": "Gunakan dengan risiko Anda sendiri" - }, - "toggleEthSignOff": { - "message": "NONAKTIF (Disarankan)" - }, - "toggleEthSignOn": { - "message": "AKTIF (Tidak disarankan)" - }, "toggleRequestQueueDescription": { "message": "Hal ini memungkinkan Anda memilih jaringan untuk setiap situs, daripada satu jaringan yang dipilih untuk semua situs. Fitur ini akan mencegah Anda berpindah jaringan secara manual, yang dapat merusak pengalaman pengguna di situs tertentu." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Alamat kontrak token" }, + "tokenDecimal": { + "message": "Desimal token" + }, "tokenDecimalFetchFailed": { "message": "Desimal token diperlukan. Temukan di: $1" }, @@ -5580,13 +5882,16 @@ "message": "ID token" }, "tokenList": { - "message": "Daftar token:" + "message": "Daftar token" }, "tokenScamSecurityRisk": { "message": "penipuan dan risiko keamanan token" }, "tokenShowUp": { - "message": "Token Anda mungkin tidak secara otomatis muncul di dompet Anda." + "message": "Token Anda mungkin tidak muncul secara otomatis di dompet Anda. " + }, + "tokenStandard": { + "message": "Standar token" }, "tokenSymbol": { "message": "Simbol token" @@ -5598,6 +5903,9 @@ "message": "$1 token baru ditemukan", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Token dalam koleksi" + }, "tooltipApproveButton": { "message": "Saya mengerti" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Total" }, + "totalVolume": { + "message": "Volume total" + }, "transaction": { "message": "transaksi" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "Transaksi dibuat dengan nilai sebesar $1 pada $2." }, + "transactionDataFunction": { + "message": "Fungsi" + }, "transactionDetailDappGasMoreInfo": { "message": "Situs yang disarankan" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Transfer dari" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Kami mengalami masalah saat menghubungkan Ledger Anda. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Menurut catatan kami, URL ini tidak sesuai dengan penyedia yang dikenal untuk ID chain ini." + }, "unapproved": { "message": "Tidak disetujui" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Perbarui" }, + "updateOrEditNetworkInformations": { + "message": "Perbarui informasi Anda atau" + }, "updateRequest": { "message": "Permintaan pembaruan" }, "updatedWithDate": { "message": "Diperbarui $1" }, + "uploadDropFile": { + "message": "Letakkan fail di sini" + }, + "uploadFile": { + "message": "Unggah fail" + }, "urlErrorMsg": { "message": "URL memerlukan awalan HTTP/HTTPS yang sesuai." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Apa ini?" }, + "wrongChainId": { + "message": "ID chain ini tidak sesuai dengan nama jaringan." + }, + "wrongNetworkName": { + "message": "Menurut catatan kami, nama jaringan mungkin tidak cocok dengan ID chain ini." + }, "xOfYPending": { "message": "$1 dari $2 berstatus menunggu", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Akun Anda" }, - "yourFundsMayBeAtRisk": { - "message": "Dana Anda mungkin berisiko" + "yourActivity": { + "message": "Aktivitas Anda" + }, + "yourBalance": { + "message": "Saldo Anda" }, "yourNFTmayBeAtRisk": { "message": "NFT Anda mungkin berisiko" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index cea9dde6fcfb..9f9085f4ed81 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -42,7 +42,7 @@ "message": "Portafoglio HW basato su QR" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (in arrivo)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "L'indirizzo nella richiesta di accesso non corrisponde all'indirizzo dell'account che stai utilizzando per accedere." @@ -1138,9 +1138,6 @@ "nonceField": { "message": "Personalizza il numero della transazione" }, - "nonceFieldDescription": { - "message": "Attiva per cambiare il numero della transazione nelle schermate di conferma. Questa è una funzionalità avanzata, usala con cautela." - }, "nonceFieldHeading": { "message": "Numero Transazione Personalizzato" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index e63e08d77cbe..44226098f705 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -42,7 +42,7 @@ "message": "QRハードウェアウォレットを接続" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (近日追加予定)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "サインインリクエストのアドレスが、サインインに使用しているアカウントのアドレスと一致していません。" @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "アカウントを選択する必要があります!" }, + "accountTypeNotSupported": { + "message": "アカウントタイプがサポートされていません" + }, "accounts": { "message": "アカウント" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "新しいイーサリアムアカウントを追加" }, + "addNewBitcoinAccount": { + "message": "新しいビットコインアカウントの追加 (ベータ)" + }, + "addNewBitcoinTestnetAccount": { + "message": "新しいビットコインアカウントの追加 (テストネット)" + }, "addNewToken": { "message": "新しいトークンを追加" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "NFTを追加" }, + "addRpcUrl": { + "message": "RPC URLを追加" + }, "addSnapAccountToggle": { "message": "「アカウントSnapの追加 (ベータ版)」を有効にする" }, @@ -305,12 +317,21 @@ "message": "トークンが見つからない場合、アドレスを貼り付けて手動でトークンを追加できます。トークンコントラクトアドレスは$1にあります", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "URLを追加" + }, "addingCustomNetwork": { "message": "ネットワークを追加中" }, "addingTokens": { "message": "トークンを追加しています" }, + "additionalNetworks": { + "message": "他のネットワーク" + }, + "additionalRpcUrl": { + "message": "他のRPC URL" + }, "address": { "message": "アドレス" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "詳細設定" }, + "advancedDetailsDataDesc": { + "message": "データ" + }, + "advancedDetailsHexDesc": { + "message": "16進法" + }, + "advancedDetailsNonceDesc": { + "message": "ナンス" + }, + "advancedDetailsNonceTooltip": { + "message": "これはアカウントのトランザクション番号です。最初のトランザクションのナンスは0で、順番に上がっていきます。" + }, "advancedGasFeeDefaultOptIn": { "message": "これらの値を$1ネットワークのデフォルトとして保存する", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "アラート" }, + "alertActionBuy": { + "message": "ETHを購入" + }, + "alertActionUpdateGas": { + "message": "ガスリミットを更新" + }, + "alertActionUpdateGasFee": { + "message": "手数料を更新" + }, + "alertActionUpdateGasFeeLevel": { + "message": "ガスオプションを更新" + }, "alertBannerMultipleAlertsDescription": { "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われる可能性があります。" }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "これは「設定」>「アラート」で変更できます" }, + "alertMessageGasEstimateFailed": { + "message": "正確な手数料を提供できず、この見積もりは高い可能性があります。カスタムガスリミットの入力をお勧めしますが、それでもトランザクションが失敗するリスクがあります。" + }, + "alertMessageGasFeeLow": { + "message": "低い手数料を選択すると、トランザクションに時間がかかり、待機時間が長くなります。より素早くトランザクションを行うには、市場に合った、または積極的な手数料のオプションを選択してください。" + }, + "alertMessageGasTooLow": { + "message": "このトランザクションを続行するには、ガスリミットを21000以上に上げる必要があります。" + }, + "alertMessageInsufficientBalance": { + "message": "アカウントにトランザクション手数料を支払うのに十分なETHがありません。" + }, + "alertMessageNetworkBusy": { + "message": "ガス価格が高く、見積もりはあまり正確ではありません。" + }, + "alertMessageNoGasPrice": { + "message": "手数料を手動で更新するまでこのトランザクションを進めることができません。" + }, + "alertMessagePendingTransactions": { + "message": "前のトランザクションが完了するまでこのトランザクションを実行できません。トランザクションをキャンセルするか加速させる方法をご覧ください。" + }, + "alertMessageSignInDomainMismatch": { + "message": "要求元のサイトはサインインしようとしているサイトではありません。ログイン情報を盗もうとしている可能性があります。" + }, + "alertMessageSignInWrongAccount": { + "message": "このサイトは正しくないアカウントでのサインインを求めています。" + }, + "alertMessageSigningOrSubmitting": { + "message": "このトランザクションは、前のトランザクションが完了しないと実行されません。" + }, "alertModalAcknowledge": { "message": "リスクを承知したうえで続行します" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "すべてのアラートを確認する" }, + "alertReasonGasEstimateFailed": { + "message": "不正確な手数料" + }, + "alertReasonGasFeeLow": { + "message": "低速" + }, + "alertReasonGasTooLow": { + "message": "低ガスリミット" + }, + "alertReasonInsufficientBalance": { + "message": "資金不足" + }, + "alertReasonNetworkBusy": { + "message": "ネットワークが混雑中" + }, + "alertReasonNoGasPrice": { + "message": "手数料の見積もりが利用できません" + }, + "alertReasonPendingTransactions": { + "message": "保留中のトランザクション" + }, + "alertReasonSignIn": { + "message": "不審なサインイン要求" + }, + "alertReasonWrongAccount": { + "message": "正しくないアカウント" + }, "alertSettingsUnconnectedAccount": { "message": "選択した未接続のアカウントを使用してWebサイトをブラウズしています" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "すべてのアクセス許可" }, + "allTimeHigh": { + "message": "最高記録" + }, + "allTimeLow": { + "message": "最低記録" + }, "allYourNFTsOf": { "message": "$1のすべてのNFT", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1および$2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "お知らせ" - }, "appDescription": { "message": "ブラウザにあるイーサリアムウォレット", "description": "The description of the application" @@ -516,17 +621,23 @@ "message": "アセットのオプション" }, "attemptSendingAssets": { - "message": "1つのネットワークから別のネットワークに直接アセットを送ろうとすると、アセットが永久に失われる可能性があります。必ずブリッジを使用してください。" + "message": "別のネットワークからアセットを送ろうとすると、アセットが失われる可能性があります。ネットワーク間で安全に資金を移動するには、必ずブリッジを使用してください。" }, "attemptSendingAssetsWithPortfolio": { - "message": "別のネットワークからアセットを送ろうとすると、アセットが失われる可能性があります。$1などのブリッジを使ってネットワーク間で安全に送金してください。" + "message": "別のネットワークからアセットを送ろうとすると、アセットが失われる可能性があります。ネットワーク間で安全に資金を移動するには、必ず$1などのブリッジを使用してください。" }, "attemptToCancelSwapForFree": { "message": "無料でスワップのキャンセルを試行" }, + "attributes": { + "message": "属性" + }, "attributions": { "message": "属性" }, + "auroraRpcDeprecationMessage": { + "message": "Infura RPC URLでAuroraがサポートされなくなりました。" + }, "authorizedPermissions": { "message": "以下の権限を承認しました" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "基本機能はオフになっています" }, + "basicConfigurationDescription": { + "message": "MetaMaskは、インターネットサービスを通じてトークンの詳細やガス設定などの基本的な機能を提供します。インターネットサービスを使用すると、この場合はMetaMaskに、ユーザーのIPアドレスが共有されます。これは他のどのWebサイトにアクセスした場合も同様で、MetaMaskはこのデータを一時的に使用し、ユーザーのデータを販売することは一切ありません。VPNを使用したり、これらのサービスをオフにしたりすることもできますが、MetaMaskでのエクスペリエンスに影響を与える可能性があります。詳細は$1をお読みください。", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "基本機能" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMaskベータ版がユーザーのシークレットリカバリーフレーズを求めることは絶対にありません。" }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "ビットコインアクティビティはサポートされていません" + }, + "bitcoinSupportSectionTitle": { + "message": "ビットコイン" + }, + "bitcoinSupportToggleDescription": { + "message": "この機能をオンにすると、既存のシークレットリカバリーフレーズで取得したビットコインアカウントをMetaMask拡張機能に追加できるようになります。これは試験運用中のベータ機能であるため、自己責任でご使用ください。新しいビットコインエクスペリエンスのフィードバックをご提供いただくには、こちらの$1に入力してください。", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "「新しいビットコインアカウントの追加 (ベータ)」を有効にする" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "この機能をオンにすると、テストネットワーク用にビットコインアカウントを追加できるようになります。" + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "「新しいビットコインアカウントの追加 (テストネット)」を有効にする" + }, "blockExplorerAccountAction": { "message": "アカウント", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "この要求を承諾することはお勧めしません。" + }, "blockaidDescriptionApproveFarming": { "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われる可能性があります。" }, @@ -666,7 +807,7 @@ "message": "このリクエストを承認すると、Blurに登録されている資産を誰かに盗まれる可能性があります。" }, "blockaidDescriptionErrored": { - "message": "エラーが発生したため、このリクエストはセキュリティプロバイダーにより確認されませんでした。慎重に進めてください。" + "message": "エラーが発生したため、セキュリティアラートをチェックできませんでした。関連するすべてのアドレスが信頼できる場合のみ続行してください。" }, "blockaidDescriptionMaliciousDomain": { "message": "悪質なドメインとやり取りしています。このリクエストを承認すると、資産を失う可能性があります。" @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われます。" }, + "blockaidDescriptionWarning": { + "message": "これは偽りの要求である可能性があります。関与しているすべてのアドレスを信頼できない限り、続行しないでください。" + }, "blockaidMessage": { "message": "プライバシーを保護 - サードパーティとデータが一切共有されません。Arbitrum、Avalanche、BNB Chain、イーサリアムメインネット、Linea、Optimism、Polygon、Base、Sepoliaで利用可能。" }, @@ -687,7 +831,7 @@ "message": "これは虚偽のリクエストです" }, "blockaidTitleMayNotBeSafe": { - "message": "リクエストは安全でない可能性があります" + "message": "ご注意ください" }, "blockaidTitleSuspicious": { "message": "これは不審なリクエストです" @@ -695,6 +839,9 @@ "blockies": { "message": "Blockie" }, + "boughtFor": { + "message": "購入価格" + }, "bridge": { "message": "ブリッジ" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "ハードウェアウォレットに接続するには、MetaMaskをGoogle Chromeで使用する必要があります。" }, + "circulatingSupply": { + "message": "循環供給量" + }, "clear": { "message": "消去" }, @@ -799,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "トークンを手動で追加するにはこちらをクリックしてください。" + "message": "トークンはいつでも手動で追加できます。" }, "close": { "message": "閉じる" @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "コレクション名" + }, "comboNoOptions": { "message": "オプションが見つかりません", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "アラートを確認したうえで続行します" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "アラートを確認したうえで続行します" + }, "confirmAlertModalDetails": { "message": "サインインすると、詐欺が判明しているサードパーティにすべての資産を奪われる可能性があります。続ける前にアラートを確認してください。" }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "$1への接続の確定" }, + "confirmDeletion": { + "message": "削除の確定" + }, + "confirmFieldPaymaster": { + "message": "手数料の支払元" + }, + "confirmFieldTooltipPaymaster": { + "message": "このトランザクションの手数料は、ペイマスターのスマートコントラクトにより支払われます。" + }, "confirmPassword": { "message": "パスワードの確認" }, "confirmRecoveryPhrase": { "message": "シークレットリカバリーフレーズの確認" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "このトランザクションの内容を完全に理解し、要求元のサイトを信頼する場合にのみ確定してください。" + "confirmRpcUrlDeletionMessage": { + "message": "RPC URLを削除してよろしいですか?このネットワークの情報は保存されません。" + }, + "confirmTitleDescPermitSignature": { + "message": "このサイトがトークンの使用許可を求めています。" + }, + "confirmTitleDescSIWESignature": { + "message": "サイトがこのアカウントを所有することを証明するためにサインインを求めています。" }, "confirmTitleDescSignature": { "message": "このメッセージの内容を承認し、要求元のサイトを信頼する場合にのみ確定してください。" }, + "confirmTitlePermitSignature": { + "message": "使用上限リクエスト" + }, + "confirmTitleSIWESignature": { + "message": "サインインリクエスト" + }, "confirmTitleSignature": { "message": "署名要求" }, @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "アカウントの作成" }, + "creatorAddress": { + "message": "クリエイターのアドレス" + }, "crossChainSwapsLink": { "message": "MetaMask Portfolioでネットワーク間でスワップ" }, @@ -1260,9 +1440,27 @@ "data": { "message": "データ" }, + "dataCollectionForMarketing": { + "message": "マーケティング目的のデータ収集" + }, + "dataCollectionForMarketingDescription": { + "message": "当社はMetaMetricsを使用して、ユーザーによる当社のマーケティングコミュニケーションとのインタラクションを把握します。また、関連ニュースをお伝えする場合もあります (製品の機能、その他資料など)。" + }, + "dataCollectionWarningPopoverButton": { + "message": "OK" + }, + "dataCollectionWarningPopoverDescription": { + "message": "マーケティング目的のデータ収集をオフにしました。これはこのデバイスにのみ適用されます。MetaMaskを他のデバイスで使用する場合は、そのデバイスでもオプトアウトしてください。" + }, "dataHex": { "message": "16進法" }, + "dataUnavailable": { + "message": "データが利用できません" + }, + "dateCreated": { + "message": "作成日" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "リクエストを解読" }, + "defaultRpcUrl": { + "message": "デフォルトのRPC URL" + }, "delete": { "message": "削除" }, @@ -1308,6 +1509,9 @@ "message": "$1ネットワークを削除しますか?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "RPC URLを削除" + }, "deposit": { "message": "入金" }, @@ -1333,18 +1537,6 @@ "details": { "message": "詳細" }, - "developerOptions": { - "message": "開発者用オプション" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "すべてのお知らせのisShownのブール値をfalseにリセットします。お知らせとは、「最新情報」ポップアップモーダルに表示される通知のことです。" - }, - "developerOptionsResetStatesOnboarding": { - "message": "オンボーディングに関するさまざまなステートをリセットし、「ウォレットのセキュリティ保護」オンボーディングページにリダイレクトします。" - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "タイムスタンプがセッションストレージに継続的に保存されるようになります" - }, "disabledGasOptionToolTipMessage": { "message": "元のガス代の10%以上という増額の条件を満たしていないため、「$1」は無効になっています。", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "不明な処理時間" }, + "editNetworkLink": { + "message": "元のネットワークを編集" + }, "editNonceField": { "message": "ナンスを編集" }, @@ -1549,12 +1744,12 @@ "message": "$1を有効にする", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "トークンの自動検出を有効にする" - }, "enabled": { "message": "有効" }, + "enabledNetworks": { + "message": "ネットワークが有効になりました" + }, "encryptionPublicKeyNotice": { "message": "$1は公開暗号鍵を必要とします。同意することによって、このサイトは暗号化されたメッセージを作成できます。", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "予想手数料" }, + "estimatedFeeTooltip": { + "message": "ネットワーク上のトランザクションの処理に支払われる金額" + }, "ethGasPriceFetchWarning": { "message": "現在メインのガスの見積もりサービスが利用できないため、バックアップのガス価格が提供されています。" }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Etherscanで表示" }, + "existingChainId": { + "message": "入力された情報は、既存のチェーンIDと関連付けられています。" + }, + "existingRpcUrl": { + "message": "このURLは別のチェーンIDに関連付けられています。" + }, "expandView": { "message": "ビューを展開" }, @@ -1701,7 +1905,7 @@ "message": "ニックネームの提案" }, "externalNameSourcesSettingDescription": { - "message": "当社はEtherscan、Infura、Lensプロトコルなどのサードパーティソースから、やり取りがあるアドレスのニックネームの提案を取得します。これらのソースは対象となるアドレスとユーザーのIPアドレスを把握できます。ユーザーのアカウントアドレスはサードパーティに公開されません。" + "message": "当社は、Etherscan、Infura、Lensプロトコルなどのサードパーティソースから、やり取りするアドレスに使用するニックネームの提案を取得します。これらのソースは対象となるアドレスとユーザーのIPアドレスを把握できます。ユーザーのアカウントアドレスはサードパーティに公開されません。" }, "failed": { "message": "失敗しました" @@ -1732,6 +1936,9 @@ "message": "ファイルのインポートが機能していない場合、ここをクリックしてください!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "正しいIDを検索:" + }, "flaskWelcomeUninstall": { "message": "この拡張機能はアンインストールしてください", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "パスワードを忘れた場合" }, + "form": { + "message": "フォーム" + }, "from": { "message": "移動元" }, @@ -1901,7 +2111,7 @@ "message": "Goerliテストネットワーク" }, "gotIt": { - "message": "了解!" + "message": "了解" }, "grantedToWithColon": { "message": "付与先:" @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "高" }, + "highestCurrentBid": { + "message": "現在の最高入札額" + }, + "highestFloorPrice": { + "message": "フロア価格の最高額" + }, "history": { "message": "履歴" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "このアクションは、ウォレットに既に一覧表示されているトークンを編集します。これは、フィッシングに使用される可能性があります。これらのトークンの表す内容を変更する意図が確実な場合にのみ承認します。$1に関する詳細をご覧ください" }, + "l1Fee": { + "message": "L1手数料" + }, + "l1FeeTooltip": { + "message": "L1ガス代" + }, + "l2Fee": { + "message": "L2手数料" + }, + "l2FeeTooltip": { + "message": "L2ガス代" + }, "lastConnected": { "message": "前回の接続" }, - "lastPriceSold": { - "message": "前回の売値" - }, "lastSold": { "message": "前回の売却" }, @@ -2495,6 +2720,12 @@ "message": "誰にも見られていないことを確認してください", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "時価総額" + }, + "marketDetails": { + "message": "マーケットの詳細" + }, "max": { "message": "最大" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "最大手数料" }, + "maxFeeTooltip": { + "message": "トランザクションの支払いに提供される最大手数料" + }, "maxPriorityFee": { "message": "最大優先手数料" }, @@ -2537,7 +2771,7 @@ "message": "MetaMask Institutionalバージョン" }, "metamaskNotificationsAreOff": { - "message": "ウォレット通知は現在アクティブではありません" + "message": "ウォレットの通知は現在無効になっています" }, "metamaskPortfolio": { "message": "MetaMask Portfolio。" @@ -2551,8 +2785,8 @@ "methodData": { "message": "方法" }, - "methodDataTransactionDescription": { - "message": "これが実行される具体的なアクションです。このデータには改ざんの可能性があるため、相手のサイトが信頼できることを確認してください。" + "methodDataTransactionDesc": { + "message": "解読された入力データに基づき実行された機能" }, "methodNotSupported": { "message": "このアカウントではサポートされていません。" @@ -2560,6 +2794,10 @@ "metrics": { "message": "メトリクス" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "選択されたアカウント ($1) は署名しようとしているアカウント ($2) と異なります" }, @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "ビットコイン" + }, "networkNameDefinition": { "message": "このネットワークに関連付けられている名前。" }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "ネットワークオプション" + }, "networkProvider": { "message": "ネットワークプロバイダー" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "「$1」が追加されました!" }, + "newNetworkEdited": { + "message": "“$1”が編集されました!" + }, "newNftAddedMessage": { "message": "NFTが追加されました!" }, @@ -2869,6 +3116,9 @@ "nftAlreadyAdded": { "message": "NFTがすでに追加されています。" }, + "nftAutoDetectionEnabled": { + "message": "NFTの自動検出が有効になりました" + }, "nftDisclaimer": { "message": "開示事項: MetaMaskはソースURLからメディアファイルを取得します。このURLは時々、NFTがミントされたマーケットプレイスにより変更されることがあります。" }, @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "指定されたドメインの名前解決ができません。" }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snap、およびほとんどのハードウェアウォレットは、現在お使いのブラウザのバージョンで使用できません。" + }, "noNFTs": { "message": "NFTはまだありません" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "トランザクション ナンスのカスタマイズ" }, - "nonceFieldDescription": { - "message": "承認画面上でナンス (トランザクション番号) を変更するには、この機能をオンにします。これは高度な機能であり、慎重に使用してください。" + "nonceFieldDesc": { + "message": "資産を送る際にナンス (トランザクション番号) を変更するには、この機能をオンにします。これは高度な機能であり、慎重に使用してください。" }, "nonceFieldHeading": { "message": "カスタムナンス" @@ -2998,7 +3251,7 @@ "message": "優先手数料 (gwei)" }, "notificationItemCheckBlockExplorer": { - "message": "BlockExplorerで確認する" + "message": "ブロックエクスプローラーで確認する" }, "notificationItemCollection": { "message": "コレクション" @@ -3016,13 +3269,13 @@ "message": "出金準備ができました" }, "notificationItemLidoStakeReadyToBeWithdrawnMessage": { - "message": "これでステーキングされていない $1 を引き出すことができます" + "message": "これでステーキングが解除された$1を引き出すことができます" }, "notificationItemLidoWithdrawalRequestedMessage": { - "message": "$1 のステーキングを解除するリクエストが送信されました" + "message": "$1のステーキングを解除するリクエストが送信されました" }, "notificationItemNFTReceivedFrom": { - "message": "NFTを次の元から受け取りました:" + "message": "NFTを次の相手から受け取りました:" }, "notificationItemNFTSentTo": { "message": "NFTを次の相手に送りました:" @@ -3037,7 +3290,7 @@ "message": "受け取りました" }, "notificationItemReceivedFrom": { - "message": "次の元から受け取りました:" + "message": "次の相手から受け取りました:" }, "notificationItemSent": { "message": "送りました" @@ -3073,7 +3326,7 @@ "message": "ステーキングの解除が完了しました" }, "notificationItemUnStaked": { - "message": "ステーキングが寛恕されました" + "message": "ステーキングが解除されました" }, "notificationItemUnStakingRequested": { "message": "ステーキングの解除がリクエストされました" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1つの新しいトークンがこのアカウントで見つかりました" }, + "numberOfTokens": { + "message": "トークンの数" + }, "ofTextNofM": { "message": "中の" }, @@ -3167,8 +3423,36 @@ "on": { "message": "オン" }, - "onboarding": { - "message": "オンボーディング" + "onboardedMetametricsAccept": { + "message": "同意する" + }, + "onboardedMetametricsDisagree": { + "message": "いいえ、結構です" + }, + "onboardedMetametricsKey1": { + "message": "最新情報" + }, + "onboardedMetametricsKey2": { + "message": "製品の機能" + }, + "onboardedMetametricsKey3": { + "message": "その他関連プロモーション資料" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "$1に加え、マーケティングコミュニケーションとのインタラクションについて把握するためにもデータを使用します。", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "これは、次のようなお伝えする情報のカスタマイズに役立ちます:" + }, + "onboardedMetametricsParagraph3": { + "message": "ユーザーから提供されたデータが販売されることは一切なく、いつでもオプトアウトできます。" + }, + "onboardedMetametricsTitle": { + "message": "エクスペリエンスの改善にご協力ください" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "IPFSゲートウェイにより、第三者がホスティングしているデータへのアクセスと表示が可能になります。カスタムIPFSゲートウェイを追加するか、引き続きデフォルトを使用できます。" @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "指標を収集する際、常に次の条件が適用されます..." }, - "onboardingMetametricsDisagree": { - "message": "結構です" - }, "onboardingMetametricsInfuraTerms": { "message": "このデータを他の目的に使用する際は、お知らせします。詳細は当社の$1をご覧ください。設定でいつでもオプトアウトできます。", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "MetaMaskの改善にご協力ください" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "このデータは、ユーザーによる当社のマーケティングコミュニケーションとのインタラクションを把握するために使用されます。また、関連ニュースをお伝えする場合もあります (製品の機能など)。" + }, "onboardingPinExtensionBillboardAccess": { "message": "フルアクセス" }, @@ -3286,6 +3570,22 @@ "message": "フィッシング検出アラートには$1との通信が必要です。jsDeliverはユーザーのIPアドレスにアクセスします。$2をご覧ください。", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1日", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1か月", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1週間", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1年", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Snapを閲覧" - }, - "openSeaToBlockaidDescription": { - "message": "このネットワークでセキュリティアラートが使用できなくなりました。Snapをインストールすると、セキュリティが向上する可能性があります。" - }, - "openSeaToBlockaidTitle": { - "message": "ご注意!" - }, "operationFailed": { "message": "操作に失敗しました" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "「接続済みのサイト」が「アクセス許可」に変更されました" }, + "permitSimulationDetailInfo": { + "message": "この数量のトークンをアカウントから転送する権限を使用者に付与しようとしています。" + }, "personalAddressDetected": { "message": "個人アドレスが検出されました。トークンコントラクトアドレスを入力してください。" }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "人気のカスタムネットワーク" }, + "popularNetworkAddToolTip": { + "message": "これらのネットワークの一部はサードパーティに依存しているため、接続の信頼性が低かったり、サードパーティによるアクティビティの追跡が可能になったりする可能性があります。$1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfolio" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "前へ" }, + "price": { + "message": "価格" + }, + "priceUnavailable": { + "message": "価格が利用できません" + }, "primaryCurrencySetting": { "message": "プライマリ通貨" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "クォートレート" }, + "rank": { + "message": "ランク" + }, "reAddAccounts": { "message": "他のアカウントを再度追加" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "受取" }, - "receiveTokensCamelCase": { - "message": "トークンの受取" - }, "recipientAddressPlaceholder": { "message": "パブリックアドレス (0x) またはENS名を入力してください" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "リセット" }, - "resetStates": { - "message": "ステートのリセット" - }, "resetWallet": { "message": "ウォレットをリセット" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "アカウントを検索" }, + "searchNfts": { + "message": "NFTを検索" + }, "searchTokens": { "message": "トークンを検索" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "ウォレットの安全を確保 (推奨)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "書き留めて、複数の秘密の場所に保管してください。" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "パスワードマネージャーに保存" + "message": "書き留めて、複数の秘密の場所に保管してください。" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "セーフティボックスに保管する。" }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,10 +4571,10 @@ "message": "トークンを選択" }, "selectNFTPrivacyPreference": { - "message": "設定でNFTの検出をオンにします" + "message": "NFTの自動検出を有効にする" }, "selectPathHelp": { - "message": "アカウントが見当たらない場合は、HDパスを切り替えてみてください。" + "message": "アカウントが見当たらない場合は、HDパスまたは現在選択されているネットワークを切り替えてみてください。" }, "selectType": { "message": "種類を選択" @@ -4284,9 +4585,6 @@ "send": { "message": "送金" }, - "sendAToken": { - "message": "トークンを送信" - }, "sendBugReport": { "message": "バグの報告をお送りください。" }, @@ -4311,7 +4609,7 @@ "description": "Symbol of the specified token" }, "sendSwapSubmissionWarning": { - "message": "このボタンをクリックすると、直ちにスワップトランザクションが開始します。続ける前に、以下のトランザクションの詳細を確認してください。" + "message": "このボタンをクリックすると、直ちにスワップトランザクションが開始します。続ける前に、トランザクションの詳細を確認してください。" }, "sendTokenAsToken": { "message": "$1を$2として送金", @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Sepoliaテストネットワーク" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Keep Alive" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMaskはこれらの信頼できるサードパーティサービスを使用して、製品の使いやすさと安全性を向上させています。" }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "これは、ネットワークごとに異なるサードパーティAPIに依存します。これにより、イーサリアムアドレスとIPアドレスが公開されます。" }, + "showLess": { + "message": "表示量を減らす" + }, "showMore": { "message": "他を表示" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "このメッセージの内容を完全に理解し、リクエスト元のサイトを信頼する場合にのみ署名してください。" }, - "signatureRequestWarning": { - "message": "このメッセージに署名するのは危険な可能性があります。このメッセージの相手に、アカウントと資産の完全なコントロールを許可しようとしている可能性があります。つまり、相手がいつでもアカウントからすべてを引き出せるようになります。慎重に進めてください。$1。" - }, "signed": { "message": "署名が完了しました" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "署名" }, + "signingInWith": { + "message": "サインイン方法:" + }, "simulationDetailsFailed": { "message": "予測結果の読み込み中にエラーが発生しました。" }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "予測される残高の増減" }, + "siweIssued": { + "message": "発行済み" + }, + "siweNetwork": { + "message": "ネットワーク" + }, + "siweRequestId": { + "message": "リクエストID" + }, + "siweResources": { + "message": "リソース" + }, + "siweSignatureSimulationDetailInfo": { + "message": "サイトにサインインしようとしていて、予想されるアカウントの変更はありません。" + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "スキップ" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "サードパーティSnapが制御するアカウント" }, + "snapConnectTo": { + "message": "$1に接続", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "$1によるユーザーの承認なしでの$2への自動接続を許可してください。", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1が$2の使用を求めています", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "Webサイト" }, + "snapHomeMenu": { + "message": "Snapホームメニュー" + }, "snapInstallRequest": { "message": "$1をインストールすると、次のアクセス許可が付与されます。", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "ソース" }, + "speed": { + "message": "速度" + }, "speedUp": { "message": "高速化" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "使用限度額が大きすぎます" }, + "spender": { + "message": "使用者" + }, "spendingCap": { "message": "使用上限" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "ステートログには、パブリックアカウントアドレスと送信済みトランザクションが含まれています。" }, - "states": { - "message": "ステート" - }, "status": { "message": "ステータス" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "送信済み" }, + "suggestedBySnap": { + "message": "$1による提案", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "提案された名前:" + }, "suggestedTokenSymbol": { "message": "推奨ティッカーシンボル:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "見積もりを取得中" + "message": "クォートを取得中..." }, "swapFetchingQuotesErrorDescription": { "message": "問題が発生しました。もう一度実行してください。エラーが解消されない場合は、カスタマサポートにお問い合わせください。" @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "このコレクション" }, + "threeMonthsAbbreviation": { + "message": "3か月", + "description": "Shortened form of '3 months'" + }, "time": { "message": "時間" }, @@ -5495,45 +5836,6 @@ "message": "移動先: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "フィッシング攻撃のリスクがあります。eth_signを無効にして自分の身を守ってください。" - }, - "toggleEthSignDescriptionField": { - "message": "この設定を有効にすると、読めない署名リクエストを受ける可能性があります。理解できないメッセージに署名すると、資金やNFTの提供に同意してしまう可能性があります。" - }, - "toggleEthSignField": { - "message": "Eth_signリクエスト" - }, - "toggleEthSignModalBannerBoldText": { - "message": "騙されている可能性があります" - }, - "toggleEthSignModalBannerText": { - "message": "この設定を有効にするよう求められた場合、" - }, - "toggleEthSignModalCheckBox": { - "message": "私は、eth_signリクエストを有効にすると、すべての資金とNFTを失う可能性があることを理解しています。" - }, - "toggleEthSignModalDescription": { - "message": "eth_signリクエストを許可すると、フィッシング攻撃を受けやすくなる可能性があります。常にURLを確認し、コードを含むメッセージに署名する際には注意してください。" - }, - "toggleEthSignModalFormError": { - "message": "テキストが正しくありません" - }, - "toggleEthSignModalFormLabel": { - "message": "続行するには、「私は理解できるものにしか署名しません」と入力してください" - }, - "toggleEthSignModalFormValidation": { - "message": "私は理解できるものにしか署名しません" - }, - "toggleEthSignModalTitle": { - "message": "自己責任でご利用ください" - }, - "toggleEthSignOff": { - "message": "オフ (推奨)" - }, - "toggleEthSignOn": { - "message": "オン (非推奨)" - }, "toggleRequestQueueDescription": { "message": "これにより、選択した単一のネットワークをすべてのサイトで使用するのではなく、サイトごとにネットワークを選択できます。この機能により、特定のサイトでのユーザーエクスペリエンスの妨げとなる、ネットワークの手動切り替えが不要になります。" }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "トークンコントラクトアドレス" }, + "tokenDecimal": { + "message": "トークンの小数桁数" + }, "tokenDecimalFetchFailed": { "message": "トークンの小数点以下の桁数が必要です。確認はこちら: $1" }, @@ -5585,6 +5890,9 @@ "tokenShowUp": { "message": "トークンはウォレットに自動的に表示されない可能性があります。" }, + "tokenStandard": { + "message": "トークン規格" + }, "tokenSymbol": { "message": "トークンシンボル" }, @@ -5595,6 +5903,9 @@ "message": "$1種類の新しいトークンが見つかりました", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "コレクションにあるトークン" + }, "tooltipApproveButton": { "message": "理解しました" }, @@ -5610,6 +5921,9 @@ "total": { "message": "合計" }, + "totalVolume": { + "message": "合計量" + }, "transaction": { "message": "トランザクション" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "トランザクションは$1の値が$2で作成されました。" }, + "transactionDataFunction": { + "message": "関数" + }, "transactionDetailDappGasMoreInfo": { "message": "サイトが提案されました" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "送金元" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Ledgerの接続に問題が発生しました。$1", "description": "$1 is a link to the wallet connection guide;" @@ -5766,7 +6087,7 @@ "message": "通知を使えば、ウォレットで何が起きているか常に把握できます。" }, "turnOnMetamaskNotificationsMessagePrivacyBold": { - "message": "「設定」>「通知」。" + "message": "「設定」>「通知」" }, "turnOnMetamaskNotificationsMessagePrivacyLink": { "message": "この機能を使用する際に当社がどのようにユーザーのプライバシーを保護するのか、ご覧ください。" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "当社の記録によると、このURLは、このチェーンIDの既知のプロバイダーと一致しません。" + }, "unapproved": { "message": "未承認" }, @@ -5844,12 +6168,21 @@ "update": { "message": "更新" }, + "updateOrEditNetworkInformations": { + "message": "情報を更新するか" + }, "updateRequest": { "message": "更新リクエスト" }, "updatedWithDate": { "message": "$1が更新されました" }, + "uploadDropFile": { + "message": "ここにファイルをドロップします" + }, + "uploadFile": { + "message": "ファイルをアップロード" + }, "urlErrorMsg": { "message": "URLには適切なHTTP/HTTPSプレフィックスが必要です。" }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "これは何ですか?" }, + "wrongChainId": { + "message": "このチェーンIDはネットワーク名と一致しません。" + }, + "wrongNetworkName": { + "message": "弊社の記録によると、ネットワーク名がこのチェーンIDと正しく一致していない可能性があります。" + }, "xOfYPending": { "message": "$2件中$1件が保留中", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "アカウント" }, - "yourFundsMayBeAtRisk": { - "message": "資金が危険にさらされている可能性があります" + "yourActivity": { + "message": "アクティビティ" + }, + "yourBalance": { + "message": "残高" }, "yourNFTmayBeAtRisk": { "message": "NFTが危険にさらされている可能性があります" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 03e87a8ada92..7286eefb09d7 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -42,7 +42,7 @@ "message": "QR 하드웨어 지갑을 연결하세요" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave(출시 예정)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "로그인 요청 주소가 현재 로그인 계정의 주소와 일치하지 않습니다." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "계정을 선택해야 합니다!" }, + "accountTypeNotSupported": { + "message": "지원하지 않는 계정 유형" + }, "accounts": { "message": "계정" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "새 이더리움 계정 추가" }, + "addNewBitcoinAccount": { + "message": "새 비트코인 계정 추가(베타)" + }, + "addNewBitcoinTestnetAccount": { + "message": "새 비트코인 계정 추가(테스트넷)" + }, "addNewToken": { "message": "신규 토큰 추가" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "NFT 추가" }, + "addRpcUrl": { + "message": "RPC URL 추가" + }, "addSnapAccountToggle": { "message": "\"계정 Snap 추가(베타)\" 활성화" }, @@ -305,12 +317,21 @@ "message": "이 토큰을 찾을 수 없으신가요? 토큰 주소를 붙여넣으면 토큰을 직접 추가할 수 있습니다. 토큰의 계약 주소는 $1에서 찾을 수 있습니다", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "URL 추가" + }, "addingCustomNetwork": { "message": "네트워크 추가" }, "addingTokens": { "message": "토큰 추가" }, + "additionalNetworks": { + "message": "추가 네트워크" + }, + "additionalRpcUrl": { + "message": "추가 RPC URL" + }, "address": { "message": "주소" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "고급 옵션" }, + "advancedDetailsDataDesc": { + "message": "데이터" + }, + "advancedDetailsHexDesc": { + "message": "16진수" + }, + "advancedDetailsNonceDesc": { + "message": "논스" + }, + "advancedDetailsNonceTooltip": { + "message": "계정의 트랜잭션 번호입니다. 첫 트랜잭션의 논스는 0이며 이는 순차적으로 증가합니다." + }, "advancedGasFeeDefaultOptIn": { "message": "이 수치를 $1 네트워크의 기본값으로 저장합니다.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "경고" }, + "alertActionBuy": { + "message": "ETH 매수" + }, + "alertActionUpdateGas": { + "message": "가스 한도 업데이트" + }, + "alertActionUpdateGasFee": { + "message": "수수료 업데이트" + }, + "alertActionUpdateGasFeeLevel": { + "message": "가스 옵션 업데이트" + }, "alertBannerMultipleAlertsDescription": { "message": "이 요청을 승인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다" }, + "alertMessageGasEstimateFailed": { + "message": "정확한 수수료를 제공할 수 없으며 예상 수수료가 높을 수 있습니다. 사용자 지정 가스 한도를 입력하는 것이 좋지만 트랜잭션이 여전히 실패할 위험이 있습니다." + }, + "alertMessageGasFeeLow": { + "message": "낮은 수수료를 선택하면 트랜잭션 속도가 느려지고 대기 시간이 길어집니다. 트랜잭션 속도를 높이려면 시장 수수료 또는 공격적 수수료 옵션을 선택하세요." + }, + "alertMessageGasTooLow": { + "message": "이 트랜잭션을 계속 진행하려면, 가스 한도를 21000 이상으로 늘려야 합니다." + }, + "alertMessageInsufficientBalance": { + "message": "계정에 트랜잭션 수수료를 지불할 수 있는 이더리움이 충분하지 않습니다." + }, + "alertMessageNetworkBusy": { + "message": "가스비가 높고 견적의 정확도도 떨어집니다." + }, + "alertMessageNoGasPrice": { + "message": "수수료를 직접 업데이트할 때까지는 이 트랜잭션을 진행할 수 없습니다." + }, + "alertMessagePendingTransactions": { + "message": "이 트랜잭션은 이전 트랜잭션이 완료될 때까지 진행되지 않습니다. 트랜잭션을 취소하거나 속도를 올리는 법을 알아보세요." + }, + "alertMessageSignInDomainMismatch": { + "message": "요청을 보낸 사이트에 로그인되어 있지 않습니다. 이는 로그인 정보를 도용하려는 시도일 수 있습니다." + }, + "alertMessageSignInWrongAccount": { + "message": "이 사이트에서 잘못된 계정으로 로그인하라고 요청합니다." + }, + "alertMessageSigningOrSubmitting": { + "message": "이 트랜잭션은 이전 트랜잭션이 완료된 경우에만 진행됩니다." + }, "alertModalAcknowledge": { "message": "위험성을 인지했으며, 계속 진행합니다" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "모든 경고 검토하기" }, + "alertReasonGasEstimateFailed": { + "message": "잘못된 수수료" + }, + "alertReasonGasFeeLow": { + "message": "느린 속도" + }, + "alertReasonGasTooLow": { + "message": "낮은 가스 한도" + }, + "alertReasonInsufficientBalance": { + "message": "자금 부족" + }, + "alertReasonNetworkBusy": { + "message": "네트워크 혼잡" + }, + "alertReasonNoGasPrice": { + "message": "수수료 견적 제공 불가" + }, + "alertReasonPendingTransactions": { + "message": "보류 중인 트랜잭션" + }, + "alertReasonSignIn": { + "message": "의심스러운 로그인 요청" + }, + "alertReasonWrongAccount": { + "message": "잘못된 계정" + }, "alertSettingsUnconnectedAccount": { "message": "연결되지 않은 계정을 선택하여 웹사이트 탐색" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "모든 권한" }, + "allTimeHigh": { + "message": "역대 최고" + }, + "allTimeLow": { + "message": "역대 최저" + }, "allYourNFTsOf": { "message": "$1의 모든 내 NFT", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 및 $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "공지" - }, "appDescription": { "message": "브라우저의 이더리움 지갑", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "자산 옵션" }, "attemptSendingAssets": { - "message": "한 네트워크에서 다른 네트워크로 자산을 직접 전송하면 자산이 영구적으로 손실될 수 있습니다. 반드시 브릿지를 이용하세요." + "message": "다른 네트워크로 자산을 직접 전송하면 자산이 영구적으로 손실될 수 있습니다. 브릿지를 이용하여 네트워크 간에 자금을 안전하게 전송하세요." }, "attemptSendingAssetsWithPortfolio": { "message": "다른 네트워크에서 자산을 전송하려고 하면 자산이 손실될 수 있습니다. $1 같은 브릿지를 사용하여 네트워크 간에 자산을 안전하게 전송하세요." @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "무료 스왑 취소 시도" }, + "attributes": { + "message": "속성" + }, "attributions": { "message": "속성" }, + "auroraRpcDeprecationMessage": { + "message": "Infura RPC URL은 더 이상 Aurora를 지원하지 않습니다." + }, "authorizedPermissions": { "message": "다음 권한을 승인했습니다." }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "기본 기능이 꺼져 있습니다." }, + "basicConfigurationDescription": { + "message": "MetaMask는 인터넷 서비스를 통해 토큰 세부 정보 및 가스 설정과 같은 기본 기능을 제공합니다. 인터넷 서비스를 이용할 때 IP 주소가 공유되며, 이 경우 MetaMask와 공유됩니다. 이는 다른 웹사이트를 방문할 때와 마찬가지입니다. MetaMask는 이 데이터를 일시적으로 사용하며 데이터를 절대 판매하지 않습니다. VPN을 사용하거나 이러한 서비스를 비활성화할 수 있지만 MetaMask 사용 환경에 영향을 미칠 수 있습니다. 자세한 내용은 $1 내용을 참고하세요.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "기본 기능" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask 베타는 비밀복구구문을 절대 묻지 않습니다." }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "비트코인 활동이 지원되지 않습니다." + }, + "bitcoinSupportSectionTitle": { + "message": "비트코인" + }, + "bitcoinSupportToggleDescription": { + "message": "이 기능을 켜면 기존 비밀복구구문에서 파생된 MetaMask 확장에 비트코인 계정을 추가할 수 있는 옵션을 이용할 수 있습니다. 이 기능은 실험적 베타 기능이므로 사용자의 책임하에 사용해야 합니다. 이 새로운 비트코인 경험에 대한 피드백을 보내려면 이 $1을(를) 작성해 주세요.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "'새 비트코인 계정 추가(베타)' 활성화" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "이 기능을 켜면 테스트 네트워크에 비트코인 계정을 추가할 수 있는 옵션이 제공됩니다." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "'새 비트코인 계정 추가(테스트넷)' 활성화" + }, "blockExplorerAccountAction": { "message": "계정", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "이 요청은 진행하지 않는 것이 좋습니다." + }, "blockaidDescriptionApproveFarming": { "message": "이 요청을 승인하면 스캠으로 알려진 타사가 회원님의 모든 자산을 가져갈 수 있습니다." }, @@ -666,7 +807,7 @@ "message": "이 요청을 승인하면, Blur에 있는 자산을 타인이 갈취할 수 있습니다." }, "blockaidDescriptionErrored": { - "message": "오류로 인해 보안업체에서 이 요청을 확인하지 못했습니다. 주의하여 진행하세요." + "message": "오류로 인해 보안 알림을 확인할 수 없었습니다. 모든 관련 주소를 신뢰하는 경우에만 계속 진행하세요." }, "blockaidDescriptionMaliciousDomain": { "message": "악성 도메인과 인터렉션하고 있습니다. 이 요청을 승인하면 본인의 자산을 잃을 수도 있습니다." @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "이 요청을 승인하면 스캠과 같은 타사가 회원님의 모든 자산을 가져갈 수 있습니다." }, + "blockaidDescriptionWarning": { + "message": "이는 사기성 요청일 수 있습니다. 관련된 모든 주소를 신뢰하는 경우에만 계속 진행하세요." + }, "blockaidMessage": { "message": "개인정보 보호 - 제3자와 데이터를 공유하지 않습니다. Arbitrum, Avalanche, BNB Chain, 이더리움 메인넷, Linea, Optimism, Polygon, Base, Sepolia에서 사용할 수 있습니다." }, @@ -687,7 +831,7 @@ "message": "사기성 요청입니다" }, "blockaidTitleMayNotBeSafe": { - "message": "요청이 안전하지 않을 수 있습니다" + "message": "조심하세요" }, "blockaidTitleSuspicious": { "message": "의심스러운 요청입니다" @@ -695,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "매수:" + }, "bridge": { "message": "브리지" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "하드웨어 지갑에 연결하려면 Google Chrome에서 MetaMask를 사용해야 합니다." }, + "circulatingSupply": { + "message": "순환 공급" + }, "clear": { "message": "지우기" }, @@ -799,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "여기를 클릭하여 토큰을 직접 추가하세요." + "message": "토큰은 언제든지 직접 추가할 수 있습니다." }, "close": { "message": "닫기" @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "컬렉션 이름" + }, "comboNoOptions": { "message": "옵션을 찾을 수 없습니다", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "경고를 인지했으며, 계속 진행합니다" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "경고를 인지했으며, 계속 진행합니다" + }, "confirmAlertModalDetails": { "message": "로그인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다. 계속하기 전에 경고를 검토하세요." }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "$1에 연결 확인" }, + "confirmDeletion": { + "message": "컨펌 삭제" + }, + "confirmFieldPaymaster": { + "message": "수수료 지불:" + }, + "confirmFieldTooltipPaymaster": { + "message": "이 트랜잭션에 대한 수수료는 Paymaster 스마트 계약에서 지불합니다." + }, "confirmPassword": { "message": "비밀번호 컨펌" }, "confirmRecoveryPhrase": { "message": "비밀복구구문 컨펌" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "요청하는 사이트를 신뢰하고 그 내용을 완전히 이해하는 경우에만 이 트랜젝션을 컨펌하세요." + "confirmRpcUrlDeletionMessage": { + "message": "정말로 RPC URL을 삭제하시겠습니까? 고객님의 정보는 이 네트워크에 저장되지 않습니다." + }, + "confirmTitleDescPermitSignature": { + "message": "해당 사이트에서 토큰 사용 승인을 요청합니다." + }, + "confirmTitleDescSIWESignature": { + "message": "회원님이 이 계정을 소유하고 있음을 확인하기 위해 로그인을 요청하는 사이트가 있습니다." }, "confirmTitleDescSignature": { "message": "요청하는 사이트를 신뢰하고 그 내용을 완전히 이해하는 경우에만 이 메시지를 컨펌하세요." }, + "confirmTitlePermitSignature": { + "message": "지출 한도 요청" + }, + "confirmTitleSIWESignature": { + "message": "로그인 요청" + }, "confirmTitleSignature": { "message": "서명 요청" }, @@ -958,7 +1135,7 @@ "message": "연결 대상:" }, "connecting": { - "message": "연결 중..." + "message": "연결 중" }, "connectingTo": { "message": "$1에 연결 중" @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "계정 생성" }, + "creatorAddress": { + "message": "크리에이터 주소" + }, "crossChainSwapsLink": { "message": "MetaMask Portfolio로 네트워크 간 스왑" }, @@ -1260,9 +1440,27 @@ "data": { "message": "데이터" }, + "dataCollectionForMarketing": { + "message": "마케팅을 위한 데이터 수집" + }, + "dataCollectionForMarketingDescription": { + "message": "MetaMetrics를 사용하여 사용자가 마케팅 커뮤니케이션과 어떻게 상호 작용하는지 파악할 것입니다. 관련 뉴스(예: 제품 기능 및 기타 자료)를 공유할 수 있습니다." + }, + "dataCollectionWarningPopoverButton": { + "message": "확인" + }, + "dataCollectionWarningPopoverDescription": { + "message": "마케팅 목적의 데이터 수집을 비활성화했습니다. 이는 이 장치에만 적용됩니다. 다른 장치에서 MetaMask를 사용하면 해당 장치에서도 데이터 수집을 비활성화해야 합니다." + }, "dataHex": { "message": "헥스" }, + "dataUnavailable": { + "message": "데이터 사용 불가" + }, + "dateCreated": { + "message": "생성일" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "암호 해독 요청" }, + "defaultRpcUrl": { + "message": "기본 RPC URL" + }, "delete": { "message": "삭제" }, @@ -1308,6 +1509,9 @@ "message": "$1 네트워크를 삭제하시겠습니까?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "RPC URL 삭제" + }, "deposit": { "message": "예치" }, @@ -1333,18 +1537,6 @@ "details": { "message": "세부 정보" }, - "developerOptions": { - "message": "개발자 옵션" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Resets isShown boolean to false for all announcements. Announcements are the notifications shown in the What's New popup modal." - }, - "developerOptionsResetStatesOnboarding": { - "message": "온보딩과 관련된 다양한 상태를 초기화하고 '지갑 보안' 온보딩 페이지로 리디렉션합니다." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "session.storage에 타임스탬프가 지속적으로 저장됩니다" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” 유형은 오리지널 가스비를 최소 10% 인상해야 하는 기준에 미치지 못하므로 비활성화되었습니다.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "알 수 없는 처리 시간" }, + "editNetworkLink": { + "message": "원본 네트워크 편집" + }, "editNonceField": { "message": "논스 편집" }, @@ -1549,12 +1744,12 @@ "message": "$1 활성화", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "토큰 자동 감지 활성화" - }, "enabled": { "message": "활성화됨" }, + "enabledNetworks": { + "message": "활성화된 네트워크" + }, "encryptionPublicKeyNotice": { "message": "$1에서 회원님의 공개 암호화 키를 요구합니다. 동의를 받으면 이 사이트에서 암호화된 메시지를 작성하여 회원님에게 전송할 수 있습니다.", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "예상 수수료" }, + "estimatedFeeTooltip": { + "message": "네트워크에서 트랜잭션을 처리하기 위해 지불한 금액입니다." + }, "ethGasPriceFetchWarning": { "message": "현재 주요 가스 견적 서비스를 사용할 수 없으므로 백업 가스 가격을 제공합니다." }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Etherscan에서 보기" }, + "existingChainId": { + "message": "입력한 정보는 기존 체인 ID와 연결되어 있습니다." + }, + "existingRpcUrl": { + "message": "이 URL은 다른 체인 ID랑 연결되어 있습니다." + }, "expandView": { "message": "보기 확장" }, @@ -1701,7 +1905,7 @@ "message": "추천 닉네임" }, "externalNameSourcesSettingDescription": { - "message": "회원님이 상호작용하는 주소에 대한 추천 닉네임을 Etherscan, Infura, Lens Protocol과 같은 제3자에게서 가져옵니다. 이러한 제3자는 해당 주소와 회원님의 IP 주소를 볼 수 있습니다. 회원님의 계정 주소는 제3자에게 노출되지 않습니다." + "message": "Etherscan, Infura, Lens Protocol과 같은 제삼자에게서 회원님이 상호작용하는 주소에 대한 추천 닉네임을 가져올 것입니다. 이러한 제삼자는 해당 주소와 회원님의 IP 주소를 볼 수 있습니다. 회원님의 계정 주소는 제삼자에게 노출되지 않습니다." }, "failed": { "message": "실패" @@ -1732,6 +1936,9 @@ "message": "파일 가져오기가 작동하지 않나요? 여기를 클릭하세요.", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "다음에서 적합한 것을 찾아보세요" + }, "flaskWelcomeUninstall": { "message": "이 확장 프로그램을 삭제해야 합니다", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "비밀번호를 잊으셨나요?" }, + "form": { + "message": "양식" + }, "from": { "message": "발신" }, @@ -1901,7 +2111,7 @@ "message": "Goerli 테스트 네트워크" }, "gotIt": { - "message": "확인했습니다!" + "message": "확인" }, "grantedToWithColon": { "message": "부여 대상:" @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "높음" }, + "highestCurrentBid": { + "message": "현재 최고 입찰" + }, + "highestFloorPrice": { + "message": "최고 바닥 가격" + }, "history": { "message": "기록" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "이 작업은 지갑에 목록에 등재되어 피싱에 사용될 수 있는 토큰을 편집합니다. 해당 토큰이 나타내는 내용을 변경하려는 경우에만 작업을 승인하세요. $1에 대해 알아보기" }, + "l1Fee": { + "message": "L1 수수료" + }, + "l1FeeTooltip": { + "message": "L1 가스비" + }, + "l2Fee": { + "message": "L2 수수료" + }, + "l2FeeTooltip": { + "message": "L2 가스비" + }, "lastConnected": { "message": "마지막 연결" }, - "lastPriceSold": { - "message": "최근 판매 가격" - }, "lastSold": { "message": "최근 판매" }, @@ -2495,6 +2720,12 @@ "message": "다른 사람이 이 화면을 보고 있지는 않은지 확인하세요.", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "시가 총액" + }, + "marketDetails": { + "message": "시장 상세 정보" + }, "max": { "message": "최대" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "최대 요금" }, + "maxFeeTooltip": { + "message": "트랜잭션에 지불하기 위해 제공되는 최대 수수료입니다." + }, "maxPriorityFee": { "message": "최대 우선 요금" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "메소드" }, - "methodDataTransactionDescription": { - "message": "이는 구체적인 조치 사항입니다. 이 데이터는 위조될 수 있으므로, 상대 사이트를 신뢰할 수 있는지 확인하세요." + "methodDataTransactionDesc": { + "message": "디코딩된 입력 데이터를 기반으로 실행되는 함수입니다." }, "methodNotSupported": { "message": "이 계정에서는 지원되지 않습니다." @@ -2560,6 +2794,10 @@ "metrics": { "message": "메트릭" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "선택한 계정($1)이 서명하려는 계정($2)과 다릅니다" }, @@ -2672,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "이 네트워크의 네이티브 토큰은 $1입니다. 이는 가스비 지불에 사용하는 토큰입니다.", + "message": "이 네트워크의 네이티브 토큰은 $1입니다. 이는 가스비 지불에 사용하는 토큰입니다. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "기본" }, + "networkNameBitcoin": { + "message": "비트코인" + }, "networkNameDefinition": { "message": "이 네트워크와 연결된 이름입니다." }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "네트워크 옵션" + }, "networkProvider": { "message": "네트워크 공급업체" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "“$1”(을)를 성공적으로 추가했습니다!" }, + "newNetworkEdited": { + "message": "“$1”(을)를 성공적으로 편집했습니다!" + }, "newNftAddedMessage": { "message": "NFT를 성공적으로 추가했습니다!" }, @@ -2869,8 +3116,11 @@ "nftAlreadyAdded": { "message": "NFT가 이미 추가되었습니다." }, + "nftAutoDetectionEnabled": { + "message": "NFT 자동 감지 활성화 완료" + }, "nftDisclaimer": { - "message": "면책 조항: MetaMask는 소스 URL에서 미디어 파일을 가져옵니다. 이러한 URL은 때때로 NFT가 민팅된 마켓플레이스에서 변경되기도 합니다." + "message": "면책 조항: MetaMask는 소스 URL에서 미디어 파일을 가져옵니다. 이러한 URL은 때때로 NFT가 민팅된 마켓플레이스에 의해 변경되기도 합니다." }, "nftOptions": { "message": "NFT 옵션" @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "도메인에 대한 해결 방법이 제공되지 않았습니다." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snap 및 대부분의 하드웨어 지갑은 현재 사용 중인 브라우저 버전에서 작동하지 않습니다." + }, "noNFTs": { "message": "아직 NFT가 없음" }, @@ -2941,7 +3194,7 @@ "message": "웹캠을 찾을 수 없음" }, "nonCustodialAccounts": { - "message": "MetaMask Institutional은 이러한 계정을 비밀복구구문 백업에 사용하려는 경우 비수탁형 계정을 사용할 수 있도록 허용합니다." + "message": "이러한 계정을 비밀복구구문 백업에 사용하려는 경우 MetaMask Institutional은 비수탁형 계정을 사용할 수 있도록 허용합니다." }, "nonce": { "message": "논스" @@ -2949,8 +3202,8 @@ "nonceField": { "message": "트랜잭션 논스 맞춤화" }, - "nonceFieldDescription": { - "message": "이 기능을 켜면 컨펌 화면에서 논스(트랜잭션 번호)를 변경할 수 있습니다. 이는 고급 기능이므로 주의해서 사용해야 합니다." + "nonceFieldDesc": { + "message": "자산을 전송할 때 논스(트랜잭션 번호)를 변경하려면 이 기능을 켭니다. 이 기능은 고급 기능이므로 신중하게 사용하세요." }, "nonceFieldHeading": { "message": "맞춤 논스" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "계정에서 1개의 새 토큰을 찾았습니다" }, + "numberOfTokens": { + "message": "토큰 수" + }, "ofTextNofM": { "message": "/" }, @@ -3167,8 +3423,36 @@ "on": { "message": "켜기" }, - "onboarding": { - "message": "온보딩" + "onboardedMetametricsAccept": { + "message": "동의합니다" + }, + "onboardedMetametricsDisagree": { + "message": "아니요, 괜찮습니다" + }, + "onboardedMetametricsKey1": { + "message": "최신 개발" + }, + "onboardedMetametricsKey2": { + "message": "제품 특징" + }, + "onboardedMetametricsKey3": { + "message": "기타 관련 프로모션 자료" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "$1 외에도 데이터를 활용하여 사용자가 마케팅 커뮤니케이션과 상호 작용하는 방식을 이해하고자 합니다.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "이를 통해 사용자와 공유하는 다음과 같은 콘텐츠를 최적화할 수 있습니다." + }, + "onboardedMetametricsParagraph3": { + "message": "사용자가 제공한 데이터는 절대 판매하지 않습니다. 데이터 수집은 언제든지 거부할 수 있습니다." + }, + "onboardedMetametricsTitle": { + "message": "사용자 경험을 개선할 수 있도록 도와주세요" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "IPFS 게이트웨이를 사용하면 타사 호스팅 데이터에 액세스하여 이를 볼 수 있습니다. 사용자 지정 IPFS 게이트웨이를 추가하셔도 좋고 기본 설정을 계속 사용하셔도 됩니다." @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "메트릭을 수집할 때는 항상..." }, - "onboardingMetametricsDisagree": { - "message": "괜찮습니다" - }, "onboardingMetametricsInfuraTerms": { "message": "이 데이터를 다른 목적으로 사용하기로 결정하면 알려드리겠습니다. 자세한 내용은 $1을(를) 참고하세요. 언제든지 설정으로 이동하여 해제할 수 있습니다.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "MetaMask 개선에 도움을 주세요" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "해당 데이터를 사용하여 사용자가 마케팅 커뮤니케이션과 어떻게 상호 작용하는지 파악할 것입니다. 관련 뉴스(예: 제품 기능 및 기타 자료)를 공유할 수 있습니다." + }, "onboardingPinExtensionBillboardAccess": { "message": "전체 액세스" }, @@ -3286,6 +3570,22 @@ "message": "피싱 감지 경고는 $1과(와)의 통신에 의존합니다. jsDeliver는 회원님의 IP 주소에 액세스할 수 있습니다. $2 보기.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1일", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1개월", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1주일", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1년", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Snap 탐색" - }, - "openSeaToBlockaidDescription": { - "message": "이 네트워크에서 더 이상 보안 경고를 사용할 수 없습니다. Snap을 설치하면 보안을 강화할 수 있습니다." - }, - "openSeaToBlockaidTitle": { - "message": "주의하세요!" - }, "operationFailed": { "message": "작업에 실패했습니다" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "이제 연결된 사이트에 권한이 부여됩니다" }, + "permitSimulationDetailInfo": { + "message": "내 계정에서 이만큼의 토큰을 사용할 수 있도록 승인합니다." + }, "personalAddressDetected": { "message": "개인 주소가 발견되었습니다. 토큰 계약 주소를 입력하세요." }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "인기 사용자 정의 네트워크" }, + "popularNetworkAddToolTip": { + "message": "이러한 네트워크 중 일부는 제삼자에 의존합니다. 이러한 연결은 안정성이 떨어지거나 제삼자가 활동을 추적할 수 있습니다. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "포트폴리오" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "이전" }, + "price": { + "message": "가격" + }, + "priceUnavailable": { + "message": "가격 사용 불가" + }, "primaryCurrencySetting": { "message": "기본 통화" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "견적 비율" }, + "rank": { + "message": "순위" + }, "reAddAccounts": { "message": "다른 계정을 다시 추가" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "받기" }, - "receiveTokensCamelCase": { - "message": "토큰 받기" - }, "recipientAddressPlaceholder": { "message": "공개 주소(0x) 또는 ENS 제목 입력" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "재설정" }, - "resetStates": { - "message": "상태 초기화" - }, "resetWallet": { "message": "지갑 초기화" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "계정 검색" }, + "searchNfts": { + "message": "NFT 검색" + }, "searchTokens": { "message": "토큰 검색" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "내 지갑 보호(권장)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "적어서 여러 비밀 장소에 보관하세요." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "비밀번호 수탁자에 저장" + "message": "적어서 여러 비밀 장소에 보관하세요." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "대여 금고에 보관." }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,10 +4571,10 @@ "message": "토큰 선택" }, "selectNFTPrivacyPreference": { - "message": "설정에서 NFT 감지 켜기" + "message": "NFT 자동 감기 지능 켜기" }, "selectPathHelp": { - "message": "아래에 기존 Ledger 계정이 표시되지 않는다면 경로를 \"Legacy (MEW / MyCrypto)\"로 변경해 보세요." + "message": "원하는 계정이 표시되지 않는다면 HD 경로 또는 현재 선택한 네트워크를 전환해 보세요." }, "selectType": { "message": "유형 선택" @@ -4284,9 +4585,6 @@ "send": { "message": "보내기" }, - "sendAToken": { - "message": "토큰 보내기" - }, "sendBugReport": { "message": "버그 리포트 전송" }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Sepolia 테스트 네트워크" }, - "serviceWorkerKeepAlive": { - "message": "서비스 작업자 유지" - }, "setAdvancedPrivacySettingsDetails": { "message": "이와 같이 MetaMask는 신용있는 타사의 서비스를 사용하여 제품 가용성과 안전성을 향상합니다." }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "이는 네트워크마다 다른 제삼자 API에 의존하며, 이더리움 주소와 IP 주소가 노출됩니다." }, + "showLess": { + "message": "간략히 보기" + }, "showMore": { "message": "더 보기" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "요청하는 사이트를 신뢰하고 그 내용을 완전히 이해하는 경우에만 이 메시지에 서명하세요." }, - "signatureRequestWarning": { - "message": "본 메시지에 서명하는 행위는 위험의 가능성을 내포하고 있습니다. 본 서명을 통해 메시지 발신 당사자에게 귀하의 계정 및 모든 자산에 대해 완전한 권한을 부여할 수 있기 때문입니다. 이를 통해 계정의 모든 잔액을 인출할 수 있기도 하다는 뜻입니다. 주의하여 진행하세요. $1" - }, "signed": { "message": "서명완료" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "서명" }, + "signingInWith": { + "message": "다음으로 로그인:" + }, "simulationDetailsFailed": { "message": "추정치를 불러오는 동안 오류가 발생했습니다." }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "예상 잔액 변동" }, + "siweIssued": { + "message": "발행됨" + }, + "siweNetwork": { + "message": "네트워크" + }, + "siweRequestId": { + "message": "요청 ID" + }, + "siweResources": { + "message": "리소스" + }, + "siweSignatureSimulationDetailInfo": { + "message": "사이트에 로그인 중이며 계정에 예상되는 변경 사항이 없습니다." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "건너뛰기" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "제삼자 Snap이 제어하는 계정입니다." }, + "snapConnectTo": { + "message": "$1에 연결", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "승인 없이 $1이(가) $2에 자동으로 연결되도록 하세요.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1에서 $2 사용을 원합니다.", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "웹사이트" }, + "snapHomeMenu": { + "message": "Snap 홈 메뉴" + }, "snapInstallRequest": { "message": "$1 설치는 다음과 같은 권한을 허용합니다.", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "소스" }, + "speed": { + "message": "속도" + }, "speedUp": { "message": "가속화" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "지출 한도가 너무 큼" }, + "spender": { + "message": "사용자" + }, "spendingCap": { "message": "지출 한도" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "상태 로그에 공개 계정 주소와 전송된 트랜잭션이 있습니다." }, - "states": { - "message": "상태" - }, "status": { "message": "상태" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "제출됨" }, + "suggestedBySnap": { + "message": "제안인: $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "추천 이름:" + }, "suggestedTokenSymbol": { "message": "추천 티커 심볼:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "견적을 가져오는 중" + "message": "견적을 가져오는 중..." }, "swapFetchingQuotesErrorDescription": { "message": "음.... 문제가 발생했습니다. 다시 시도해 보고 오류가 해결되지 않는다면 고객 지원에 문의하세요." @@ -5434,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "다음으로 변경했습니다:" + "message": "다음을 사용 중입니다:" }, "switchingNetworksCancelsPendingConfirmations": { "message": "네트워크를 전환하면 대기 중인 모든 컨펌 작업이 취소됩니다." @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "이 컬렉션" }, + "threeMonthsAbbreviation": { + "message": "3개월", + "description": "Shortened form of '3 months'" + }, "time": { "message": "시간" }, @@ -5495,45 +5836,6 @@ "message": "수신: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "피싱 공격의 위험이 있습니다. eth_sign을 비활성화해 자신을 보호하세요." - }, - "toggleEthSignDescriptionField": { - "message": "이 설정을 사용하면 읽을 수 없는 요청에 서명하게 될 위험이 있습니다. 이해할 수 없는 메시지에 서명하면 자금이나 NFT 손실에 동의할 수도 있습니다." - }, - "toggleEthSignField": { - "message": "Eth_서명 요청" - }, - "toggleEthSignModalBannerBoldText": { - "message": " 사기 가능성이 있습니다" - }, - "toggleEthSignModalBannerText": { - "message": "이 설정을 켜라는 요청을 받았다면," - }, - "toggleEthSignModalCheckBox": { - "message": "eth_sign 요청을 활성화하면 내 모든 자금과 NFT를 잃을 수도 있다는 사실을 이해합니다. " - }, - "toggleEthSignModalDescription": { - "message": "eth_sign 요청을 허용하면 피싱 공격에 취약해질 수 있습니다. 항상 URL을 검토하고 코드가 포함된 메시지에 서명할 때는 주의하세요." - }, - "toggleEthSignModalFormError": { - "message": "텍스트가 잘못되었습니다" - }, - "toggleEthSignModalFormLabel": { - "message": "계속하려면 \"본인은 이해한 사항에만 서명합니다\"라고 입력하세요" - }, - "toggleEthSignModalFormValidation": { - "message": "본인은 이해한 사항에만 서명합니다" - }, - "toggleEthSignModalTitle": { - "message": "위험을 감수하고 사용하기" - }, - "toggleEthSignOff": { - "message": "끄기(권장 사항)" - }, - "toggleEthSignOn": { - "message": "켜기(비권장 사항)" - }, "toggleRequestQueueDescription": { "message": "이 기능을 이용하면 모든 사이트에 한 가지 네트워크를 선택하여 사용하는 대신, 사이트별로 네트워크를 다르게 선택할 수 있습니다. 이 기능을 사용하면 수동으로 네트워크를 전환하지 않아도 되므로 특정 사이트에서 사용자 경험이 저해되지 않습니다." }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "토큰 계약 주소" }, + "tokenDecimal": { + "message": "토큰 십진수" + }, "tokenDecimalFetchFailed": { "message": "토큰 십진수가 필요합니다. $1에서 찾아보세요" }, @@ -5583,7 +5888,10 @@ "message": "토큰 사기 및 보안 위험" }, "tokenShowUp": { - "message": "토큰이 지갑에서 자동으로 표시되지 않을 수 있습니다." + "message": "토큰이 지갑에 자동으로 표시되지 않을 수 있습니다. " + }, + "tokenStandard": { + "message": "토큰 표준" }, "tokenSymbol": { "message": "토큰 기호" @@ -5595,6 +5903,9 @@ "message": "$1개의 새 토큰을 찾았습니다", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "컬렉션 내 토큰" + }, "tooltipApproveButton": { "message": "이해했습니다" }, @@ -5610,6 +5921,9 @@ "total": { "message": "합계" }, + "totalVolume": { + "message": "총 거래량" + }, "transaction": { "message": "트랜잭션" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "$2에서 $1 값으로 생성된 트랜잭션" }, + "transactionDataFunction": { + "message": "기능" + }, "transactionDetailDappGasMoreInfo": { "message": "추천 사이트" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "전송 위치" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Ledger 연결에 오류가 발생했습니다. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "기록에 따르면 이 URL은 이 체인 ID의 알려진 제공업체와 일치하지 않습니다." + }, "unapproved": { "message": "승인되지 않음" }, @@ -5844,12 +6168,21 @@ "update": { "message": "업데이트" }, + "updateOrEditNetworkInformations": { + "message": "정보를 업데이트하거나" + }, "updateRequest": { "message": "업데이트 요청" }, "updatedWithDate": { "message": "$1에 업데이트됨" }, + "uploadDropFile": { + "message": "여기에 파일을 드롭" + }, + "uploadFile": { + "message": "파일 업로드" + }, "urlErrorMsg": { "message": "URI에는 적절한 HTTP/HTTPS 접두사가 필요합니다." }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "이것은 무엇인가요?" }, + "wrongChainId": { + "message": "이 체인 ID는 네트워크 이름과 일치하지 않습니다." + }, + "wrongNetworkName": { + "message": "기록에 따르면 네트워크 이름이 이 체인 ID와 일치하지 않습니다." + }, "xOfYPending": { "message": "$1/$2개 보류 중", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "계정" }, - "yourFundsMayBeAtRisk": { - "message": "자금이 위험할 수 있습니다" + "yourActivity": { + "message": "내 활동" + }, + "yourBalance": { + "message": "내 잔액" }, "yourNFTmayBeAtRisk": { "message": "NFT가 위험할 수 있습니다" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index fbace0f2117f..debc2d35640a 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -900,9 +900,6 @@ "nonceField": { "message": "I-customize ang nonce ng transaksyon" }, - "nonceFieldDescription": { - "message": "I-on ito para baguhin ang nonce (numero ng transaksyon) sa mga screen ng kumpirmasyon. Isa itong advanced na feature, gamitin nang may pag-iingat." - }, "nonceFieldHeading": { "message": "Custom na Nonce" }, @@ -1116,13 +1113,10 @@ "securityAndPrivacy": { "message": "Seguridad at Privacy" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Isulat ito at itabi sa maraming tagong lugar." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "I-save sa password manager" + "message": "Isulat ito at itabi sa maraming tagong lugar." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Ilagay sa safe-deposit box." }, "seedPhraseIntroSidebarCopyOne": { diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 232cc255e0bf..3a930315bcbc 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -42,7 +42,7 @@ "message": "Conecte sua carteira de hardware QR" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (em breve)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "O endereço na solicitação de entrada não coincide com o endereço da conta que você está usando para entrar." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Você precisa selecionar uma conta!" }, + "accountTypeNotSupported": { + "message": "Tipo de conta não compatível" + }, "accounts": { "message": "Contas" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Adicionar uma nova conta Ethereum" }, + "addNewBitcoinAccount": { + "message": "Adicionar uma nova conta Bitcoin (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Adicionar uma nova conta Bitcoin (Testnet)" + }, "addNewToken": { "message": "Adicionar novo token" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Adicionar NFTs" }, + "addRpcUrl": { + "message": "Adicionar URL da RPC" + }, "addSnapAccountToggle": { "message": "Ativar \"Adicionar Snap da conta (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "Não consegue encontrar um token? Você pode adicioná-lo manualmente colando seu endereço. Os endereços de contrato do token se encontram em $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Adicionar URL" + }, "addingCustomNetwork": { "message": "Adicionar rede" }, "addingTokens": { "message": "Adicionando tokens" }, + "additionalNetworks": { + "message": "Redes adicionais" + }, + "additionalRpcUrl": { + "message": "URL da RPC adicional" + }, "address": { "message": "Endereço" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Configurações avançadas" }, + "advancedDetailsDataDesc": { + "message": "Dados" + }, + "advancedDetailsHexDesc": { + "message": "Hexadecimal" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Esse é o número de transação de uma conta. O nonce da primeira transação é 0 e aumenta em ordem sequencial." + }, "advancedGasFeeDefaultOptIn": { "message": "Salvar esses valores como padrão para a rede $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Alerta" }, + "alertActionBuy": { + "message": "Comprar ETH" + }, + "alertActionUpdateGas": { + "message": "Atualizar limite de gás" + }, + "alertActionUpdateGasFee": { + "message": "Atualizar taxa" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Atualizar opções de gás" + }, "alertBannerMultipleAlertsDescription": { "message": "Se você aprovar esta solicitação, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, + "alertMessageGasEstimateFailed": { + "message": "Não conseguimos fornecer uma taxa precisa, e essa estimativa pode estar alta. Sugerimos que você informe um limite de gás personalizado, mas há o risco de a transação falhar mesmo assim." + }, + "alertMessageGasFeeLow": { + "message": "Ao escolher uma taxa baixa, a expectativa é de transações mais lentas e tempos de espera maiores. Para transações mais rápidas, escolha as opções de taxa Mercado ou Agressiva." + }, + "alertMessageGasTooLow": { + "message": "Para continuar com essa transação, você precisará aumentar o limite de gás para 21000 ou mais." + }, + "alertMessageInsufficientBalance": { + "message": "Você não tem ETH suficiente em sua conta para pagar as taxas de transação." + }, + "alertMessageNetworkBusy": { + "message": "Os preços do gás são altos e as estimativas são menos precisas." + }, + "alertMessageNoGasPrice": { + "message": "Não podemos prosseguir com essa transação até você atualizar manualmente a taxa." + }, + "alertMessagePendingTransactions": { + "message": "Essa transação não será processada até que a transação anterior seja concluída. Saiba como cancelar ou acelerar uma transação." + }, + "alertMessageSignInDomainMismatch": { + "message": "O site solicitante não é o mesmo em que você está entrando. Isso pode se tratar de uma tentativa de roubar suas credenciais de login." + }, + "alertMessageSignInWrongAccount": { + "message": "Este site está pedindo que você entre usando a conta incorreta." + }, + "alertMessageSigningOrSubmitting": { + "message": "Essa transação só será processada quando sua transação anterior for concluída." + }, "alertModalAcknowledge": { "message": "Eu reconheço o risco e ainda quero prosseguir" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Conferir todos os alertas" }, + "alertReasonGasEstimateFailed": { + "message": "Taxa imprecisa" + }, + "alertReasonGasFeeLow": { + "message": "Velocidade lenta" + }, + "alertReasonGasTooLow": { + "message": "Baixo limite de gás" + }, + "alertReasonInsufficientBalance": { + "message": "Fundos insuficientes" + }, + "alertReasonNetworkBusy": { + "message": "Rede ocupada" + }, + "alertReasonNoGasPrice": { + "message": "Estimativa de taxa indisponível" + }, + "alertReasonPendingTransactions": { + "message": "Transação pendente" + }, + "alertReasonSignIn": { + "message": "Solicitação de entrada suspeita" + }, + "alertReasonWrongAccount": { + "message": "Conta incorreta" + }, "alertSettingsUnconnectedAccount": { "message": "Navegando em um site com uma conta não conectada selecionada" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Todas as permissões" }, + "allTimeHigh": { + "message": "Alta histórica" + }, + "allTimeLow": { + "message": "Baixa histórica" + }, "allYourNFTsOf": { "message": "Todos os seus NFTs de $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 e $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Comunicados" - }, "appDescription": { "message": "Uma carteira de Ethereum no seu navegador", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Opções do ativo" }, "attemptSendingAssets": { - "message": "Se você tentar enviar ativos diretamente de uma rede para outra, isso poderá resultar na perda permanente deles. Certifique-se de usar uma ponte." + "message": "Você poderá perder seus ativos se tentar enviá-los a partir de outra rede. Transfira fundos entre redes com segurança usando uma ponte." }, "attemptSendingAssetsWithPortfolio": { "message": "Você poderá perder seus ativos se tentar enviá-los a partir de outra rede. Transfira os fundos com segurança entre redes usando uma ponte, como $1" @@ -524,11 +629,14 @@ "attemptToCancelSwapForFree": { "message": "Tentar cancelar a troca sem custo" }, + "attributes": { + "message": "Atributos" + }, "attributions": { "message": "Atribuições" }, "auroraRpcDeprecationMessage": { - "message": "O URL de RPC (Chamadas de Procedimento Remoto) da Infura não oferece mais suporte à rede Aurora." + "message": "A URL de RPC (Chamadas de Procedimento Remoto) da Infura não suporta mais Aurora." }, "authorizedPermissions": { "message": "Você concedeu as seguintes permissões" @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "A funcionalidade básica está desativada" }, + "basicConfigurationDescription": { + "message": "A MetaMask oferece recursos básicos como detalhes de token e configurações de gás por meio de serviços de internet. Quando você usa serviços de internet, seu endereço IP é compartilhado, neste caso com a MetaMask. É exatamente igual a quando você visita qualquer site. A MetaMask usa esses dados temporariamente e nunca os vende. Você pode usar uma VPN ou desligar esses serviços, mas isso poderá afetar sua experiência com a MetaMask. Para saber mais, leia nossa $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Funcionalidade básica" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "O MetaMask Beta nunca pedirá sua Frase de Recuperação Secreta." }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Atividades com Bitcoin não são suportadas" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Ativar esse recurso lhe dará a opção de adicionar uma conta Bitcoin à sua extensão da MetaMask derivada de sua Frase de Recuperação Secreta existente. Este é um recurso beta experimental, portanto seu uso será por sua conta e risco. Para nos dar seu feedback sobre esta nova experiência Bitcoin, preencha este $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Ative \"Adicionar uma nova conta Bitcoin (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Ao ativar esse recurso, você terá a opção de adicionar uma conta Bitcoin para a rede de teste." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Ative \"Adicionar uma nova conta Bitcoin (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Conta", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Não é recomendável prosseguir com esta solicitação." + }, "blockaidDescriptionApproveFarming": { "message": "Se você aprovar essa solicitação, algum terceiro conhecido por aplicar golpes poderá tomar todos os seus ativos." }, @@ -669,7 +807,7 @@ "message": "Se você aprovar essa solicitação, alguém poderá roubar seus ativos listados na Blur." }, "blockaidDescriptionErrored": { - "message": "Em razão de um erro, essa solicitação não foi confirmada pelo provedor de segurança. Prossiga com cautela." + "message": "Devido a um erro, não foi possível verificar os alertas de segurança. Prossiga somente se você confiar em todos os endereços envolvidos." }, "blockaidDescriptionMaliciousDomain": { "message": "Você está interagindo com um domínio mal-intencionado. Se você aprovar essa solicitação, poderá perder seus ativos." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Se você aprovar essa solicitação, algum terceiro conhecido por aplicar golpes poderá tomar todos os seus ativos." }, + "blockaidDescriptionWarning": { + "message": "Este pedido pode ser fraudulento. Continue somente se você confiar em todos os endereços envolvidos." + }, "blockaidMessage": { "message": "Proteção de privacidade: nenhum dado é compartilhado com terceiros. Disponível em Arbitrum, Avalanche, BNB Chain, Mainnet da Ethereum, Linea, Optimism, Polygon, Base e Sepolia." }, @@ -690,7 +831,7 @@ "message": "Esta solicitação é enganosa" }, "blockaidTitleMayNotBeSafe": { - "message": "A solicitação pode não ser segura" + "message": "Tenha cautela" }, "blockaidTitleSuspicious": { "message": "Esta solicitação é suspeita" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Comprado para" + }, "bridge": { "message": "Ponte" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Você precisa usar a MetaMask no Google Chrome para se conectar com a sua carteira de hardware." }, + "circulatingSupply": { + "message": "Suprimento em circulação" + }, "clear": { "message": "Limpar" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Clique aqui para adicionar os tokens manualmente." + "message": "Sempre é possível adicionar tokens manualmente." }, "close": { "message": "Fechar" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Nome da coleção" + }, "comboNoOptions": { "message": "Nenhuma opção encontrada", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Confirmo que recebi os alertas e ainda quero prosseguir" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Reconheço o alerta e quero prosseguir mesmo assim" + }, "confirmAlertModalDetails": { "message": "Se você fizer login, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos. Confira os alertas antes de prosseguir." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Confirmar conexão ao $1" }, + "confirmDeletion": { + "message": "Confirmar exclusão" + }, + "confirmFieldPaymaster": { + "message": "Taxa paga por" + }, + "confirmFieldTooltipPaymaster": { + "message": "A taxa dessa transação será paga pelo contrato inteligente do tesoureiro." + }, "confirmPassword": { "message": "Confirmar a senha" }, "confirmRecoveryPhrase": { "message": "Confirmar Frase de Recuperação Secreta" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Só confirme essa transação se você entende integralmente o conteúdo e confia no site solicitante." + "confirmRpcUrlDeletionMessage": { + "message": "Tem certeza de que deseja excluir o URL da RPC? Suas informações não serão salvas para essa rede." + }, + "confirmTitleDescPermitSignature": { + "message": "Este site quer permissão para gastar seus tokens." + }, + "confirmTitleDescSIWESignature": { + "message": "Um site quer que você faça login para comprovar que é titular desta conta." }, "confirmTitleDescSignature": { "message": "Confirme esta mensagem somente se você aprova o conteúdo e confia no site solicitante." }, + "confirmTitlePermitSignature": { + "message": "Solicitação de limite de gastos" + }, + "confirmTitleSIWESignature": { + "message": "Solicitação de entrada" + }, "confirmTitleSignature": { "message": "Solicitação de assinatura" }, @@ -961,7 +1135,7 @@ "message": "Conectado com" }, "connecting": { - "message": "Conectando..." + "message": "Conectando" }, "connectingTo": { "message": "Conectando a $1" @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Criar conta" }, + "creatorAddress": { + "message": "Endereço do criador" + }, "crossChainSwapsLink": { "message": "Faça trocas entre redes com o MetaMask Portfolio" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Dados" }, + "dataCollectionForMarketing": { + "message": "Coleta de dados para marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "Usaremos o MetaMetrics para saber como você interage com nossas comunicações de marketing. Poderemos compartilhar novidades relevantes (como recursos de produtos e outros materiais)." + }, + "dataCollectionWarningPopoverButton": { + "message": "OK" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Você desativou a coleta de dados para fins de marketing. Isso é aplicável apenas a este dispositivo. Se você usa a MetaMask em outros dispositivos, desative-a neles também." + }, "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "dados não disponíveis" + }, + "dateCreated": { + "message": "Data de criação" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Solicitação de descriptografia" }, + "defaultRpcUrl": { + "message": "URL padrão da RPC" + }, "delete": { "message": "Excluir" }, @@ -1311,6 +1509,9 @@ "message": "Excluir rede $1?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Excluir URL da RPC" + }, "deposit": { "message": "Depositar" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Detalhes" }, - "developerOptions": { - "message": "Opções para desenvolvedores" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Redefine o valor booleano de isShown para falso em todos os comunicados. Comunicados são as notificações exibidas na tela pop-up de \"Novidades\"." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Redefine vários estados relacionados a integração e redireciona para a página \"Proteja sua carteira\" da integração." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Resulta no salvamento contínuo de um carimbo de data/hora em session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” está desativado porque não satisfaz o aumento mínimo de 10% em relação à taxa de gás original.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Tempo de processamento desconhecido" }, + "editNetworkLink": { + "message": "editar a rede original" + }, "editNonceField": { "message": "Editar nonce" }, @@ -1552,12 +1744,12 @@ "message": "ativar $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Ativar detecção automática de tokens" - }, "enabled": { "message": "Ativado" }, + "enabledNetworks": { + "message": "Redes habilitadas" + }, "encryptionPublicKeyNotice": { "message": "$1 gostaria da sua chave pública de criptografia. Ao consentir, este site conseguirá redigir mensagens criptografadas para você.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Taxa estimada" }, + "estimatedFeeTooltip": { + "message": "Valor pago para processar a transação na rede." + }, "ethGasPriceFetchWarning": { "message": "O preço de backup do gás é fornecido porque a estimativa de gás principal está indisponível no momento." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Ver no Etherscan" }, + "existingChainId": { + "message": "As informações que você inseriu estão associadas a um ID de cadeia existente." + }, + "existingRpcUrl": { + "message": "Este URL está associado a outro ID de cadeia." + }, "expandView": { "message": "Expandir exibição" }, @@ -1704,7 +1905,7 @@ "message": "Apelidos propostos" }, "externalNameSourcesSettingDescription": { - "message": "Buscaremos os apelidos propostos para os endereços com os quais você interage em fontes terceirizadas como Etherscan, Infura e Lens Protocol. Essas fontes poderão ver esses endereços e seu endereço IP. O endereço da sua conta não será exposto a terceiros." + "message": "Buscaremos os apelidos propostos para os endereços com os quais você interage em fontes terceirizadas como Etherscan, Infura e Lens Protocol. Essas fontes poderão ver esses endereços e o seu endereço IP. O endereço da sua conta não será exposto a terceiros." }, "failed": { "message": "Falha" @@ -1735,6 +1936,9 @@ "message": "A importação de arquivo não está funcionando? Clique aqui!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Encontre a opção correta em:" + }, "flaskWelcomeUninstall": { "message": "você deve desinstalar essa extensão", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Esqueceu a senha?" }, + "form": { + "message": "formulário" + }, "from": { "message": "De" }, @@ -1904,7 +2111,7 @@ "message": "Rede de teste Goerli" }, "gotIt": { - "message": "Entendi!" + "message": "Entendi" }, "grantedToWithColon": { "message": "Concedido a:" @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "alta" }, + "highestCurrentBid": { + "message": "Maior lance atual" + }, + "highestFloorPrice": { + "message": "Maior preço mínimo" + }, "history": { "message": "Histórico" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Essa ação editará os tokens já listados na sua carteira, que podem ser usados para praticar phishing contra você. Só aprove se você tiver certeza de que quer alterar o que esses tokens representam. Saiba mais sobre $1" }, + "l1Fee": { + "message": "Taxa da L1" + }, + "l1FeeTooltip": { + "message": "Taxa de gás da L1" + }, + "l2Fee": { + "message": "Taxa da L2" + }, + "l2FeeTooltip": { + "message": "Taxa de gás da L2" + }, "lastConnected": { "message": "Última conexão" }, - "lastPriceSold": { - "message": "Último preço de venda" - }, "lastSold": { "message": "Última venda" }, @@ -2498,6 +2720,12 @@ "message": "Certifique-se de que ninguém está olhando", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Capitalização de mercado" + }, + "marketDetails": { + "message": "Detalhes do mercado" + }, "max": { "message": "Máximo" }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Taxa máxima" }, + "maxFeeTooltip": { + "message": "Uma taxa máxima fornecida para pagar pela transação." + }, "maxPriorityFee": { "message": "Taxa de prioridade máxima" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Método" }, - "methodDataTransactionDescription": { - "message": "Esta é a ação específica que será realizada. Estes dados podem ser falsos, portanto certifique-se de que confia no site do outro lado." + "methodDataTransactionDesc": { + "message": "Função executada com base nos dados de entrada decodificados." }, "methodNotSupported": { "message": "Não suportado com esta conta." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Métricas" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Sua conta selecionada ($1) é diferente da conta que está tentando assinar ($2)" }, @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "O nome associado a essa rede." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Opções da rede" + }, "networkProvider": { "message": "Provedor de rede" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "“$1” foi adicionado com sucesso!" }, + "newNetworkEdited": { + "message": "“$1” foi editada com sucesso!" + }, "newNftAddedMessage": { "message": "O NFT foi adicionado com sucesso!" }, @@ -2872,8 +3116,11 @@ "nftAlreadyAdded": { "message": "O NFT já foi adicionado." }, + "nftAutoDetectionEnabled": { + "message": "Detecção automática de NFTs ativada" + }, "nftDisclaimer": { - "message": "Aviso: a MetaMask obtém o arquivo de mídia do URL de origem. Às vezes, esse URL é modificado pelo marketplace onde o NFT foi mintado." + "message": "Aviso legal: a MetaMask obtém o arquivo de mídia do URL de origem. Às vezes, esse URL é modificado pelo marketplace onde o NFT foi mintado." }, "nftOptions": { "message": "Opções de NFT" @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Nenhuma resolução fornecida para o domínio." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snaps, e a maioria das carteiras de hardware, não funcionarão com a versão atual do navegador." + }, "noNFTs": { "message": "Nenhum NFT até agora" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Personalizar o nonce da transação" }, - "nonceFieldDescription": { - "message": "Ative essa opção para alterar o nonce (número da transação) nas telas de confirmação. Trata-se de um recurso avançado, por isso use com cuidado." + "nonceFieldDesc": { + "message": "Ative esta opção para alterar o nonce (número da transação) ao enviar ativos. Use com cautela, pois este é um recurso avançado." }, "nonceFieldHeading": { "message": "Nonce personalizado" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 novo token encontrado nesta conta" }, + "numberOfTokens": { + "message": "Número de tokens" + }, "ofTextNofM": { "message": "de" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Ativado" }, - "onboarding": { - "message": "Integração" + "onboardedMetametricsAccept": { + "message": "Concordo" + }, + "onboardedMetametricsDisagree": { + "message": "Não, obrigado" + }, + "onboardedMetametricsKey1": { + "message": "Últimos desenvolvimentos" + }, + "onboardedMetametricsKey2": { + "message": "Recursos de produtos" + }, + "onboardedMetametricsKey3": { + "message": "Outros materiais promocionais relevantes" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Além do $1, gostaríamos de usar dados para entender como você interage com comunicações de marketing.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Isso nos ajuda a personalizar o que compartilhamos com você, como:" + }, + "onboardedMetametricsParagraph3": { + "message": "Lembre-se, nunca vendemos os dados que você fornece e você pode desativar quando quiser." + }, + "onboardedMetametricsTitle": { + "message": "Ajude-nos a melhorar sua experiência" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "O gateway IPFS possibilita acessar e visualizar dados hospedados por terceiros. Você pode adicionar um gateway IPFS personalizado ou continuar usando o padrão." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Quando coletamos as métricas, elas sempre são..." }, - "onboardingMetametricsDisagree": { - "message": "Não, obrigado" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Ajude-nos a melhorar a MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Usaremos esses dados para saber como você interage com nossas comunicações de marketing. Podemos compartilhar novidades relevantes (como recursos de produtos)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Acesso total" }, @@ -3289,6 +3570,22 @@ "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1D", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1M", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1S", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1A", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Explorar snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Os alertas de segurança não estão mais disponíveis nesta rede. Instalar um snap pode melhorar sua segurança." - }, - "openSeaToBlockaidTitle": { - "message": "Atenção!" - }, "operationFailed": { "message": "Falha na operação" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Sites conectados agora são permissões" }, + "permitSimulationDetailInfo": { + "message": "Você está autorizando o consumidor a gastar esta quantidade de tokens de sua conta." + }, "personalAddressDetected": { "message": "Endereço pessoal detectado. Insira o endereço de contrato do token." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Redes personalizadas populares" }, + "popularNetworkAddToolTip": { + "message": "Algumas dessas redes dependem de terceiros. As conexões podem ser menos confiáveis ​​ou permitir que terceiros rastreiem atividades. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfólio" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Anterior" }, + "price": { + "message": "Preço" + }, + "priceUnavailable": { + "message": "preço não disponível" + }, "primaryCurrencySetting": { "message": "Moeda principal" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Taxa de cotação" }, + "rank": { + "message": "Classificação" + }, "reAddAccounts": { "message": "readicione outras contas" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Receber" }, - "receiveTokensCamelCase": { - "message": "Receber tokens" - }, "recipientAddressPlaceholder": { "message": "Insira o endereço público (0x) ou o nome ENS" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Redefinir" }, - "resetStates": { - "message": "Redefinir estados" - }, "resetWallet": { "message": "Redefinir carteira" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Pesquisar contas" }, + "searchNfts": { + "message": "Pesquisar NFTs" + }, "searchTokens": { "message": "Pesquisar tokens" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Proteger minha carteira (recomendado)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Anote e guarde em vários locais secretos." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Salve em um gerenciador de senhas" + "message": "Anote e guarde em vários locais secretos." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Guarde em um cofre de banco." }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Selecionar token" }, "selectNFTPrivacyPreference": { - "message": "Ative a detecção de NFTs nas Configurações" + "message": "Ativar detecção automática de NFTs" }, "selectPathHelp": { - "message": "Se você não vir as contas esperadas, tente alternar o caminho do HD." + "message": "Se as contas que você esperava não forem exibidas, tente mudar o caminho do HD ou a rede atualmente selecionada." }, "selectType": { "message": "Selecione o tipo" @@ -4287,9 +4585,6 @@ "send": { "message": "Enviar" }, - "sendAToken": { - "message": "Enviar um token" - }, "sendBugReport": { "message": "Envie-nos um relatório de bugs." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Rede de teste Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Keep Alive" - }, "setAdvancedPrivacySettingsDetails": { "message": "A MetaMask utiliza esses serviços terceirizados de confiança para aumentar a usabilidade e a segurança dos produtos." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Isso depende de diferentes APIs de terceiros para cada rede, o que expõe seu endereço Ethereum e seu endereço IP." }, + "showLess": { + "message": "Mostrar menos" + }, "showMore": { "message": "Exibir mais" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Assine essa mensagem apenas se entende integralmente o conteúdo e confia no site solicitante." }, - "signatureRequestWarning": { - "message": "Assinar essa mensagem pode ser perigoso. Você pode estar dando controle total da sua conta e ativos à pessoa na outra ponta dessa mensagem. Isso significa que ela pode zerar sua conta a qualquer momento. Prossiga com cautela. $1." - }, "signed": { "message": "Assinado" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Assinando" }, + "signingInWith": { + "message": "Assinando com" + }, "simulationDetailsFailed": { "message": "Houve um erro ao carregar sua estimativa." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Estimar alterações de saldo" }, + "siweIssued": { + "message": "Emitido" + }, + "siweNetwork": { + "message": "Rede" + }, + "siweRequestId": { + "message": "ID da solicitação" + }, + "siweResources": { + "message": "Recursos" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Você está fazendo login em um site e não há alterações previstas em sua conta." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Pular" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Contas controladas por Snaps de terceiros." }, + "snapConnectTo": { + "message": "Conectar-se a $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Permitir conexão automática de $1 com $2 sem a sua aprovação.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 quer usar $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Site" }, + "snapHomeMenu": { + "message": "Menu inicial do Snap" + }, "snapInstallRequest": { "message": "Ao instalar $1, serão dadas as permissões a seguir.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Fonte" }, + "speed": { + "message": "Velocidade" + }, "speedUp": { "message": "Acelerar" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "O limite de gastos está alto demais" }, + "spender": { + "message": "Consumidor" + }, "spendingCap": { "message": "Limite de gastos" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Logs de estado podem conter o seu endereço e transações enviadas da sua conta pública." }, - "states": { - "message": "Estados" - }, "status": { "message": "Status" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Enviada" }, + "suggestedBySnap": { + "message": "Sugerido por $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Nome sugerido:" + }, "suggestedTokenSymbol": { "message": "Símbolo do ticker sugerido:" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Buscando cotações" + "message": "Buscando cotações..." }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm, ocorreu algum erro. Tente novamente. Ou, se os erros persistirem, entre em contato com o Suporte." @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "esta coleção" }, + "threeMonthsAbbreviation": { + "message": "3M", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Hora" }, @@ -5498,45 +5836,6 @@ "message": "Para: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Você está vulnerável a ataques de phishing. Proteja-se desativando eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Se você ativar essa configuração, poderá receber solicitações de assinatura ilegíveis. Ao assinar uma mensagem que não entende, você poderá estar concordando em ceder seus fundos e NFTs." - }, - "toggleEthSignField": { - "message": "Solicitações eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " você pode estar sendo vítima de um golpe" - }, - "toggleEthSignModalBannerText": { - "message": "Se pediram para você ativar essa configuração," - }, - "toggleEthSignModalCheckBox": { - "message": "Entendo que poderei perder todos os meus fundos e NFTs se eu ativar solicitações eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Permitir solicitações eth_sign pode deixar você vulnerável a ataques de phishing. Sempre revise o URL e tenha cuidado ao assinar mensagens que contenham código." - }, - "toggleEthSignModalFormError": { - "message": "O texto está incorreto" - }, - "toggleEthSignModalFormLabel": { - "message": "Insira \"Eu só assino o que eu entendo\" para continuar" - }, - "toggleEthSignModalFormValidation": { - "message": "Eu só assino o que eu entendo" - }, - "toggleEthSignModalTitle": { - "message": "Use por sua conta e risco" - }, - "toggleEthSignOff": { - "message": "DESLIGADO (recomendado)" - }, - "toggleEthSignOn": { - "message": "LIGADO (não recomendado)" - }, "toggleRequestQueueDescription": { "message": "Isso permite que você selecione uma rede para cada site em vez de uma única rede selecionada para todos eles. O recurso evitará que você alterne manualmente entre redes, o que pode atrapalhar sua experiência de usuário em determinados sites." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Endereço de contrato do token" }, + "tokenDecimal": { + "message": "Decimal do token" + }, "tokenDecimalFetchFailed": { "message": "É necessário o decimal do token. Encontre-o em: $1" }, @@ -5580,7 +5882,7 @@ "message": "ID do token" }, "tokenList": { - "message": "Listas de tokens:" + "message": "Listas de tokens" }, "tokenScamSecurityRisk": { "message": "golpes e riscos de segurança envolvendo tokens" @@ -5588,6 +5890,9 @@ "tokenShowUp": { "message": "Seus tokens podem não aparecer automaticamente em sua carteira." }, + "tokenStandard": { + "message": "Padrão de token" + }, "tokenSymbol": { "message": "Símbolo do token" }, @@ -5598,6 +5903,9 @@ "message": "$1 novos tokens encontrados", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Tokens em coleção" + }, "tooltipApproveButton": { "message": "Estou ciente" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Total" }, + "totalVolume": { + "message": "Volume total" + }, "transaction": { "message": "transação" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "Transação criada com valor de $1 às $2." }, + "transactionDataFunction": { + "message": "Função" + }, "transactionDetailDappGasMoreInfo": { "message": "Site sugerido" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Transferir de" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Estamos com problemas para conectar o seu Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "De acordo com nossos registros, este URL não corresponde a um provedor conhecido para este ID de cadeia." + }, "unapproved": { "message": "Não aprovado" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Atualizar" }, + "updateOrEditNetworkInformations": { + "message": "Atualize suas informações ou" + }, "updateRequest": { "message": "Solicitação de atualização" }, "updatedWithDate": { "message": "Atualizado em $1" }, + "uploadDropFile": { + "message": "Solte seu arquivo aqui" + }, + "uploadFile": { + "message": "Fazer upload de arquivo" + }, "urlErrorMsg": { "message": "URLs precisam do prefixo HTTP/HTTPS adequado." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "O que é isso?" }, + "wrongChainId": { + "message": "Este ID de cadeia não corresponde ao nome da rede." + }, + "wrongNetworkName": { + "message": "De acordo com os nossos registros, o nome da rede pode não corresponder a esta ID de cadeia." + }, "xOfYPending": { "message": "$1 de $2 pendente(s)", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Suas contas" }, - "yourFundsMayBeAtRisk": { - "message": "Seus fundos podem estar em risco" + "yourActivity": { + "message": "Sua atividade" + }, + "yourBalance": { + "message": "Seu saldo" }, "yourNFTmayBeAtRisk": { "message": "Seu NFT pode estar em risco" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 688f5758ac26..a4b211bb0529 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -42,7 +42,7 @@ "message": "Carteira de hardware baseada em QR code" }, "QRHardwareWalletSteps2Description": { - "message": "AirGap Vault e Ngrave (em breve)" + "message": "Ngrave Zero" }, "about": { "message": "Sobre" @@ -1363,9 +1363,6 @@ "nonceField": { "message": "Personalizar nonce da transação" }, - "nonceFieldDescription": { - "message": "Ative isso para alterar o nonce (número da transação) nas telas de confirmação. Esse é um recurso avançado; use com cautela." - }, "nonceFieldHeading": { "message": "Nonce personalizado" }, @@ -1702,13 +1699,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Proteger minha carteira (recomendado)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Anote e guarde em vários locais secretos." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Salve em um gerenciador de senhas" + "message": "Anote e guarde em vários locais secretos." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Guarde em um cofre de banco." }, "seedPhraseIntroSidebarCopyOne": { diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index b97ed6f2e67e..f3f948d9a84e 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -42,7 +42,7 @@ "message": "Подключите свой аппаратный кошелек на основе QR-кодов" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (скоро появятся)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Адрес в запросе на вход не соответствует адресу счета, который вы используете для входа." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Вам необходимо выбрать счет!" }, + "accountTypeNotSupported": { + "message": "Тип счета не поддерживается" + }, "accounts": { "message": "Счета" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Добавить новый счет Ethereum" }, + "addNewBitcoinAccount": { + "message": "Добавить новый счет в биткойнах (бета-версия)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Добавить новый счет в биткойнах (тестнет)" + }, "addNewToken": { "message": "Добавить новый токен" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Добавить NFT" }, + "addRpcUrl": { + "message": "Добавить URL-адрес RPC" + }, "addSnapAccountToggle": { "message": "Включите «Добавить Snap счета (бета)»" }, @@ -305,12 +317,21 @@ "message": "Не можете найти токен? Можно вручную добавить любой токен, вставив его адрес. Адреса контракта токена можно найти на $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Добавить URL-адрес" + }, "addingCustomNetwork": { "message": "Добавление сети" }, "addingTokens": { "message": "Добавление токенов" }, + "additionalNetworks": { + "message": "Дополнительные сети" + }, + "additionalRpcUrl": { + "message": "Дополнительный URL-адрес RPC" + }, "address": { "message": "Адрес" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Дополнительная конфигурация" }, + "advancedDetailsDataDesc": { + "message": "Данные" + }, + "advancedDetailsHexDesc": { + "message": "Шестнадцатиричные" + }, + "advancedDetailsNonceDesc": { + "message": "Одноразовый код" + }, + "advancedDetailsNonceTooltip": { + "message": "Это номер транзакции счета. Одноразовый код для первой транзакции равен 0 и увеличивается в последовательном порядке." + }, "advancedGasFeeDefaultOptIn": { "message": "Сохраните эти значения как значения по умолчанию для сети $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Оповещение" }, + "alertActionBuy": { + "message": "Купить ETH" + }, + "alertActionUpdateGas": { + "message": "Обновить лимит газа" + }, + "alertActionUpdateGasFee": { + "message": "Изменить комиссию" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Обновить параметры газа" + }, "alertBannerMultipleAlertsDescription": { "message": "Если вы одобрите этот запрос, третья сторона, которая, как известно, совершала мошеннические действия, может похитить все ваши активы." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Это можно изменить в разделе «Настройки» > «Оповещения»" }, + "alertMessageGasEstimateFailed": { + "message": "Мы не можем указать точную сумму комиссии, и эта оценка может быть высокой. Мы предлагаем вам ввести индивидуальный лимит газа, но существует риск, что транзакция все равно не удастся." + }, + "alertMessageGasFeeLow": { + "message": "При выборе низкой комиссии транзакций будут медленнее, а время ожидания — дольше. Для более быстрых транзакций выберите вариант комиссии «Рыночная» или «Агрессивная»." + }, + "alertMessageGasTooLow": { + "message": "Чтобы продолжить эту транзакцию, вам необходимо увеличить лимит газа до 21 000 или выше." + }, + "alertMessageInsufficientBalance": { + "message": "На вашем счету недостаточно ETH для оплаты комиссий за транзакцию." + }, + "alertMessageNetworkBusy": { + "message": "Цены газа высоки, а оценки менее точны." + }, + "alertMessageNoGasPrice": { + "message": "Мы не сможем продолжить эту транзакцию, пока вы не обновите комиссию вручную." + }, + "alertMessagePendingTransactions": { + "message": "Эта транзакция не будет выполнена, пока не завершится предыдущая транзакция. Узнайте, как отменить или ускорить транзакцию." + }, + "alertMessageSignInDomainMismatch": { + "message": "Сайт, отправляющий запрос, не является сайтом, на который вы входите. Это может быть попыткой украсть ваши учетные данные." + }, + "alertMessageSignInWrongAccount": { + "message": "Этот сайт просит вас войти в систему, используя неправильный счет." + }, + "alertMessageSigningOrSubmitting": { + "message": "Эта транзакция будет выполнена только после завершения вашей предыдущей транзакции." + }, "alertModalAcknowledge": { "message": "Я осознал(-а) риск и все еще хочу продолжить" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Просмотреть все оповещения" }, + "alertReasonGasEstimateFailed": { + "message": "Неточная комиссия" + }, + "alertReasonGasFeeLow": { + "message": "Низкая скорость" + }, + "alertReasonGasTooLow": { + "message": "Низкий лимит газа" + }, + "alertReasonInsufficientBalance": { + "message": "Недостаточно средств" + }, + "alertReasonNetworkBusy": { + "message": "Сеть занята" + }, + "alertReasonNoGasPrice": { + "message": "Оценка комиссии недоступна" + }, + "alertReasonPendingTransactions": { + "message": "Ожидается транзакция" + }, + "alertReasonSignIn": { + "message": "Подозрительный запрос на вход" + }, + "alertReasonWrongAccount": { + "message": "Неверный счет" + }, "alertSettingsUnconnectedAccount": { "message": "Просмотр веб-сайта с выбранным неподключенным счетом" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Все разрешения" }, + "allTimeHigh": { + "message": "Максимум за все время" + }, + "allTimeLow": { + "message": "Минимум за все время" + }, "allYourNFTsOf": { "message": "Все ваши NFT из $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 и $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Объявления" - }, "appDescription": { "message": "Кошелек Ethereum в вашем браузере", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Параметры актива" }, "attemptSendingAssets": { - "message": "Попытка отправки активов напрямую из одной сети в другую может привести к необратимой потере активов. Следует использовать мост." + "message": "Вы можете потерять свои активы, если попытаетесь отправить их из другой сети. Безопасно переводите средства между сетями с помощью моста." }, "attemptSendingAssetsWithPortfolio": { "message": "Попытка отправки активов напрямую из одной сети в другую может привести к необратимой потере активов. Следует использовать мост, такой как $1" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Бесплатная попытка отменить своп" }, + "attributes": { + "message": "Атрибуты" + }, "attributions": { "message": "Атрибуции" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Базовый функционал отключен" }, + "basicConfigurationDescription": { + "message": "MetaMask предлагает базовые функции, такие как сведения о токенах и настройки газа, через интернет-службы. Когда вы пользуетесь интернет-службами, ваш IP-адрес передается, в данном случае MetaMask. Это похоже на посещенеи вами любого веб-сайта. MetaMask временно использует ваши данные и никогда не продает их. Вы можете использовать VPN или отключить эти службы, но это может негативно повлиять на вашу работу с MetaMask. Чтобы узнать больше, прочитайте нашу $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Базовый функционал" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "Бета-версия MetaMask никогда не запрашивает у вас секретную фразу для восстановления." }, + "billionAbbreviation": { + "message": "Б", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Активность биткойна не поддерживается" + }, + "bitcoinSupportSectionTitle": { + "message": "Биткойн" + }, + "bitcoinSupportToggleDescription": { + "message": "Включение этой функции даст вам возможность добавить в ваше расширение MetaMask счет для биткойнов, созданный на основе вашей существующей секретной фразы для восстановления. Это экспериментальная бета-функция, которая используется на свой страх и риск. Пожалуйста, оставьте нам отзыв об этом новом способе взаимодействия с биткойном, заполнив эту $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Включить «Добавить новый счет в биткойнах (бета-версия)»" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Включение этой функции даст вам возможность добавить счет в биткойнах для тестнета." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Включить «Добавить новый счет в биткойнах (тестнет)»" + }, "blockExplorerAccountAction": { "message": "Счет", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Мы не рекомендуем продолжать выполнение этого запроса." + }, "blockaidDescriptionApproveFarming": { "message": "Если вы одобрите этот запрос, третья сторона, известная мошенничеством, может похитить все ваши активы." }, @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Если вы одобрите этот запрос, третья сторона, известная мошенничеством, похитит все ваши активы." }, + "blockaidDescriptionWarning": { + "message": "Это может быть обманный запрос. Продолжайте, только если вы доверяете каждому задействованному адресу." + }, "blockaidMessage": { "message": "Сохранение конфиденциальности – никакие данные не передаются третьим сторонам. Доступно в Arbitrum, Avalanche, BNB Chain, Мейн-нете Ethereum, Linea, Optimism, Polygon, Base и Sepolia." }, @@ -690,7 +831,7 @@ "message": "Это запрос с целью обмана" }, "blockaidTitleMayNotBeSafe": { - "message": "Запрос может быть небезопасным" + "message": "Будьте осторожны" }, "blockaidTitleSuspicious": { "message": "Это подозрительный запрос" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Куплено за" + }, "bridge": { "message": "Мост" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Вам необходимо использовать MetaMask в Google Chrome, чтобы подключиться к аппаратному кошельку." }, + "circulatingSupply": { + "message": "Циркулирующее предложение" + }, "clear": { "message": "Очистить" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Нажмите здесь, чтобы вручную добавить токены." + "message": "Вы также можете добавлять токены вручную." }, "close": { "message": "Закрыть" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Имя коллекции" + }, "comboNoOptions": { "message": "Вариантов не найдено", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Я подтвердил(-а) получение оповещений и все еще хочу продолжить" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Я подтвердил(-а) получение предупреждения и все еще хочу продолжить" + }, "confirmAlertModalDetails": { "message": "Если вы войдете, третья сторона, которая, как известно, совершала мошеннические действия, может похитиь твсе ваши активы. Прежде чем продолжить, просмотрите оповещения." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Подтвердите подключение к $1" }, + "confirmDeletion": { + "message": "Подтвердить удаление" + }, + "confirmFieldPaymaster": { + "message": "Комиссия оплачена" + }, + "confirmFieldTooltipPaymaster": { + "message": "Комиссия за эту транзакцию будет оплачена смарт-контрактом paymaster." + }, "confirmPassword": { "message": "Подтвердить пароль" }, "confirmRecoveryPhrase": { "message": "Подтвердите секретную фразу для восстановления" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Подтверждайте эту транзакцию только в том случае, если вы полностью понимаете ее содержание и доверяете запрашивающему сайту." + "confirmRpcUrlDeletionMessage": { + "message": "Уверены, что хотите удалить URL-адрес RPC? Ваша информация не будет сохранена для этой сети." + }, + "confirmTitleDescPermitSignature": { + "message": "Этот сайт хочет получить разрешение на расходование ваших токенов." + }, + "confirmTitleDescSIWESignature": { + "message": "Сайт требует, чтобы вы вошли в систему и доказали, что вы являетесь владельцем этого счета." }, "confirmTitleDescSignature": { "message": "Подтверждайте это сообщение только в том случае, если вы одобряете его содержание и доверяете запрашивающему сайту." }, + "confirmTitlePermitSignature": { + "message": "Запрос лимита расходов" + }, + "confirmTitleSIWESignature": { + "message": "Запрос на вход" + }, "confirmTitleSignature": { "message": "Запрос подписи" }, @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Создать счет" }, + "creatorAddress": { + "message": "Адрес автора" + }, "crossChainSwapsLink": { "message": "Меняйте сети с помощью MetaMask Portfolio" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Данные" }, + "dataCollectionForMarketing": { + "message": "Сбор данных для маркетинга" + }, + "dataCollectionForMarketingDescription": { + "message": "Мы будем использовать MetaMetrics, чтобы узнать, как вы взаимодействуете с нашими маркетинговыми сообщениями. Мы можем делиться соответствующими новостями (например, новостями о функциях продукта и другими материалами)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Хорошо" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Вы отключили сбор данных для наших маркетинговых целей. Это касается только этого устройства. Если вы используете MetaMask на других устройствах, обязательно отключите его и там." + }, "dataHex": { "message": "Шестнадцатиричные" }, + "dataUnavailable": { + "message": "данные недоступны" + }, + "dateCreated": { + "message": "Дата создания" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Расшифровать запрос" }, + "defaultRpcUrl": { + "message": "URL-адрес RPC по умолчанию" + }, "delete": { "message": "Удалить" }, @@ -1311,6 +1509,9 @@ "message": "Удалить сеть $1?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Удалить URL-адрес RPC" + }, "deposit": { "message": "Депозит" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Подробности" }, - "developerOptions": { - "message": "Параметры разработчика" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Сбрасывает логическое значение isShown в значение false для всех объявлений. Объявления — это уведомления, отображаемые во всплывающем модальном окне «Что нового»." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Сбрасывает различные состояния, связанные с регистрацией, и перенаправляет на страницу регистрации «Защитите свой кошелек»." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "В результате отметка времени постоянно сохраняется в session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "$1 отключена, поскольку не соответствует минимальному увеличению на 10% от первоначальной платы за газ.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Время обработки неизвестно" }, + "editNetworkLink": { + "message": "изменить исходную сеть" + }, "editNonceField": { "message": "Изменить одноразовый номер" }, @@ -1552,12 +1744,12 @@ "message": "активирует для $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Включите автоопределение токена" - }, "enabled": { "message": "Включено" }, + "enabledNetworks": { + "message": "Включенные сети" + }, "encryptionPublicKeyNotice": { "message": "$1 запрашивает ваш открытый ключ шифрования. После получения вашего согласия на это данный сайт сможет создавать зашифрованные сообщения для отправки в ваш адрес.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Примерная комиссия" }, + "estimatedFeeTooltip": { + "message": "Сумма, уплаченная за обработку транзакции в сети." + }, "ethGasPriceFetchWarning": { "message": "Указана резервная цена газа, поскольку основной сервис определения цены газа сейчас недоступен." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Посмотреть на Etherscan" }, + "existingChainId": { + "message": "Введенная вами информация связана с существующим ID блокчейна." + }, + "existingRpcUrl": { + "message": "Этот URL-адрес связан с другим ID блокчейна." + }, "expandView": { "message": "Развернуть представление" }, @@ -1735,6 +1936,9 @@ "message": "Импорт файлов не работает? Нажмите здесь!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Найдите подходящий в:" + }, "flaskWelcomeUninstall": { "message": "вам нужно должны удалить это расширение", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Забыли пароль?" }, + "form": { + "message": "форма" + }, "from": { "message": "От" }, @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "высокая" }, + "highestCurrentBid": { + "message": "Самое высокое текущее предложение" + }, + "highestFloorPrice": { + "message": "Самая высокая минимальная цена" + }, "history": { "message": "История" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Это действие изменит токены, уже указанные в вашем кошельке, которые можно использовать для фишинга. Утверждайте, только если вы уверены, что хотите изменить то, что представляют эти токены. Узнайте подробнее о $1" }, + "l1Fee": { + "message": "Комиссия L1" + }, + "l1FeeTooltip": { + "message": "Плата за газ L1" + }, + "l2Fee": { + "message": "Комиссия L2" + }, + "l2FeeTooltip": { + "message": "Плата за газ L2" + }, "lastConnected": { "message": "Последнее подключение" }, - "lastPriceSold": { - "message": "Последняя цена продажи" - }, "lastSold": { "message": "Последняя продажа" }, @@ -2498,6 +2720,12 @@ "message": "Убедитесь, что никто не смотрит", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Рыночная капитализация" + }, + "marketDetails": { + "message": "Сведения о рынке" + }, "max": { "message": "Макс." }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Максимальная комиссия" }, + "maxFeeTooltip": { + "message": "Максимальная комиссия, предусмотренная для оплаты транзакции." + }, "maxPriorityFee": { "message": "Максимальная плата за приоритет" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Метод" }, - "methodDataTransactionDescription": { - "message": "Это конкретные действия, которые будут предприняты. Эти данные могут быть подделаны, поэтому убедитесь, что вы доверяете сайту на другом конце." + "methodDataTransactionDesc": { + "message": "Функция выполняется на основе декодированных входных данных." }, "methodNotSupported": { "message": "Не поддерживается с этим счётом." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Показатели" }, + "millionAbbreviation": { + "message": "млн", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Выбранный вами счет ($1) отличается от счета, который вы пытаетесь подписать ($2)" }, @@ -2675,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Нативный токен этой сети — $1. Этот токен используется для внесения платы за газ.", + "message": "Нативный токен этой сети — $1. Этот токен используется для внесения платы за газ. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Биткоин" + }, "networkNameDefinition": { "message": "Имя, связанное с этой сетью." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Параметры сети" + }, "networkProvider": { "message": "Поставщик услуг сети" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "«$1» успешно добавлен!" }, + "newNetworkEdited": { + "message": "«$1» успешно изменен!" + }, "newNftAddedMessage": { "message": "NFT успешно добавлен!" }, @@ -2872,6 +3116,9 @@ "nftAlreadyAdded": { "message": "NFT уже добавлен." }, + "nftAutoDetectionEnabled": { + "message": "Автоопределение NFT включено" + }, "nftDisclaimer": { "message": "Отказ от ответственности: MetaMask извлекает медиафайл из исходного URL. Этот URL иногда изменяется торговой площадкой, на которой был выполнен минтинг NFT." }, @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Разрешение для домена не предоставлено." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snaps и большинство аппаратных кошельков не будут работать с вашей текущей версией браузера." + }, "noNFTs": { "message": "Пока нет NFT" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "Настроить одноразовый номер транзакции" }, - "nonceFieldDescription": { - "message": "Включите это, чтобы изменить одноразовый номер (номер транзакции) на экранах подтверждения. Это продвинутая функция, используйте ее с осторожностью." + "nonceFieldDesc": { + "message": "Включите это, чтобы изменить одноразовый номер (номер транзакции) при отправке активов. Это продвинутая функция, используйте ее с осторожностью." }, "nonceFieldHeading": { "message": "Пользовательский одноразовый номер" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 новый токен найден в этом счете" }, + "numberOfTokens": { + "message": "Количество токенов" + }, "ofTextNofM": { "message": "из" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Вкл." }, - "onboarding": { - "message": "Регистрация" + "onboardedMetametricsAccept": { + "message": "Я согласен(-на)" + }, + "onboardedMetametricsDisagree": { + "message": "Нет, спасибо" + }, + "onboardedMetametricsKey1": { + "message": "Последние разработки" + }, + "onboardedMetametricsKey2": { + "message": "Особенности продукта" + }, + "onboardedMetametricsKey3": { + "message": "Другие соответствующие промоматериалы" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics," + }, + "onboardedMetametricsParagraph1": { + "message": "Кроме $1, мы хотели бы использовать данные, чтобы понять, как вы взаимодействуете с рекламными сообщениями.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Это помогает нам персонализировать то, чем мы делимся с вами, например:" + }, + "onboardedMetametricsParagraph3": { + "message": "Помните, что мы никогда не продаем предоставленные вами данные и вы можете отказаться от их предоставления в любое время." + }, + "onboardedMetametricsTitle": { + "message": "Помогите нам улучшить ваш опыт" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Шлюз IPFS позволяет получать доступ к данным, размещенным третьими сторонами, и просматривать их. Вы можете добавить пользовательский шлюз IPFS или продолжить использовать шлюз по умолчанию." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Когда мы собираем показатели, они всегда будут..." }, - "onboardingMetametricsDisagree": { - "message": "Нет, спасибо" - }, "onboardingMetametricsInfuraTerms": { "message": "Мы сообщим вам, если решим использовать эти данные для других целей. Вы можете ознакомиться с нашей $1 для получения дополнительной информации. Помните, что вы можете перейти в настройки и отказаться в любой момент.", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Помогите нам улучшить MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Мы будем использовать эти данные, чтобы узнать, как вы взаимодействуете с нашими маркетинговыми сообщениями. Мы можем отправлять соответствующие новости (например, об особенностях продукта)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Полный доступ" }, @@ -3289,6 +3570,22 @@ "message": "Оповещения об обнаружении фишинга зависят от связи с $1. jsDeliver получит доступ к вашему IP-адресу. Посмотрите $ 2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 Д", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 М", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 Н", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 Г", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Обзор Snaps" - }, - "openSeaToBlockaidDescription": { - "message": "Оповещения безопасности больше не доступны в этой сети. Установка Snap может повысить вашу безопасность." - }, - "openSeaToBlockaidTitle": { - "message": "Внимание!" - }, "operationFailed": { "message": "Операция не удалась" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Подключенные сайты теперь имеют разрешения" }, + "permitSimulationDetailInfo": { + "message": "Вы даёте расходующему лицу разрешение потратить именно столько токенов из вашего аккаунта" + }, "personalAddressDetected": { "message": "Обнаружен личный адрес. Введите адрес контракта токена." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Популярные пользовательские сети" }, + "popularNetworkAddToolTip": { + "message": "Некоторые из этих сетей являются зависимыми от третьих сторон. Соединения могут быть менее надежными или позволять третьим сторонам отслеживать активность. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfolio" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Пред." }, + "price": { + "message": "Цена" + }, + "priceUnavailable": { + "message": "цена недоступна" + }, "primaryCurrencySetting": { "message": "Основная валюта" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Курс котировки" }, + "rank": { + "message": "Рейтинг" + }, "reAddAccounts": { "message": "повторно добавить любые другие счета" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Получить" }, - "receiveTokensCamelCase": { - "message": "Получить токены" - }, "recipientAddressPlaceholder": { "message": "Введите публичный адрес (0x) или имя ENS" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Сбросить" }, - "resetStates": { - "message": "Сбросить состояния" - }, "resetWallet": { "message": "Сбросить кошелек" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Поиск счетов" }, + "searchNfts": { + "message": "Поиск NFT" + }, "searchTokens": { "message": "Поиск токенов" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Защитить мой кошелек (рекомендуется)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Запишите и храните в нескольких секретных местах." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Сохранить в менджере паролей" + "message": "Запишите и храните в нескольких секретных местах." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Храните в банковской ячейке." }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Выбрать токен" }, "selectNFTPrivacyPreference": { - "message": "Включите определение NFT в настройках" + "message": "Включите автоопределение NFT" }, "selectPathHelp": { - "message": "Если вы не видите ожидаемые счета, попробуйте переключиться на путь HD." + "message": "Если вы не видите ожидаемые счета, попробуйте переключиться на путь HD или текущую выбранную сеть." }, "selectType": { "message": "Выбрать тип" @@ -4287,9 +4585,6 @@ "send": { "message": "Отправить" }, - "sendAToken": { - "message": "Отправить токен" - }, "sendBugReport": { "message": "Отправьте нам отчет об ошибке." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Тестовая сеть Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Поддерживать активным сервисный модуль" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask использует эти доверенные сторонние сервисы для повышения удобства использования и безопасности продукта." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Это базируется на различных сторонних API для каждой сети, которые раскрывают ваш адрес Ethereum и ваш IP-адрес." }, + "showLess": { + "message": "Показать меньше" + }, "showMore": { "message": "Показать больше" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Подписывайте это сообщение только в том случае, если вы полностью понимаете его содержание и доверяете запрашивающему сайту." }, - "signatureRequestWarning": { - "message": "Подписание этого сообщения может быть опасным. Возможно, вы предоставляете полный контроль над своим счетом и активами стороне на другом конце этого сообщения. Это означает, что она может опустошить ваш счет в любое время. Действуйте с осторожностью. $1." - }, "signed": { "message": "Подписано" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "Подписание" }, + "signingInWith": { + "message": "Вход с помощью" + }, "simulationDetailsFailed": { "message": "Не удалось загрузить прогноз." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Спрогнозировать изменения баланса" }, + "siweIssued": { + "message": "Выдано" + }, + "siweNetwork": { + "message": "Сеть" + }, + "siweRequestId": { + "message": "ID запроса" + }, + "siweResources": { + "message": "Ресурсы" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Вы входите на сайт, и в вашем счете не происходит никаких прогнозируемых изменений." + }, + "siweURI": { + "message": "URL-адрес" + }, "skip": { "message": "Пропустить" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Счета, контролируемые сторонними Snap." }, + "snapConnectTo": { + "message": "Подключиться к $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Позвольте $1 автоматически подключаться к $2 без вашего одобрения.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 хочет использовать $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Веб-сайт" }, + "snapHomeMenu": { + "message": "Главное меню Snap" + }, "snapInstallRequest": { "message": "Установка $1 дает ему следующие разрешения.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Источник" }, + "speed": { + "message": "Скорость" + }, "speedUp": { "message": "Ускорить" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Лимит расходов слишком велик" }, + "spender": { + "message": "Расходующее лицо" + }, "spendingCap": { "message": "Лимит расходов" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Журналы состояния содержат ваши адреса публичных счетов и отправленные транзакции." }, - "states": { - "message": "Состояния" - }, "status": { "message": "Статус" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Отправлено" }, + "suggestedBySnap": { + "message": "Рекомендовано $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Предлагаемое имя:" + }, "suggestedTokenSymbol": { "message": "Рекомендуемый символ-тикер:" }, @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Вы переключились на" + "message": "Сейчас вы используете" }, "switchingNetworksCancelsPendingConfirmations": { "message": "В случае смены сетей все ожидающие подтверждения будут отменены" @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "эта коллекция" }, + "threeMonthsAbbreviation": { + "message": "3 М", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Время" }, @@ -5498,45 +5836,6 @@ "message": "Адресат: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Вы подвержены риску фишинговых атак. Защитите себя, отключив eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Если вы включите этот параметр, вы можете получать запросы подписи, которые невозможно прочитать. Подписав сообщение, которое вы не понимаете, вы можете согласиться отдать свои средства и NFT." - }, - "toggleEthSignField": { - "message": "Запросы eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " вас могут обмануть" - }, - "toggleEthSignModalBannerText": { - "message": "Если вас попросили включить этот параметр," - }, - "toggleEthSignModalCheckBox": { - "message": "Я понимаю, что могу потерять все свои средства и NFT, если включу запросы eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Разрешение запросов eth_sign может сделать вас уязвимыми для фишинговых атак. Всегда проверяйте URL и будьте осторожны при подписании сообщений, содержащих код." - }, - "toggleEthSignModalFormError": { - "message": "Текст неверный" - }, - "toggleEthSignModalFormLabel": { - "message": "Введите «Я подписываю только то, что понимаю», чтобы продолжить." - }, - "toggleEthSignModalFormValidation": { - "message": "Я подписываю только то, что понимаю" - }, - "toggleEthSignModalTitle": { - "message": "Используйте на свой риск" - }, - "toggleEthSignOff": { - "message": "ВЫКЛ. (рекомендуется)" - }, - "toggleEthSignOn": { - "message": "ВКЛ. (не рекомендуется)" - }, "toggleRequestQueueDescription": { "message": "Это позволяет вам выбрать сеть для каждого сайта вместо одной выбранной сети для всех сайтов. Эта функция не позволит вам переключать сети вручную, что может снизить удобство использования некоторых сайтов." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Адрес контракта токена" }, + "tokenDecimal": { + "message": "Число десятичных знаков токена" + }, "tokenDecimalFetchFailed": { "message": "Требуется десятичный токен. Найдите его здесь: $1" }, @@ -5586,7 +5888,10 @@ "message": "мошенничество с токенами и угрозы безопасности" }, "tokenShowUp": { - "message": "Ваши токены могут не отображаться автоматически в вашем кошельке." + "message": "Ваши токены могут не отображаться автоматически в вашем кошельке. " + }, + "tokenStandard": { + "message": "Стандарт токена" }, "tokenSymbol": { "message": "Символ токена" @@ -5598,6 +5903,9 @@ "message": "Найдены $1 новых токена(-ов)", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Токены в коллекции" + }, "tooltipApproveButton": { "message": "Я понимаю" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Итого" }, + "totalVolume": { + "message": "Общий объем" + }, "transaction": { "message": "транзакция" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "В $2 создана транзакция на сумму $1." }, + "transactionDataFunction": { + "message": "Функция" + }, "transactionDetailDappGasMoreInfo": { "message": "Рекомендовано сайтом" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Перевести из" }, + "trillionAbbreviation": { + "message": "трлн", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "У нас возникли проблемы с подключением вашего Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Согласно нашим данным, этот URL-адрес не соответствует известному поставщику для этого ID блокчейна." + }, "unapproved": { "message": "Не одобрен" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Обновить" }, + "updateOrEditNetworkInformations": { + "message": "Обновите свою информацию или" + }, "updateRequest": { "message": "Запрос обновления" }, "updatedWithDate": { "message": "Обновлено $1" }, + "uploadDropFile": { + "message": "Переместите свой файл сюда" + }, + "uploadFile": { + "message": "Загрузить файл" + }, "urlErrorMsg": { "message": "URL должен иметь соответствующий префикс HTTP/HTTPS." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Что это?" }, + "wrongChainId": { + "message": "Этот ID блокчейна не соответствует имени сети." + }, + "wrongNetworkName": { + "message": "Согласно нашим записям, имя сети может не соответствовать этому ID блокчейна." + }, "xOfYPending": { "message": "$1 из $2 ожидающих", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Ваши счета" }, - "yourFundsMayBeAtRisk": { - "message": "Ваши средства могут быть в опасности" + "yourActivity": { + "message": "Ваша деятельность" + }, + "yourBalance": { + "message": "Ваш баланс" }, "yourNFTmayBeAtRisk": { "message": "Ваш NFT могут быть в опасности" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index f354b2070f3c..7eecf1acfd13 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -42,7 +42,7 @@ "message": "Ikonekta ang iyong QR na wallet na hardware" }, "QRHardwareWalletSteps2Description": { - "message": "AirGap Vault at Ngrave (paparating na)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Ang address sa kahilingan sa pag-sign in ay hindi tugma sa address ng account na ginagamit mo sa pag-sign in." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Kailangan mong pumili ng account!" }, + "accountTypeNotSupported": { + "message": "Hindi suportado ang uri ng account" + }, "accounts": { "message": "Mga Account" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Magdagdag ng bagong account sa Ethereum" }, + "addNewBitcoinAccount": { + "message": "Magdagdag ng bagong account sa Bitcoin (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Magdagdag ng bagong account sa Bitcoin (Testnet)" + }, "addNewToken": { "message": "Magdagdag ng bagong token" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Magdagdag ng mga NFT" }, + "addRpcUrl": { + "message": "Magdagdag ng RPC URL" + }, "addSnapAccountToggle": { "message": "Paganahin ang \"Idagdag ang account Snap (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "Hindi makahanap ng token? Maaari kang manu-manong magdagdag ng anumang token sa pamamagitan ng pag-paste ng address nito. Ang mga address ng kontrata ng token ay matatagpuan sa $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Idagdag ang URL" + }, "addingCustomNetwork": { "message": "Nagdaragdag ng Network" }, "addingTokens": { "message": "Nagdaragdag ng mga token" }, + "additionalNetworks": { + "message": "Mga karagdagang network" + }, + "additionalRpcUrl": { + "message": "Karagdagang RPC URL" + }, "address": { "message": "Address" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Advanced na pagsasaayos" }, + "advancedDetailsDataDesc": { + "message": "Data" + }, + "advancedDetailsHexDesc": { + "message": "Hex" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Ito ang numero ng transaksyon ng account. Ang nonce para sa unang transaksyon ay 0 at nadaragdagan ito sa sunod-sunod na pagkakaayos." + }, "advancedGasFeeDefaultOptIn": { "message": "I-save ang mga value na ito bilang default ko para sa $1 network.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Alerto" }, + "alertActionBuy": { + "message": "Bumili ng ETH" + }, + "alertActionUpdateGas": { + "message": "I-update ang gas limit" + }, + "alertActionUpdateGasFee": { + "message": "I-update ang bayad" + }, + "alertActionUpdateGasFeeLevel": { + "message": "I-update ang mga opsyon sa gas" + }, "alertBannerMultipleAlertsDescription": { "message": "Kung aaprubahan mo ang kahilingang ito, maaaring kunin ng third party na kilala sa mga panloloko ang lahat asset mo." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Puwede itong baguhin sa \"Mga Setting > Mga Alerto\"" }, + "alertMessageGasEstimateFailed": { + "message": "Hindi kami makapagbigay ng tumpak na bayad at ang pagtantiya na ito ay maaaring mataas. Iminumungkahi namin na maglagay ka ng naka-custom na gas limit, ngunit may panganib na mabigo pa rin ang transaksyon." + }, + "alertMessageGasFeeLow": { + "message": "Kapag pumipili ng mababang bayad, asahan ang mas mabagal na mga transaksyon at mas matagal na paghihintay. Para sa mas mabilis na transaksyon, piliin ang Market o Aggressive na mga pagpipilian sa bayad." + }, + "alertMessageGasTooLow": { + "message": "Para magpatuloy sa transaksyong ito, kakailanganin mong dagdagan ang gas limit sa 21000 o mas mataas." + }, + "alertMessageInsufficientBalance": { + "message": "Wala kang sapat na ETH sa iyong account para bayaran ang mga bayad sa transaksyon." + }, + "alertMessageNetworkBusy": { + "message": "Ang mga presyo ng gas ay mataas at ang pagtantiya ay hindi gaanong tumpak." + }, + "alertMessageNoGasPrice": { + "message": "Hindi tayo makakapagpatuloy sa transaksyong ito hanggang sa manwal mong i-update ang bayad." + }, + "alertMessagePendingTransactions": { + "message": "Hindi magpapatuloy ang transaksyong ito hanggang makumpleto ang naunang transaksyon. Alamin kung paano kanselahin o pabilisin ang transaksyon." + }, + "alertMessageSignInDomainMismatch": { + "message": "Ang site na humihiling ay hindi ang site kung saan ka nagsa-signin. Ito ay maaring isang pagtatangka para nakawin ang iyong mga kredensiyal sa pag-login." + }, + "alertMessageSignInWrongAccount": { + "message": "Hinihingi sa iyo ng site na ito na mag-sign in gamit ang maling account." + }, + "alertMessageSigningOrSubmitting": { + "message": "Magpapatuloy lamang ang transaksyong ito kapag nakumpleto mo na ang naunang transaksyon." + }, "alertModalAcknowledge": { "message": "Kinikilala ko ang panganib at nais ko pa rin magpatuloy" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Suriin ang lahat ng alerto" }, + "alertReasonGasEstimateFailed": { + "message": "Hindi tumpak na bayad" + }, + "alertReasonGasFeeLow": { + "message": "Mabagal na bilis" + }, + "alertReasonGasTooLow": { + "message": "Mababang gas limit" + }, + "alertReasonInsufficientBalance": { + "message": "Hindi sapat ang pondo" + }, + "alertReasonNetworkBusy": { + "message": "Busy ang network" + }, + "alertReasonNoGasPrice": { + "message": "Hindi available ang pagtantiya sa bayad" + }, + "alertReasonPendingTransactions": { + "message": "Nakabinbin na transaksyon" + }, + "alertReasonSignIn": { + "message": "Kahina-hinalang hiling na pag-signin" + }, + "alertReasonWrongAccount": { + "message": "Maling account" + }, "alertSettingsUnconnectedAccount": { "message": "Nagba-browse sa isang website na may napiling hindi konektadong account" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Lahat ng Pahintulot" }, + "allTimeHigh": { + "message": "All time high" + }, + "allTimeLow": { + "message": "All time low" + }, "allYourNFTsOf": { "message": "Lahat ng iyong NFT mula sa $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 at $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Mga Anunsiyo" - }, "appDescription": { "message": "Ethereum Wallet sa iyong Browser", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Mga opsyon ng asset" }, "attemptSendingAssets": { - "message": "Kung tatangkain mong magpadala ng mga asset nang direkta mula sa isang network papunta sa isa pa, maaari itong magresulta sa permanenteng pagkawala ng asset. Siguraduhing gumamit ng bridge." + "message": "Maaari mong mawala ang mga asset kung susubukan mo itong ipadala mula sa isang network papunta sa isa pa. Ligtas ng maglipat ng pondo sa pagitan ng mga network sa pamamagitan ng paggamit ng bridge." }, "attemptSendingAssetsWithPortfolio": { "message": "Maaaring mawala ang iyong mga asset kung susubukan mong ipadala ito mula sa ibang network. Ilipat ang mga pondo nang ligtas sa pagitan ng mga network gamit ang isang bridge, tulad ng $1" @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "Subukang kanselahin ang swap nang libre" }, + "attributes": { + "message": "Mga Attribute" + }, "attributions": { "message": "Mga Attribution" }, + "auroraRpcDeprecationMessage": { + "message": "Ang Infura RPC URL ay hindi na sinusuportahan ang Aurora." + }, "authorizedPermissions": { "message": "Inawtorisahan mo ang mga sumusunod na pahintulot" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Naka-off ang batayang kapakinabangan" }, + "basicConfigurationDescription": { + "message": "Nag-aalok ang MetaMask ng mga batayang tampok tulad ng mga detalye ng token at mga setting ng gas sa pamamagitan ng serbisyo sa internet. Kapag gumamit ka ng serbisyo sa internet, ibabahagi ang iyong IP address, sa kasong ito sa MetaMask. Tulad ito ng pagbisita mo sa anumang website. Ang MetaMask ay pansamantalang ginagamit ang datos na ito at hindi ibebenta ang iyong datos. Maaari kang gumamit ng VPN o i-off ang mga serbisyon ito, ngunit maaari itong makaapekto sa iyong karanasan sa MetaMask. Upang matuto pa basahin ang aming $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Batayang kapakinabangan" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "Hindi kailanman hihingiin sa iyo ng MetaMask Beta ang iyong Lihim na Parirala sa Pagbawi." }, + "billionAbbreviation": { + "message": "B", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Ang aktibidad ng Bitcoin ay hindi suportado" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Ang pag-on sa tampok na ito ay nagbibigay sa iyo ng opsyon na magdagdag ng Account sa Bitcoin sa iyong MetaMask Extension na nagmula sa iyong umiiral na Lihim na Parirala sa Pagbawi. Ito ay isang eksperimental na tampok sa Beta, kaya gamitin ito nang may-pag-iingat. Upang bigyan kami ng feedback sa bagong karanasan na ito sa Bitcoin, mangyaring sagutan ito $1.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Paganahin ang \"Magdagdag ng bagong account sa Bitcoin (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Ang pag-on sa tampok na ito ay magbibigay sa iyo ng opsyon na magdagdag ng Account sa Bitcoin para sa test network." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Paganahin ang \"Magdagdag ng bagong account sa Bitcoin (Testnet)\"" + }, "blockExplorerAccountAction": { "message": "Account", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Hindi namin inirerekomendang magpatuloy sa hiling na ito." + }, "blockaidDescriptionApproveFarming": { "message": "Kung aaprubahan mo ang kahilingang ito, posibleng kunin ng third party na kilala sa mga panloloko ang lahat ng asset mo." }, @@ -666,7 +807,7 @@ "message": "Kung aaprubahan mo ang kahilingang ito, posibleng nakawin ng ibang tao ang mga asset mo na nakalista sa Blur." }, "blockaidDescriptionErrored": { - "message": "Dahil sa error, hindi na-verify ang kahilingang ito ng tagapagbigay ng seguridad. Magpatuloy nang may pag-iingat." + "message": "Dahil sa error, hindi namin masuri para sa alerto sa seguridad. Magpatuloy lamang kung pinagkakatiwalan mo ang kaugnay na address." }, "blockaidDescriptionMaliciousDomain": { "message": "Nakikipag-ugnayan ka sa isang mapaminsalang domain. Kung aaprubahan mo ang kahilingang ito, posibleng mawala sa iyo ang mga asset mo." @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Kung aaprubahan mo ang kahilingang ito, kukunin ng third party na kilala sa mga panloloko ang lahat ng asset mo." }, + "blockaidDescriptionWarning": { + "message": "Maaaring ito ay isang mapanlinlang na hiling. Magpatuloy lang kung pinagkakatiwalaan mo ang bawat address na kaugnay." + }, "blockaidMessage": { "message": "Pagpapanatili ng privacy - walang data na ibinabahagi sa mga third party. Available sa Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Base at Sepolia." }, @@ -687,7 +831,7 @@ "message": "Ito ay mapanlinlang na kahilingan" }, "blockaidTitleMayNotBeSafe": { - "message": "Maaaring hindi ligtas ang kahilingan" + "message": "Maging maingat" }, "blockaidTitleSuspicious": { "message": "Ito ay kahina-hinalang kahilingan" @@ -695,6 +839,9 @@ "blockies": { "message": "Mga Blocky" }, + "boughtFor": { + "message": "Binili para sa/kay" + }, "bridge": { "message": "Bridge" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Kailangan mong gamitin ang MetaMask sa Google Chrome para maikonekta sa iyong Wallet na Hardware." }, + "circulatingSupply": { + "message": "Umiikot na supply" + }, "clear": { "message": "I-clear" }, @@ -799,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Pindutin ito para manu-manong idagdag ang mga token." + "message": "Maaari ka ring magdagdag ng mga token nang manu-mano." }, "close": { "message": "Isara" @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Pangalan ng koleksyon" + }, "comboNoOptions": { "message": "Walang opsyon na nahanap", "description": "Default text shown in the combo field dropdown if no options." @@ -836,6 +989,9 @@ "message": "Kumpirmahin" }, "confirmAlertModalAcknowledgeMultiple": { + "message": "Kinikilala ko ang mga alerto at nais ko pa ring magpatuloy" + }, + "confirmAlertModalAcknowledgeSingle": { "message": "Kinikilala ko ang mga alerto at nais ko pa rin magpatuloy" }, "confirmAlertModalDetails": { @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Kumpirmahin ang koneksyon sa $1" }, + "confirmDeletion": { + "message": "Kumpirmahin ang pagtanggal" + }, + "confirmFieldPaymaster": { + "message": "Ang bayarin ay binayaran ni" + }, + "confirmFieldTooltipPaymaster": { + "message": "Ang bayarin para sa transaksyong ito ay babayaran ng paymaster smart contract." + }, "confirmPassword": { "message": "Kumpirmahin ang password" }, "confirmRecoveryPhrase": { "message": "Kumpirmahin ang Lihim na Parirala sa Pagbawi" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Kumpirmahin lamang ang transaksyong ito kung ganap mong nauunawaan ang nilalaman at nagtitiwala sa site na humihiling." + "confirmRpcUrlDeletionMessage": { + "message": "Sigurado ka ba na nais mong tanggalin ang RPC URL? Ang iyong impormasyon ay hindi mase-save sa network na ito." + }, + "confirmTitleDescPermitSignature": { + "message": "Ang site na ito ay nais ng permiso para gamitin ang iyong mga token." + }, + "confirmTitleDescSIWESignature": { + "message": "Nais ng site na mag-sign in ka upang patunayan na pag-aari mo ang account na ito." }, "confirmTitleDescSignature": { "message": "Kumpirmahin lamang ang mensaheng ito kung ganap mong nauunawaan ang nilalaman at nagtitiwala sa site na humihiling." }, + "confirmTitlePermitSignature": { + "message": "Hiling sa limitasyon ng paggastos" + }, + "confirmTitleSIWESignature": { + "message": "Kahilingan sa pag-sign in" + }, "confirmTitleSignature": { "message": "Kahilingan sa paglagda" }, @@ -958,7 +1135,7 @@ "message": "Nakakonekta sa" }, "connecting": { - "message": "Kumokonekta..." + "message": "Kumokonekta" }, "connectingTo": { "message": "Kumokonekta sa $1" @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Gumawa ng account" }, + "creatorAddress": { + "message": "Address ng creator" + }, "crossChainSwapsLink": { "message": "Mag-swap sa mga network gamit ang MetaMask Portfolio" }, @@ -1260,9 +1440,27 @@ "data": { "message": "Datos" }, + "dataCollectionForMarketing": { + "message": "Koleksyon ng data para sa marketing" + }, + "dataCollectionForMarketingDescription": { + "message": "Gagamit kami ng MetaMetrics upang matutunan kung paano ka nakikipag-ugnayan sa aming komunikasyon sa marketing. Maaari naming ibahagi ang mga may kaugnayang balita (tulad ng mga tampok ng produkto at iba pang materyal)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Okay" + }, + "dataCollectionWarningPopoverDescription": { + "message": "In-off mo ang koleksyon ng data para sa layunin sa marketing. Ito ay inilalapat lamang sa device na ito. Kung gumagamit ka ng MetaMask sa ibang mga device, siguraduhing mag-opt out din doon." + }, "dataHex": { "message": "Hex" }, + "dataUnavailable": { + "message": "hindi available ang data" + }, + "dateCreated": { + "message": "Petsa ng paggawa" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "Kahilingan sa pag-decrypt" }, + "defaultRpcUrl": { + "message": "Default na RPC URL" + }, "delete": { "message": "Tanggalin" }, @@ -1308,6 +1509,9 @@ "message": "Tanggalin ang $1 network?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Tanggalin ang RPC URL" + }, "deposit": { "message": "Deposito" }, @@ -1333,18 +1537,6 @@ "details": { "message": "Mga Detalye" }, - "developerOptions": { - "message": "Mga Opsyon ng Developer" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Ang mga reset ay ipinapakita sa boolean para maging false sa lahat ng anunsiyo. Ang mga anunisyo ay mga notipikasyon na ipinapakita sa What's New popup modal." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Nire-reset ang iba't ibang estado kaugnay sa onboarding at nire-redirect sa \"Secure Your Wallet\" onboarding page." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Magreresulta sa isang timestamp na tuloy-tuloy na naka-save sa session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "Ang “$1” ay hindi pinagagana dahil hindi nito naabot ang pinakamababang 10% na dagdag mula sa orihinal na bayad sa gas.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "Hindi kilalang oras ng pagproseso" }, + "editNetworkLink": { + "message": "i-edit ang orihinal na network" + }, "editNonceField": { "message": "I-edit sa Nonce" }, @@ -1549,12 +1744,12 @@ "message": "paganahin ang $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Paganahin ang autodetection ng token" - }, "enabled": { "message": "Pinagana" }, + "enabledNetworks": { + "message": "Mga pinapaganang network" + }, "encryptionPublicKeyNotice": { "message": "Kailangan ng $1 ang iyong pampublikong encryption key. Sa pamamagitan ng pagbibigay ng pahintulot, makakagawa ang site na ito ng mga naka-encrypt na mensahe para sa iyo.", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "Tinatayang singil" }, + "estimatedFeeTooltip": { + "message": "Halaga na binayaran para iproseso ang transaksyon sa network." + }, "ethGasPriceFetchWarning": { "message": "Ang backup na presyo ng gas ay ibinigay dahil ang pangunahing serbisyo ng pagtantya ng gas ay hindi available sa ngayon." }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Tingnan sa Etherscan" }, + "existingChainId": { + "message": "Ang impormasyon na iyong inilagay ay nauugnay sa isang umiiral na ID ng chain." + }, + "existingRpcUrl": { + "message": "Ang URL na ito ay nauugnay sa ibang ID ng chain." + }, "expandView": { "message": "I-expand ang view" }, @@ -1732,6 +1936,9 @@ "message": "Hindi gumagana ang pag-import ng file? Mag-click dito!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Hanapin ang tama sa:" + }, "flaskWelcomeUninstall": { "message": "dapat mong i-uninstall ang extension na ito", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "Nakalimutan ang password?" }, + "form": { + "message": "form" + }, "from": { "message": "Mula kay/sa" }, @@ -1901,7 +2111,7 @@ "message": "Goerli Test Network" }, "gotIt": { - "message": "OK!" + "message": "Nakuha ko" }, "grantedToWithColon": { "message": "Ipinagkaloob kay:" @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "mataas" }, + "highestCurrentBid": { + "message": "Pinakamataas na kasalukuyang bid" + }, + "highestFloorPrice": { + "message": "Pinakamataas na floor price" + }, "history": { "message": "History" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "Mae-edit ng aksyong ito ang mga token na nakalista na sa iyong wallet, na puwedeng gamitin para i-phish ka. Aprubahan lang kung sigurado kang gusto mong baguhin kung ano ang kinakatawan ng mga token na ito. Alamin pa ang tungkol sa $1" }, + "l1Fee": { + "message": "L1 na bayad" + }, + "l1FeeTooltip": { + "message": "L1 na bayad sa gas" + }, + "l2Fee": { + "message": "L2 na bayad" + }, + "l2FeeTooltip": { + "message": "L2 na bayad sa gas" + }, "lastConnected": { "message": "Huling Kumonekta" }, - "lastPriceSold": { - "message": "Huling presyong naibenta" - }, "lastSold": { "message": "Huling naibenta" }, @@ -2495,6 +2720,12 @@ "message": "Siguraduhing walang tumitingin", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Market cap" + }, + "marketDetails": { + "message": "Mga detalye ng market" + }, "max": { "message": "Max" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "Pinakamataas na bayad" }, + "maxFeeTooltip": { + "message": "Ang pinakamalaking bayad na inilaan para bayaran ang transaksyon." + }, "maxPriorityFee": { "message": "Pinakamataas na bayad sa priyoridad" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "Pamamaraan" }, - "methodDataTransactionDescription": { - "message": "Ito ang espesipikong aksyon na gagawin. Ang data na ito ay maaaring mapeke, kaya siguraduhin na pinagkakatiwalaan mo ang site sa kabilang panig." + "methodDataTransactionDesc": { + "message": "Isinakatuparang function batay sa na-decode na input data." }, "methodNotSupported": { "message": "Hindi suportado sa account na ito." @@ -2560,6 +2794,10 @@ "metrics": { "message": "Metrics" }, + "millionAbbreviation": { + "message": "M", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Ang pinili mong account na ($1) ay kakaiba sa account na sinusubukan mong mag-sign in ($2)" }, @@ -2672,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Ang native token sa network na ito ay $1. Ito ang token na ginagamit para sa mga bayad sa gas.", + "message": "Ang native token sa network na ito ay $1. Ito ang token na ginagamit para sa mga gas fee. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Ang pangalan ay nauugnay sa network na ito." }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Mga pagpipilian na network" + }, "networkProvider": { "message": "Network provider" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "Ang “$1” matagumpay na naidagdag!" }, + "newNetworkEdited": { + "message": "Ang “$1” ay matagumpay na na-edit!" + }, "newNftAddedMessage": { "message": "Matagumpay na naidagdag ang NFT!" }, @@ -2869,8 +3116,11 @@ "nftAlreadyAdded": { "message": "Naidagdag na ang NFT." }, + "nftAutoDetectionEnabled": { + "message": "Pinagana ang autodetection ng NFT" + }, "nftDisclaimer": { - "message": "Disclaimer: Kinukuha ng MetaMask ang media file mula sa pinagmulang url. Ang url na ito kung minsan ay pinapalitan ng marketplace kung saan naka-mint ang NFT." + "message": "Disclaimer: Kinukuha ng MetaMask ang media file mula sa pinagmulang url. Ang url na ito kung minsan ay pinapalitan ng marketplace kung saan nalikha ang NFT." }, "nftOptions": { "message": "Mga Pagpipilian sa NFT" @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "Walang resolusyon para sa ibinigay na domain." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Ang mga Snap, at karamihan sa mga hardware wallet, ay hindi gagana sa iyong kasalukuyang bersyon ng browser." + }, "noNFTs": { "message": "Wala pang mga NFT" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "I-customize ang nonce ng transaksyon" }, - "nonceFieldDescription": { - "message": "I-on ito para baguhin ang nonce (numero ng transaksyon) sa mga screen ng kumpirmasyon. Ito ay isang advanced na feature, gamitin nang maingat." + "nonceFieldDesc": { + "message": "I-on ito para baguhin ang nonce (numero ng transaksyon) kapag nagpapadala ng mga asset. Ito ay isang advanced na feature, gamitin nang may pag-iingat." }, "nonceFieldHeading": { "message": "I-custom ang nonce" @@ -2998,7 +3251,7 @@ "message": "Bayad sa priyoridad (GWEI)" }, "notificationItemCheckBlockExplorer": { - "message": "Tingnan sa BlockExplorer" + "message": "Tingnan sa Block Explorer" }, "notificationItemCollection": { "message": "Koleksyon" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "1 bagong token ang nakita sa account na ito" }, + "numberOfTokens": { + "message": "Bilang ng token" + }, "ofTextNofM": { "message": "ng" }, @@ -3167,8 +3423,36 @@ "on": { "message": "Naka-on" }, - "onboarding": { - "message": "Onboarding" + "onboardedMetametricsAccept": { + "message": "Sumasang-ayon ako" + }, + "onboardedMetametricsDisagree": { + "message": "Huwag na lang" + }, + "onboardedMetametricsKey1": { + "message": "Pinakabagong mga pagpapaunlad" + }, + "onboardedMetametricsKey2": { + "message": "Mga tampok ng produkto" + }, + "onboardedMetametricsKey3": { + "message": "Iba pang kaugnay na mga materyal sa promosyon" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Dagdag pa sa $1, nais naming gamitin ang data upang maunawaan kung paano ka nakikipag-ugnayan sa komunikasyon sa marketing.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Tumutulong ito sa amin na maiangkop ang aming ibinabahagi sa iyo, tulad ng:" + }, + "onboardedMetametricsParagraph3": { + "message": "Tandaan, hindi namin ibebenta ang data na iyong ibibigay at maaari kang mag-opt out anumang oras." + }, + "onboardedMetametricsTitle": { + "message": "Tulungan kami na paghusayin ang iyong karanasan" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Ginagawang posible ng IPFS gateway na ma-access at makita ang datos na pinangangasiwaan ng mga third party. Maaari kang magdagdag ng custom na IPFS gateway o magpatuloy sa paggamit ng default." @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Kapag kami ay nangangalap ng metrics, ito ay palaging..." }, - "onboardingMetametricsDisagree": { - "message": "Salamat nalang" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Tulungan kaming mapahusay ang MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Gagamitin namin ang data na ito upang matutunan kung paano ka nakikipag-ugnayan sa aming mga komunikasyon sa marketing. Maaari kaming magbahagi ng kaugnay na balita (tulad ng mga tampok ng produkto)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Buong Access" }, @@ -3286,6 +3570,22 @@ "message": "Ang mga alerto sa pagtuklas ng phishing ay umaasa sa komunikasyon sa $1. Ang jsDeliver ay magkakaroon ng access sa iyong IP address. Tingnan ang $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1D", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1M", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1W", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1Y", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Galugarin ang mga Snap" - }, - "openSeaToBlockaidDescription": { - "message": "Ang mga alerto sa seguridad ay hindi na available sa network na ito. Ang pag-install ng Snap ay maaaring magpahusay sa iyong seguridad." - }, - "openSeaToBlockaidTitle": { - "message": "Paalala!" - }, "operationFailed": { "message": "Nabigo ang Operasyon" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Ang mga konektadong site ay pahintulot na ngayon" }, + "permitSimulationDetailInfo": { + "message": "Binibigyan mo ang gumagastos ng permiso upang gumastos ng ganito karaming token mula sa iyong account." + }, "personalAddressDetected": { "message": "Natukoy ang personal na address. Ilagay ang address ng kontrata ng token." }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "Mga sikat na custom na network" }, + "popularNetworkAddToolTip": { + "message": "Ang ilan sa mga network na ito ay umaasa sa mga third party. Ang koneksyon na ito ay maaaring hindi gaanong maaasahan o binibigyang-daan ang mga third-party na mag-track ng aktibidad. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfolio" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "Nakaraan" }, + "price": { + "message": "Presyo" + }, + "priceUnavailable": { + "message": "hindi available ang presyo" + }, "primaryCurrencySetting": { "message": "Pangunahing Currency" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "Quote rate" }, + "rank": { + "message": "Rank" + }, "reAddAccounts": { "message": "idagdag muli ang anumang ibang mga account" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "Tumanggap" }, - "receiveTokensCamelCase": { - "message": "Tumanggap ng mga token" - }, "recipientAddressPlaceholder": { "message": "Ilagay ang pampublikong address (0x) o ENS name" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "I-reset" }, - "resetStates": { - "message": "I-reset ang Mga Estado" - }, "resetWallet": { "message": "I-reset ang wallet" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "Maghanap ng mga account" }, + "searchNfts": { + "message": "Hanapin ang mga NFT" + }, "searchTokens": { "message": "Maghanap ng mga token" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "I-secure ang aking wallet (inirerekomenda)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Isulat at itago sa maraming sikretong lugar." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "I-save sa tagapamahala ng password" + "message": "Isulat at itago sa maraming sikretong lugar." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Itago sa safe-deposit box." }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,10 +4571,10 @@ "message": "Pumili ng token" }, "selectNFTPrivacyPreference": { - "message": "I-on ang pagtuklas ng NFT sa Mga Setting" + "message": "Paganahin ang autodetection ng NFT" }, "selectPathHelp": { - "message": "Kung hindi mo makita ang mga account na inaasahan mo, subukang lumipat ng HD path." + "message": "Kung hindi mo makita ang mga account na inaasahan mo, subukang lumipat ng HD path o kasalukuyang piniling network." }, "selectType": { "message": "Pumili ng Uri" @@ -4284,9 +4585,6 @@ "send": { "message": "Magpadala" }, - "sendAToken": { - "message": "Magpadala ng token" - }, "sendBugReport": { "message": "Padalhan kami ng ulat ng bug." }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Sepolia test network" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker Keep Alive" - }, "setAdvancedPrivacySettingsDetails": { "message": "Ginagamit ng MetaMask ang mga pinagkakatiwalaang serbisyo ng third-party na ito para mapahusay ang kakayahang magamit at kaligtasan ng produkto." }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Ito ay umaasa sa ibang third party na API para sa bawat network, na naglalantad sa iyong address sa Ethereum at iyong IP address." }, + "showLess": { + "message": "Magpakita ng mas kaunti" + }, "showMore": { "message": "Ipakita ang iba pa" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Pirmahan lang ang mensaheng ito kung ganap mong nauunawaan ang nilalaman at nagtitiwala sa site na humihiling." }, - "signatureRequestWarning": { - "message": "Ang pagpirma sa mensaheng ito ay maaaring mapanganib. Maaari kang magbigay ng buong kontrol ng iyong account at mga asset sa partido sa kabilang dulo ng mensaheng ito. Nangangahulugan iyon na maaari nitong ubusin ang laman ng iyong account anumang oras. Magpatuloy nang may pag-iingat. $1." - }, "signed": { "message": "Napirmahan" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "Pinipirmahan" }, + "signingInWith": { + "message": "Nagsa-sign in gamit ang" + }, "simulationDetailsFailed": { "message": "Mayroong error sa pag-load ng iyong pagtataya." }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Tinatayang mga pagbabago sa balanse" }, + "siweIssued": { + "message": "Ibinigay" + }, + "siweNetwork": { + "message": "Network" + }, + "siweRequestId": { + "message": "Request ID" + }, + "siweResources": { + "message": "Mga Mapagkukunan" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Ikaw ay nagsa-sign in sa isang site at walang mga inaasahang pagbabago sa iyong account." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Laktawan" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "Mga account na kontrol lang ng mga third-party na Snap." }, + "snapConnectTo": { + "message": "Kumonekta sa $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Hayaan ang $1 na awtomatikong kumonekta sa $2 nang wala ang iyong pag-apruba.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "Si $1 ay gustong gumamit ng $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "Website" }, + "snapHomeMenu": { + "message": "Snap Home Menu" + }, "snapInstallRequest": { "message": "Ang pag-install ng $1 ay nagbibigay ng mga sumusunod na pahintulot.", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "Pinagmulan" }, + "speed": { + "message": "Bilis" + }, "speedUp": { "message": "Pabilisin" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Masyadong malaki ang limitasyon sa paggastos" }, + "spender": { + "message": "Gumagastos" + }, "spendingCap": { "message": "Limitasyon sa paggastos" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "Naglalaman ang mga log ng estado ng iyong mga address ng pampublikong account at ipinadalang transaksyon." }, - "states": { - "message": "Mga estado" - }, "status": { "message": "Katayuan" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "Naisumite" }, + "suggestedBySnap": { + "message": "Iminumungkahi ng $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Iminumungkahing pangalan:" + }, "suggestedTokenSymbol": { "message": "Iminumungkahing ticket symbol:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Kinukuha ang mga quote" + "message": "Kinukuha ang mga quote..." }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm... nagkaproblema. Subukan ulit, o kung magpapatuloy ang mga error, makipag-ugnayan sa customer support." @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "koleksyong ito" }, + "threeMonthsAbbreviation": { + "message": "3M", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Oras" }, @@ -5495,45 +5836,6 @@ "message": "Para kay/sa: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Nanganganib ka sa mga phishing na pag-atake. Protektahan ang sarili mo sa pamamagitan ng pag-off sa eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Kung pagaganahin mo ang setting na ito, maaari kang makakuha ng mga kahilingan sa paglagda na hindi nababasa. Sa pamamagitan ng pagpirma sa isang mensaheng hindi mo naiintindihan, posible kang sumang-ayon na ipamigay ang iyong mga pondo at NFT." - }, - "toggleEthSignField": { - "message": "Mga kahilingan sa Eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " baka niloloko ka" - }, - "toggleEthSignModalBannerText": { - "message": "Kung hiniling sa iyo na i-on ang setting na ito," - }, - "toggleEthSignModalCheckBox": { - "message": "Nauunawaan ko na maaaring mawala ang aking pondo at mga NFT kapag pinagana ko ang mga kahilingan sa eth-sign. " - }, - "toggleEthSignModalDescription": { - "message": "Ang pagbibigay ng pahintulot sa mga kahilingan ng eth_sign ay magpapahina sa iyo upang atakehin ng phishing. Palaging i-review ang URL a t mag-ingat kapag nagsa-sign ng mga mensahe na naglalaman ng code." - }, - "toggleEthSignModalFormError": { - "message": "Mali ang text" - }, - "toggleEthSignModalFormLabel": { - "message": "Ilagay ang “pipirmahan ko lang kung ano ang nauunawaan ko” para magpatuloy" - }, - "toggleEthSignModalFormValidation": { - "message": "Pipirmahan ko lang kung ano ang nauunawaan ko" - }, - "toggleEthSignModalTitle": { - "message": "Gamitin sa sarili mong pananagutan" - }, - "toggleEthSignOff": { - "message": "I-OFF (Inirerekomenda)" - }, - "toggleEthSignOn": { - "message": "I-ON (Hindi inirerekomenda)" - }, "toggleRequestQueueDescription": { "message": "Pinahihintulutan ka nitong pumili ng network para sa bawat site sa halip na iisang piniling network para sa lahat ng site. Pipigilan ka ng feature na ito na magpalit ng network nang mano-mano, na maaaring makasira sa iyong karanasan bilang user sa ilang partikular na site." }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "Address ng kontrata ng token" }, + "tokenDecimal": { + "message": "Decimal na token" + }, "tokenDecimalFetchFailed": { "message": "Kailangan ng decimal ng token. Hanapin ito sa: $1" }, @@ -5577,13 +5882,16 @@ "message": "ID ng Token" }, "tokenList": { - "message": "Mga listahan ng token:" + "message": "Mga listahan ng token" }, "tokenScamSecurityRisk": { "message": "mga panloloko sa token at panganib sa seguridad" }, "tokenShowUp": { - "message": "Maaaring hindi awtomatikong lumabas ang iyong mga token sa iyong wallet." + "message": "Maaaring hindi awtomatikong lumabas ang iyong mga token sa iyong wallet. " + }, + "tokenStandard": { + "message": "Pamantayan ng token" }, "tokenSymbol": { "message": "Simbolo ng token" @@ -5595,6 +5903,9 @@ "message": "$1 bagong token ang nakita", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Mga token sa koleksyon" + }, "tooltipApproveButton": { "message": "Nauunawaan ko" }, @@ -5610,6 +5921,9 @@ "total": { "message": "Kabuuan" }, + "totalVolume": { + "message": "Kabuuang volume" + }, "transaction": { "message": "transaksyon" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "Nagawa ang transaksyon na nagkakahalagang $1 sa $2." }, + "transactionDataFunction": { + "message": "Function" + }, "transactionDetailDappGasMoreInfo": { "message": "Minungkahing site" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "Maglipat mula kay/sa" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Nagkakaproblema kami sa pagkonekta ng iyong Ledger. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Ayon sa aming mga talaan, ang URL na ito ay hindi tumutugma sa isang kilalang provider para sa ID ng chain na ito." + }, "unapproved": { "message": "Hindi inaprubahan" }, @@ -5844,12 +6168,21 @@ "update": { "message": "I-update" }, + "updateOrEditNetworkInformations": { + "message": "I-update ang iyong impormasyon o" + }, "updateRequest": { "message": "Hiling sa pag-update" }, "updatedWithDate": { "message": "Na-update noong $1" }, + "uploadDropFile": { + "message": "I-drop ang file mo rito" + }, + "uploadFile": { + "message": "I-upload ang file" + }, "urlErrorMsg": { "message": "Kinakailangan ng mga URL ang naaangkop na HTTP/HTTPS prefix." }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "Ano ito?" }, + "wrongChainId": { + "message": "Ang ID ng chain na ito ay hindi tugma sa pangalan ng network." + }, + "wrongNetworkName": { + "message": "Ayon sa aming mga talaan, ang pangalan ng network ay maaaring hindi tumugma nang tama sa ID ng chain na ito." + }, "xOfYPending": { "message": "$1 ng $2 ang nakabinbin", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "Mga account mo" }, - "yourFundsMayBeAtRisk": { - "message": "Maaaring nasa panganib ang iyong pondo" + "yourActivity": { + "message": "Iyong aktibidad" + }, + "yourBalance": { + "message": "Iyong balanse" }, "yourNFTmayBeAtRisk": { "message": "Maaaring nasa panganib ang iyong NFT" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 5af4aed30230..cc40ce53b487 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -42,7 +42,7 @@ "message": "QR donanım cüzdanınızı bağlayın" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (çok yakında)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Giriş talebindeki adres, giriş yapmak için kullandığınız hesapla uyumlu değil." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Bir hesap seçmeniz gerekiyor!" }, + "accountTypeNotSupported": { + "message": "Hesap türü desteklenmiyor" + }, "accounts": { "message": "Hesaplar" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Yeni bir Ethereum hesabı ekle" }, + "addNewBitcoinAccount": { + "message": "Yeni bir Bitcoin hesabı ekle (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Yeni bir Bitcoin hesabı ekle (Test Ağı)" + }, "addNewToken": { "message": "Yeni token ekleyin" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "NFT ekleyin" }, + "addRpcUrl": { + "message": "RPC URL adresi ekle" + }, "addSnapAccountToggle": { "message": "\"Snap hesabı ekle (Beta)\" özelliğini etkinleştir" }, @@ -305,12 +317,21 @@ "message": "Bir tokeni bulamadınız mı? Adresini yapıştırarak dilediğiniz tokeni manuel olarak ekleyebilirsiniz. Token sözleşme adreslerini $1 alanında bulabilirsiniz", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "URL ekle" + }, "addingCustomNetwork": { "message": "Ağ Ekleniyor" }, "addingTokens": { "message": "Token'leri ekleme" }, + "additionalNetworks": { + "message": "İlave ağlar" + }, + "additionalRpcUrl": { + "message": "Diğer RPC URL" + }, "address": { "message": "Adres" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Gelişmiş yapılandırma" }, + "advancedDetailsDataDesc": { + "message": "Veri" + }, + "advancedDetailsHexDesc": { + "message": "On Altılı" + }, + "advancedDetailsNonceDesc": { + "message": "Nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Bir hesabın işlem numarasıdır. İlk işlem için nonce 0 olup sıralı olarak artar." + }, "advancedGasFeeDefaultOptIn": { "message": "Bu değerleri $1 ağı için varsayılanım olarak kaydet.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Uyarı" }, + "alertActionBuy": { + "message": "ETH Al" + }, + "alertActionUpdateGas": { + "message": "Gaz limitini güncelle" + }, + "alertActionUpdateGasFee": { + "message": "Ücreti güncelle" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Gaz seçeneklerini güncelle" + }, "alertBannerMultipleAlertsDescription": { "message": "Bu talebi onaylarsanız dolandırıcılıkla bilinen üçüncü bir taraf tüm varlıklarınızı çalabilir." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "\"Ayarlar > Uyarılar\" kısmında değiştirilebilir" }, + "alertMessageGasEstimateFailed": { + "message": "Kesin bir ücret sunamıyoruz ve bu tahmin yüksek olabilir. Özel bir gaz limiti girmenizi öneririz ancak işlemin yine de başarısız olma riski vardır." + }, + "alertMessageGasFeeLow": { + "message": "Düşük bir ücret seçerken işlemlerin daha yavaş olmasını ve bekleme sürelerinin daha uzun olmasını bekleyebilirsiniz. Daha hızlı işlemler için Piyasa veya Agresif ücret seçeneklerini seçin." + }, + "alertMessageGasTooLow": { + "message": "Bu işlemle devam etmek için gaz limitini 21000 veya üzeri olacak şekilde artırmanız gerekecek." + }, + "alertMessageInsufficientBalance": { + "message": "Hesabınızda işlem ücretlerini ödemek için yeterli ETH yok." + }, + "alertMessageNetworkBusy": { + "message": "Gaz fiyatları yüksektir ve tahmin daha az kesindir." + }, + "alertMessageNoGasPrice": { + "message": "Siz ücreti manuel olarak güncelleyene dek bu işleme devam edemiyoruz." + }, + "alertMessagePendingTransactions": { + "message": "Önceki bir işlem tamamlanana dek bu işlem gerçekleşmeyecektir. Bir işlemi nasıl iptal edeceğinizi veya hızlandıracağınızı öğrenin." + }, + "alertMessageSignInDomainMismatch": { + "message": "Talepte bulunan site giriş yaptığınız site değil. Bu durum oturum açma bilgilerinizi çalma teşebbüsü olabilir." + }, + "alertMessageSignInWrongAccount": { + "message": "Bu site sizden yanlış hesabı kullanarak giriş yapmanızı istiyor." + }, + "alertMessageSigningOrSubmitting": { + "message": "Bu işlem sadece önceki işleminiz tamamlandıktan sonra gerçekleşecek." + }, "alertModalAcknowledge": { "message": "Riski anlıyor ve yine de ilerlemek istiyorum" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Tüm uyarıları incele" }, + "alertReasonGasEstimateFailed": { + "message": "Ücret yanlış" + }, + "alertReasonGasFeeLow": { + "message": "Hız yavaş" + }, + "alertReasonGasTooLow": { + "message": "Gaz limiti düşük" + }, + "alertReasonInsufficientBalance": { + "message": "Para yetersiz" + }, + "alertReasonNetworkBusy": { + "message": "Ağ meşgul" + }, + "alertReasonNoGasPrice": { + "message": "Ücret tahmini mevcut değil" + }, + "alertReasonPendingTransactions": { + "message": "İşlem beklemede" + }, + "alertReasonSignIn": { + "message": "Şüpheli giriş talebi" + }, + "alertReasonWrongAccount": { + "message": "Yanlış hesap" + }, "alertSettingsUnconnectedAccount": { "message": "Bağlı olmayan bir hesap ile bir web sitesine göz atma seçildi" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Tüm İzinler" }, + "allTimeHigh": { + "message": "Tüm zamanların en yükseği" + }, + "allTimeLow": { + "message": "Tüm zamanların en düşüğü" + }, "allYourNFTsOf": { "message": "Tüm $1 NFT'leriniz", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 ve $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Duyurular" - }, "appDescription": { "message": "Tarayıcında bir Ethereum Cüzdanı", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Varlık seçenekleri" }, "attemptSendingAssets": { - "message": "Varlıkları doğrudan bir ağdan diğerine göndermeye çalışırsanız bu durum kalıcı varlık kaybına neden olabilir. Bir köprü kullandığınızdan emin olun." + "message": "Varlıklarınızı doğrudan bir ağdan diğerine göndermeye çalışırsanız onları kaybedebilirsiniz. Fonları köprü kullanarak ağlar arasında güvenli bir şekilde transfer edin." }, "attemptSendingAssetsWithPortfolio": { "message": "Varlıkları doğrudan bir ağdan diğerine göndermeye çalışırsanız bu durum kalıcı varlık kaybına neden olabilir. $1 gibi bir köprü kullandığınızdan emin olun" @@ -524,6 +629,9 @@ "attemptToCancelSwapForFree": { "message": "Swap işlemini ücretsiz iptal etme girişimi" }, + "attributes": { + "message": "Özellikler" + }, "attributions": { "message": "Özellikler" }, @@ -594,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Temel işlevsellik kapalı" }, + "basicConfigurationDescription": { + "message": "MetaMask, internet hizmetleri üzerinden token bilgileri ve gaz ayarları gibi temel özellikler sunar. İnternet hizmetlerini kullandığınızda IP adresiniz, bu durumda MetaMask ile, paylaşılır. Bu tıpkı herhangi bir web sitesini ziyaret ettiğinizde olduğu gibidir. MetaMask bu verileri geçici olarak kullanır ve verilerinizi hiçbir zaman satmaz. Bir VPN kullanabilir veya bu hizmetleri kapatabilirsiniz ancak bu durum MetaMask deneyiminizi etkileyebilir. Daha fazla bilgi için $1 bölümümüzü okuyun.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Temel işlevsellik" }, @@ -637,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask hiçbir zaman Gizli Kurtarma İfadenizi istemez." }, + "billionAbbreviation": { + "message": "MR", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Bitcoin aktivitesi desteklenmiyor" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Bu özelliği açtığınızda mevcut Gizli Kurtarma İfadenizden türetilen MetaMask Uzantınıza bir Bitcoin Hesabı ekleme seçeneğiniz olacaktır. Bu, deneysel bir Beta özelliğidir, bu yüzden riski kendinize ait olarak kullanmalısınız. Bu yeni Bitcoin deneyimi hakkında bize geri bildirim göndermek için lütfen bu $1 bölümünü doldurun.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "\"Yeni bir Bitcoin hesabı ekle (Beta)\" seçeneğini etkinleştir" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Bu özelliği açtığınızda test ağı için bir Bitcoin Hesabı ekleme seçeneğiniz olacak." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "\"Yeni bir Bitcoin hesabı ekle (Test Ağı)\" seçeneğini etkinleştir" + }, "blockExplorerAccountAction": { "message": "Hesap", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -662,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Bu taleple ilerlemenizi önermiyoruz." + }, "blockaidDescriptionApproveFarming": { "message": "Bu talebi onaylarsanız dolandırıcılıkla ünlü üçüncü bir taraf tüm varlıklarınızı çalabilir." }, @@ -669,7 +807,7 @@ "message": "Bu talebi onaylarsanız birisi Blur üzerinde yer alan varlıklarınızı çalabilir." }, "blockaidDescriptionErrored": { - "message": "Bu talep bir hatadan dolayı güvenlik sağlayıcısı tarafından doğrulanmadı. Dikkatli bir şekilde ilerleyin." + "message": "Bir hatadan dolayı güvenlik uyarılarını kontrol edemedik. Sadece ilgili her adrese güveniyorsanız ilerleyin." }, "blockaidDescriptionMaliciousDomain": { "message": "Kötü niyetli bir alanla etkileşimde bulunuyorsunuz. Bu talebi onaylarsanız varlıklarınızı kaybedebilirsiniz." @@ -683,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Bu talebi onaylarsanız dolandırıcılıkla ünlü üçüncü bir taraf tüm varlıklarınızı çalar." }, + "blockaidDescriptionWarning": { + "message": "Bu aldatıcı bir talep olabilir. Sadece ilgili her adrese güveniyorsanız devam edin." + }, "blockaidMessage": { "message": "Gizlilik koruması - hiçbir veri üçüncü taraflarla paylaşılmaz. Arbitrum, Avalanche, BNB chain, Ethereum Ana Ağı, Linea, Optimism, Polygon, Base ve Sepolia için sunulur." }, @@ -690,7 +831,7 @@ "message": "Bu aldatıcı bir talep" }, "blockaidTitleMayNotBeSafe": { - "message": "Talep güvenli olmayabilir" + "message": "Dikkatli olun" }, "blockaidTitleSuspicious": { "message": "Bu şüpheli bir talep" @@ -698,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Satın alma amacı" + }, "bridge": { "message": "Köprü" }, @@ -782,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Donanım Cüzdanınıza bağlamak için MetaMask'ı Google Chrome'da kullanmanız gerekir." }, + "circulatingSupply": { + "message": "Dolaşımdaki arz" + }, "clear": { "message": "Temizle" }, @@ -802,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Tokenleri manuel olarak eklemek için buaraya tıklayın." + "message": "Dilediğiniz zaman tokenleri manuel olarak ekleyebilirsiniz." }, "close": { "message": "Kapat" @@ -816,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Koleksiyon adı" + }, "comboNoOptions": { "message": "Hiçbir seçenek bulunamadı", "description": "Default text shown in the combo field dropdown if no options." @@ -841,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Uyarıları kabul ediyorum ve yine de ilerlemek istiyorum" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Uyarıyı kabul ediyor ve yine de ilerlemek istiyorum" + }, "confirmAlertModalDetails": { "message": "Oturum açarsanız dolandırıcıklarla bilinen üçüncü bir taraf tüm varlıklarınızı ele geçirebilir. İlerlemeden önce lütfen uyarıları inceleyin." }, @@ -856,18 +1009,39 @@ "confirmConnectionTitle": { "message": "$1 ile bağlantıyı onayla" }, + "confirmDeletion": { + "message": "Silme işlemini onayla" + }, + "confirmFieldPaymaster": { + "message": "Ücreti ödeyen taraf" + }, + "confirmFieldTooltipPaymaster": { + "message": "Bu işlemin ücreti paymaster akıllı sözleşmesi tarafından ödenecektir." + }, "confirmPassword": { "message": "Şifreyi onayla" }, "confirmRecoveryPhrase": { "message": "Gizli Kurtarma İfadesini Onayla" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Bu işlemi sadece içeriği tam olarak anlıyorsanız ve talepte bulunan siteye güveniyorsanız onaylayın." + "confirmRpcUrlDeletionMessage": { + "message": "RPC URL adresini silmek istediğinizden emin misiniz? Bilgileriniz bu ağ için kaydedilmeyecektir." + }, + "confirmTitleDescPermitSignature": { + "message": "Bu site token'lerinizi harcamak için izin istiyor." + }, + "confirmTitleDescSIWESignature": { + "message": "Bir site bu hesabın sahibi olduğunuzu kanıtlamak için giriş yapmanızı istiyor." }, "confirmTitleDescSignature": { "message": "Bu mesajı sadece içeriği onaylıyorsanız ve talepte bulunan siteye güveniyorsanız onaylayın." }, + "confirmTitlePermitSignature": { + "message": "Harcama üst limiti talebi" + }, + "confirmTitleSIWESignature": { + "message": "Giriş talebi" + }, "confirmTitleSignature": { "message": "İmza talebi" }, @@ -961,7 +1135,7 @@ "message": "Şununla bağlanıldı:" }, "connecting": { - "message": "Bağlanıyor..." + "message": "Bağlanıyor" }, "connectingTo": { "message": "Şuna bağlanılıyor: $1" @@ -1094,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Hesap oluştur" }, + "creatorAddress": { + "message": "Oluşturucu adresi" + }, "crossChainSwapsLink": { "message": "MetaMask Portfolio ile ağlar arasında swap gerçekleştirin" }, @@ -1263,9 +1440,27 @@ "data": { "message": "Veri" }, + "dataCollectionForMarketing": { + "message": "Pazarlama amacıyla veri toplama" + }, + "dataCollectionForMarketingDescription": { + "message": "MetaMetrics'i, pazarlama iletişimlerimizle nasıl etkileşimde bulunduğunuzu öğrenmek için kullanacağız. İlgili haberleri (ürün özellikleri ve diğer materyaller gibi) paylaşabiliriz." + }, + "dataCollectionWarningPopoverButton": { + "message": "Tamam" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Pazarlama amacıyla veri toplama seçeneğini kapattınız. Bu, sadece bu cihaz için geçerlidir. MetaMask'i başka cihazlarda kullanırsanız bu cihazlarda da vazgeçtiğinizden emin olun." + }, "dataHex": { "message": "On Altılı" }, + "dataUnavailable": { + "message": "veri mevcut değil" + }, + "dateCreated": { + "message": "Oluşturulma tarihi" + }, "dcent": { "message": "D'Cent" }, @@ -1295,6 +1490,9 @@ "decryptRequest": { "message": "Şifre çözme talebi" }, + "defaultRpcUrl": { + "message": "Varsayılan RPC URL adresi" + }, "delete": { "message": "Sil" }, @@ -1311,6 +1509,9 @@ "message": "$1 ağını sil?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "RPC URL'sini sil" + }, "deposit": { "message": "Para Yatır" }, @@ -1336,18 +1537,6 @@ "details": { "message": "Ayrıntılar" }, - "developerOptions": { - "message": "Geliştirici Seçenekleri" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Tüm duyurular için isShown boolean değerini false olarak sıfırlar. Duyurular, Yenilikler açılır penceresi modalında gösterilen bildirimlerdir." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Katılım ile ilgili çeşitli durumları sıfırlar ve \"Cüzdanınızı Koruyun\" katılım sayfasına yeniden yönlendirir." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Zaman damgasının sürekli olarak oturum depolama alanına kaydedilmesine neden olur" - }, "disabledGasOptionToolTipMessage": { "message": "Orijinal gaz ücretinden minimum %10'luk bir artışı karşılamadığı için \"$1\" devre dışı bırakıldı.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1524,6 +1713,9 @@ "editGasTooLow": { "message": "Bilinmeyen işlem süresi" }, + "editNetworkLink": { + "message": "orijinal ağı düzenle" + }, "editNonceField": { "message": "Nonce'u düzenle" }, @@ -1552,12 +1744,12 @@ "message": "şunu etkinleştir: $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Otomatik token algılamayı etkinleştir" - }, "enabled": { "message": "Etkinleştirildi" }, + "enabledNetworks": { + "message": "Etkinleştirilen ağlar" + }, "encryptionPublicKeyNotice": { "message": "$1 genel şifreleme anahtarınızı istiyor. Bunu onayladığınızda bu site sizin için şifrelenmiş mesajlar oluşturabilecektir.", "description": "$1 is the web3 site name" @@ -1662,6 +1854,9 @@ "estimatedFee": { "message": "Tahmini ücret" }, + "estimatedFeeTooltip": { + "message": "Ağda işlemi gerçekleştirmek için ödenen tutar." + }, "ethGasPriceFetchWarning": { "message": "Ana gaz tahmini hizmeti olarak sunulan yedek gaz fiyatı şu anda kullanılamıyor." }, @@ -1681,6 +1876,12 @@ "etherscanViewOn": { "message": "Etherscan'de görüntüle" }, + "existingChainId": { + "message": "Girdiğiniz bilgiler mevcut bir zincir kimliği ile ilişkilidir." + }, + "existingRpcUrl": { + "message": "Bu URL adresi başka bir zincir kimliği ile ilişkilidir." + }, "expandView": { "message": "Görünümü genişlet" }, @@ -1704,7 +1905,7 @@ "message": "Önerilen takma adlar" }, "externalNameSourcesSettingDescription": { - "message": "Etherscan, Infura ve Lens Protocol gibi üçüncü taraf kaynaklardan etkileşimde bulunduğunuz adresler için önerilen takma adları alırız. Bu kaynaklar o adresleri ve sizin IP adresinizi görebilir. Hesap adresiniz üçüncü taraflarla paylaşılmaz." + "message": "Etherscan, Infura ve Lens Protocol gibi üçüncü taraf kaynaklardan etkileşimde bulunduğunuz adeesler için önerilen takma adları alırız. Bu kaynaklar o adresleri ve sizin IP adresinizi görebilir. Hesap adresiniz üçüncü taraflarla paylaşılmaz." }, "failed": { "message": "Başarısız oldu" @@ -1735,6 +1936,9 @@ "message": "Dosya içe aktarma çalışmıyor mu? Buraya tıklayın!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Doğrusunu şurada bulabilirsiniz:" + }, "flaskWelcomeUninstall": { "message": "bu uzantıyı kaldırmalısın", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1774,6 +1978,9 @@ "forgotPassword": { "message": "Şifrenizi mi unuttunuz?" }, + "form": { + "message": "form" + }, "from": { "message": "Kimden" }, @@ -1904,7 +2111,7 @@ "message": "Goerli test ağı" }, "gotIt": { - "message": "Anladım!" + "message": "Anladım" }, "grantedToWithColon": { "message": "İzin verilen:" @@ -1982,6 +2189,12 @@ "highLowercase": { "message": "yüksek" }, + "highestCurrentBid": { + "message": "Mevcut en yüksek teklif" + }, + "highestFloorPrice": { + "message": "En yüksek taban fiyat" + }, "history": { "message": "Geçmiş" }, @@ -2321,12 +2534,21 @@ "knownTokenWarning": { "message": "Bu eylem kimlik avı için kullanılabilecek şekilde cüzdanınızda zaten listelenmiş olan tokenleri düzenleyecektir. Sadece bu tokenlerin neyi temsil ettiğini değiştirmek istediğinizden eminseniz onaylayın. $1 hakkında daha fazla bilgi edinin" }, + "l1Fee": { + "message": "L1 ücreti" + }, + "l1FeeTooltip": { + "message": "L1 gaz ücreti" + }, + "l2Fee": { + "message": "L2 ücreti" + }, + "l2FeeTooltip": { + "message": "L2 gaz ücreti" + }, "lastConnected": { "message": "Son bağlanma" }, - "lastPriceSold": { - "message": "Son satış fiyatı" - }, "lastSold": { "message": "Son satış" }, @@ -2498,6 +2720,12 @@ "message": "Hiç kimsenin bakmadığından emin olun", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Piyasa değeri" + }, + "marketDetails": { + "message": "Piyasa bilgileri" + }, "max": { "message": "Maksimum" }, @@ -2507,6 +2735,9 @@ "maxFee": { "message": "Maks. ücret" }, + "maxFeeTooltip": { + "message": "İşlemi ödemek için sunulan maksimum ücret." + }, "maxPriorityFee": { "message": "Maks. öncelik ücreti" }, @@ -2554,8 +2785,8 @@ "methodData": { "message": "Yöntem" }, - "methodDataTransactionDescription": { - "message": "Atılacak spesifik adım budur. Bu veriler sahte olabilir, o yüzden diğer taraftaki siteye güvendiğinizden emin olun." + "methodDataTransactionDesc": { + "message": "Şifresi çözülmüş giriş verilerine göre gerçekleştirilen işlev." }, "methodNotSupported": { "message": "Bu hesap ile desteklenmez." @@ -2563,6 +2794,10 @@ "metrics": { "message": "Metrikler" }, + "millionAbbreviation": { + "message": "MN", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Seçili hesap ($1) imza atmaya çalışan hesaptan ($2) farklı" }, @@ -2675,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "Bu ağdaki yerli token $1. Bu gaz ücretleri için kullanılan tokendir.", + "message": "Bu ağdaki yerli token $1. Bu, gaz ücretleri için kullanılan tokendir. ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2744,6 +2979,9 @@ "networkNameBase": { "message": "Temel" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Bu ağ ile ilişkilendirilmiş ad." }, @@ -2768,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Ağ seçenekleri" + }, "networkProvider": { "message": "Ağ sağlayıcısı" }, @@ -2836,6 +3077,9 @@ "newNetworkAdded": { "message": "\"$1\" başarılı bir şekilde eklendi!" }, + "newNetworkEdited": { + "message": "“$1” başarılı bir şekilde düzenlendi!" + }, "newNftAddedMessage": { "message": "NFT başarılı bir şekilde eklendi!" }, @@ -2872,6 +3116,9 @@ "nftAlreadyAdded": { "message": "NFT zaten eklenmiş." }, + "nftAutoDetectionEnabled": { + "message": "NFT otomatik algılama etkinleştirildi" + }, "nftDisclaimer": { "message": "Sorumluluğun Reddi: MetaMask medya dosyasını kaynak url adresinden çeker. Bu url adresi bazen NFT'nin mint edildiği pazar yeri tarafından değiştirilir." }, @@ -2922,6 +3169,9 @@ "noDomainResolution": { "message": "Alan adı için çözümleme sunulmamış." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Geçerli tarayıcı sürümünüzle snap'ler ve çoğu donanım cüzdanı çalışmayacak." + }, "noNFTs": { "message": "Henüz NFT yok" }, @@ -2952,8 +3202,8 @@ "nonceField": { "message": "İşlem nonce'unu özelleştir" }, - "nonceFieldDescription": { - "message": "Onay ekranlarında nonce'u (işlem numarası) değiştirmek için bunu açın. Bu gelişmiş bir özelliktir, dikkatli kullanın." + "nonceFieldDesc": { + "message": "Varlık gönderirken nonce'u (işlem numarasını) değiştirmek için bunu açın. Bu gelişmiş bir özelliktir, dikkatli kullanın." }, "nonceFieldHeading": { "message": "Özel nonce" @@ -3155,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "Bu hesapta 1 yeni token bulundu" }, + "numberOfTokens": { + "message": "Token sayısı" + }, "ofTextNofM": { "message": "/" }, @@ -3170,8 +3423,36 @@ "on": { "message": "Açık" }, - "onboarding": { - "message": "Katılım" + "onboardedMetametricsAccept": { + "message": "Kabul ediyorum" + }, + "onboardedMetametricsDisagree": { + "message": "Hayır, istemiyorum" + }, + "onboardedMetametricsKey1": { + "message": "En yeni gelişmeler" + }, + "onboardedMetametricsKey2": { + "message": "Ürün özellikleri" + }, + "onboardedMetametricsKey3": { + "message": "İlgili diğer promosyon materyalleri" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "$1 bağlantısına ek olarak pazarlama iletişimleri ile nasıl etkileşimde bulunduğunuzu anlamak amacıyla verileri kullanmak istiyoruz.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Bu, sizinle neleri paylaşacağımızı kişiselleştirebilmemize yardımcı olur, örneğin:" + }, + "onboardedMetametricsParagraph3": { + "message": "Unutmayın, sunduğunuz verileri hiçbir zaman satmayız ve dilediğiniz zaman tercihinizi değiştirebilirsiniz." + }, + "onboardedMetametricsTitle": { + "message": "Deneyiminizi iyileştirmemize yardımcı olun" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "IPFS ağ geçidi üçüncü tarafların barındırdığı veriler için erişim ve görüntülemeyi mümkün kılar. Özel bir IPFS ağ geçidi ekleyebilir veya varsayılanı kullanmaya devam edebilirsiniz." @@ -3209,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Ölçümleri toplarken bu her zaman aşağıdaki gibi olacaktır..." }, - "onboardingMetametricsDisagree": { - "message": "Hayır, istemiyorum" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3246,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "MetaMask'i iyileştirmemize yardımcı olun" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Bu verileri, pazarlama iletişimlerimizle nasıl etkileşimde bulunduğunuzu öğrenmek için kullanacağız. İlgili haberleri (ürün özellikleri gibi) paylaşabiliriz." + }, "onboardingPinExtensionBillboardAccess": { "message": "Tam erişim" }, @@ -3289,6 +3570,22 @@ "message": "Kimlik avı tespiti uyarıları $1 ile iletişime bağlıdır. jsDeliver IP adresinize erişim sağlayacaktır. Şunu görüntüleyin: $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1G", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1A", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1H", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1Y", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3313,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Snap'leri Keşfet" - }, - "openSeaToBlockaidDescription": { - "message": "Güvenlik uyarıları artık bu ağda mevcut değil. Bir Snap yüklemek güvenliğinizi artırabilir." - }, - "openSeaToBlockaidTitle": { - "message": "Dikkat!" - }, "operationFailed": { "message": "İşlem Başarısız Oldu" }, @@ -3650,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Bağlı siteler şimdi izinler oldu" }, + "permitSimulationDetailInfo": { + "message": "Harcama yapan tarafa hesabınızdan bu kadar çok token'i harcama izni veriyorsunuz." + }, "personalAddressDetected": { "message": "Kişisel adres algılandı. Token sözleşme adresini girin." }, @@ -3682,6 +3973,10 @@ "popularCustomNetworks": { "message": "Popüler özel ağlar" }, + "popularNetworkAddToolTip": { + "message": "Bu ağların bazıları üçüncü taraflara dayalıdır. Bağlantılar daha az güvenilir olabilir veya üçüncü tarafların aktiviteleri takip etmesine olanak sağlayabilir. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portföy" }, @@ -3694,6 +3989,12 @@ "prev": { "message": "Önceki" }, + "price": { + "message": "Fiyat" + }, + "priceUnavailable": { + "message": "fiyat mevcut değil" + }, "primaryCurrencySetting": { "message": "Öncelikli para birimi" }, @@ -3846,6 +4147,9 @@ "quoteRate": { "message": "Kota oranı" }, + "rank": { + "message": "Sıralama" + }, "reAddAccounts": { "message": "diğer hesapları yeniden ekle" }, @@ -3858,9 +4162,6 @@ "receive": { "message": "Al" }, - "receiveTokensCamelCase": { - "message": "Token'leri al" - }, "recipientAddressPlaceholder": { "message": "Genel adres (0x) veya ENS adı girin" }, @@ -4015,9 +4316,6 @@ "reset": { "message": "Sıfırla" }, - "resetStates": { - "message": "Durumları Sıfırla" - }, "resetWallet": { "message": "Cüzdanı sıfırla" }, @@ -4153,6 +4451,9 @@ "searchAccounts": { "message": "Hesapları ara" }, + "searchNfts": { + "message": "NFT ara" + }, "searchTokens": { "message": "Token ara" }, @@ -4197,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Cüzdanımı koru (önerilir)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Birden fazla gizli yerde not ederek saklayın." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Bir şifre yöneticisine kaydedin" + "message": "Birden fazla gizli yerde not ederek saklayın." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Bir kasada saklayın." }, "seedPhraseIntroSidebarCopyOne": { @@ -4273,10 +4571,10 @@ "message": "Token seç" }, "selectNFTPrivacyPreference": { - "message": "Ayarlarda NFT algılamayı açın" + "message": "NFT Otomatik Algılamayı etkinleştir" }, "selectPathHelp": { - "message": "Beklediğiniz hesapları görmüyorsanız HD yoluna geçmeyi deneyin." + "message": "Beklediğiniz hesapları görmüyorsanız HD yoluna veya geçerli seçilen ağa geçmeyi deneyin." }, "selectType": { "message": "Tür Seç" @@ -4287,9 +4585,6 @@ "send": { "message": "Gönder" }, - "sendAToken": { - "message": "Bir token gönder" - }, "sendBugReport": { "message": "Bize bir hata raporu gönder." }, @@ -4340,9 +4635,6 @@ "sepolia": { "message": "Sepolia test ağı" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker'ı Canlı Tut" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask, ürünün kullanılabilirliğini ve güvenliğini iyileştirmek amacıyla bu güvenilir üçüncü taraf hizmetlerini kullanır." }, @@ -4399,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Ethereum adresinizi ve IP adresinizi açığa çıkaran her ağ için farklı üçüncü taraf API'lerine güvenir." }, + "showLess": { + "message": "Daha az göster" + }, "showMore": { "message": "Daha fazlasını göster" }, @@ -4429,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Bumesajı sadece içeriği tam olarak anlıyorsanız ve talepte bulunan siteye güveniyorsanız imzalayın." }, - "signatureRequestWarning": { - "message": "Bu mesajın imzalanması tehlikeli olabilir. Hesabınızın ve varlıklarınızın tüm kontrolünü bu mesajın diğer ucundaki tarafa veriyor olabilirsiniz. Başka bir deyişle istedikleri zaman hesabınızı boşaltabilirler. Dikkatle ilerleyin. $1." - }, "signed": { "message": "İmzalandı" }, @@ -4441,6 +4733,9 @@ "signing": { "message": "İmzalanıyor" }, + "signingInWith": { + "message": "Şununla giriş yap:" + }, "simulationDetailsFailed": { "message": "Tahmininiz yüklenirken bir hata oldu." }, @@ -4478,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Bakiye değişikliklerini tahmin edin" }, + "siweIssued": { + "message": "Düzenlendi" + }, + "siweNetwork": { + "message": "Ağ" + }, + "siweRequestId": { + "message": "Kimlik Talebi" + }, + "siweResources": { + "message": "Kaynaklar" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Bir siteye giriş yapıyorsunuz ve hesabınızda öngörülen herhangi bir değişiklik yok." + }, + "siweURI": { + "message": "URL adresi" + }, "skip": { "message": "Atla" }, @@ -4581,6 +4894,14 @@ "snapAccountsDescription": { "message": "Üçüncü taraf Snapleri tarafından kontrol edilen hesaplar." }, + "snapConnectTo": { + "message": "$1 adresine bağlan", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Onayınız olmadan $1 otomatik olarak $2 adresine bağlansın.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 şunu kullanmak istiyor: $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4592,6 +4913,9 @@ "snapDetailWebsite": { "message": "Web Sitesi" }, + "snapHomeMenu": { + "message": "Snap Ana Menüsü" + }, "snapInstallRequest": { "message": "$1 adlı snap'i yüklemek ona aşağıdaki izinleri verir.", "description": "$1 is the snap name." @@ -4714,6 +5038,9 @@ "source": { "message": "Kaynak" }, + "speed": { + "message": "Hız" + }, "speedUp": { "message": "Hızlandır" }, @@ -4748,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Harcama limiti çok büyük" }, + "spender": { + "message": "Harcama Yapan Taraf" + }, "spendingCap": { "message": "Harcama üst limiti" }, @@ -4868,9 +5198,6 @@ "stateLogsDescription": { "message": "Durum günlükleri genel hesap adreslerinizi ve gönderilen işlemleri içerir." }, - "states": { - "message": "Durumlar" - }, "status": { "message": "Durum" }, @@ -4975,6 +5302,13 @@ "submitted": { "message": "Gönderildi" }, + "suggestedBySnap": { + "message": "$1 tarafından öneriliyor", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Önerilen isim:" + }, "suggestedTokenSymbol": { "message": "Önerilen ticker sembolü:" }, @@ -5089,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Teklifler alınıyor" + "message": "Teklifler alınıyor..." }, "swapFetchingQuotesErrorDescription": { "message": "Hımmm... bir hata oluştu. Tekrar deneyin veya sorun devam ederse müşteri hizmetleri destek bölümüyle iletişime geçin." @@ -5437,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Şuna geçiş yaptınız:" + "message": "Şu anda kullandığınız ağ" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Ağ değiştirmek bekleyen tüm onayları iptal eder" @@ -5485,6 +5819,10 @@ "thisCollection": { "message": "bu koleksiyon" }, + "threeMonthsAbbreviation": { + "message": "3A", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Zaman" }, @@ -5498,45 +5836,6 @@ "message": "Alıcı: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Kimlik avı saldırıları bakımından risk altındasınız. eth_sign özelliğini kapatarak kendinizi koruyun." - }, - "toggleEthSignDescriptionField": { - "message": "Bu ayarı etkinleştirirseniz okunabilir olmayan imza talepleri alabilirsiniz. Anlamadığınız bir mesajı imzaladığınızda paranızı ve NFT'lerinizi vermeyi kabul ediyor olabilirsiniz." - }, - "toggleEthSignField": { - "message": "Eth_sign talepleri" - }, - "toggleEthSignModalBannerBoldText": { - "message": " dolandırılıyor olabilirsiniz" - }, - "toggleEthSignModalBannerText": { - "message": "Bu ayarı açmanız istendi ise" - }, - "toggleEthSignModalCheckBox": { - "message": "eth_sign taleplerini etkinleştirirsem tüm paramı ve NFT'lerimi kaybedebileceğimi anlıyorum. " - }, - "toggleEthSignModalDescription": { - "message": "eth_sign taleplerine izin vermeniz sizi kimlik avı saldırılarına karşı hassas hale getirebilir. Her zaman URL adresini inceleyin ve kod içeren mesajları imzalarken dikkat edin." - }, - "toggleEthSignModalFormError": { - "message": "Metin yanlış" - }, - "toggleEthSignModalFormLabel": { - "message": "Devam etmek için \"Sadece anladığım şeyleri imzalıyorum\" girin" - }, - "toggleEthSignModalFormValidation": { - "message": "Sadece anladığım şeyleri imzalıyorum" - }, - "toggleEthSignModalTitle": { - "message": "Kullanım riski size aittir" - }, - "toggleEthSignOff": { - "message": "Kapalı (Önerilir)" - }, - "toggleEthSignOn": { - "message": "Açık (Önerilmez)" - }, "toggleRequestQueueDescription": { "message": "Bu, tüm siteler için tek bir seçili ağ yerine her bir site için bir ağ seçebilmenize olanak sağlar. Bu özellik, manuel olarak ağ değiştirmenizi önleyebilir ve bu da belirli sitelerde kullanıcı deneyiminizi bozabilir." }, @@ -5564,6 +5863,9 @@ "tokenContractAddress": { "message": "Token sözleşme adresi" }, + "tokenDecimal": { + "message": "Token ondalığı" + }, "tokenDecimalFetchFailed": { "message": "Token ondalığı gereklidir. Şurada bulabilirsiniz: $1" }, @@ -5586,7 +5888,10 @@ "message": "token dolandırıcılıkları ve güvenlik riskleri" }, "tokenShowUp": { - "message": "Tokenleriniz cüzdanınızda otomatik olarak görünmeyebilir." + "message": "Tokenleriniz cüzdanınızda otomatik olarak görünmeyebilir. " + }, + "tokenStandard": { + "message": "Token standardı" }, "tokenSymbol": { "message": "Token sembolü" @@ -5598,6 +5903,9 @@ "message": "$1 yeni token bulundu", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Koleksiyondaki tokenler" + }, "tooltipApproveButton": { "message": "Anladım" }, @@ -5613,6 +5921,9 @@ "total": { "message": "Toplam" }, + "totalVolume": { + "message": "Toplam hacim" + }, "transaction": { "message": "işlem" }, @@ -5628,6 +5939,9 @@ "transactionCreated": { "message": "İşlem $2 itibariyle $1 değeriyle oluşturuldu." }, + "transactionDataFunction": { + "message": "İşlev" + }, "transactionDetailDappGasMoreInfo": { "message": "Site önerisi" }, @@ -5718,6 +6032,10 @@ "transferFrom": { "message": "Transfer kaynağı:" }, + "trillionAbbreviation": { + "message": "T", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Ledger'ınıza bağlanırken sorun yaşıyoruz. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5796,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Kayıtlarımıza göre bu URL adresi bu zincir kimliğinin bilinen bir sağlayıcısı ile uyumlu değil." + }, "unapproved": { "message": "Onaylanmadı" }, @@ -5847,12 +6168,21 @@ "update": { "message": "Güncelle" }, + "updateOrEditNetworkInformations": { + "message": "Bilgilerinizi güncelleyin veya" + }, "updateRequest": { "message": "Talebi güncelle" }, "updatedWithDate": { "message": "$1 güncellendi" }, + "uploadDropFile": { + "message": "Dosyanızı buraya sürükleyin" + }, + "uploadFile": { + "message": "Dosya yükle" + }, "urlErrorMsg": { "message": "URL adresleri için uygun HTTP/HTTPS ön eki gerekir." }, @@ -6068,6 +6398,12 @@ "whatsThis": { "message": "Bu nedir?" }, + "wrongChainId": { + "message": "Bu zincir kimliği ağ adı ile uyumlu değil." + }, + "wrongNetworkName": { + "message": "Kayıtlarımıza göre ağ adı bu zincir kimliği ile doğru şekilde eşleşmiyor olabilir." + }, "xOfYPending": { "message": "$1 / $2 bekliyor", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6091,8 +6427,11 @@ "yourAccounts": { "message": "Hesaplarınız" }, - "yourFundsMayBeAtRisk": { - "message": "Paranız tehlikede olabilir" + "yourActivity": { + "message": "Aktiviteniz" + }, + "yourBalance": { + "message": "Bakiyeniz" }, "yourNFTmayBeAtRisk": { "message": "NFT'niz tehlikede olabilir" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index bbfdfef6325e..c4795a502a7e 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -42,7 +42,7 @@ "message": "Kết nối với ví cứng QR của bạn" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (sắp ra mắt)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "Địa chỉ trong yêu cầu đăng nhập không trùng khớp với địa chỉ của tài khoản bạn đang sử dụng để đăng nhập." @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "Bạn cần chọn một tài khoản!" }, + "accountTypeNotSupported": { + "message": "Loại tài khoản không được hỗ trợ" + }, "accounts": { "message": "Tài khoản" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "Thêm tài khoản Ethereum mới" }, + "addNewBitcoinAccount": { + "message": "Thêm tài khoản Bitcoin mới (Beta)" + }, + "addNewBitcoinTestnetAccount": { + "message": "Thêm tài khoản Bitcoin mới (Mạng thử nghiệm)" + }, "addNewToken": { "message": "Thêm token mới" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "Thêm NFT" }, + "addRpcUrl": { + "message": "Thêm URL RPC" + }, "addSnapAccountToggle": { "message": "Bật \"Thêm tài khoản Snap (Beta)\"" }, @@ -305,12 +317,21 @@ "message": "Bạn không tìm thấy token? Bạn có thể dán địa chỉ của bất kỳ token nào để thêm token đó theo cách thủ công. Bạn có thể tìm thấy địa chỉ hợp đồng token trên $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Thêm URL" + }, "addingCustomNetwork": { "message": "Thêm mạng" }, "addingTokens": { "message": "Đang thêm token" }, + "additionalNetworks": { + "message": "Mạng bổ sung" + }, + "additionalRpcUrl": { + "message": "URL RPC Bổ sung" + }, "address": { "message": "Địa chỉ" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "Cấu hình nâng cao" }, + "advancedDetailsDataDesc": { + "message": "Dữ liệu" + }, + "advancedDetailsHexDesc": { + "message": "Thập lục phân" + }, + "advancedDetailsNonceDesc": { + "message": "Số nonce" + }, + "advancedDetailsNonceTooltip": { + "message": "Đây là số giao dịch của một tài khoản. Số nonce cho giao dịch đầu tiên là 0 và tăng dần theo thứ tự." + }, "advancedGasFeeDefaultOptIn": { "message": "Lưu các giá trị này làm giá trị mặc định cho mạng $1.", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "Cảnh báo" }, + "alertActionBuy": { + "message": "Mua ETH" + }, + "alertActionUpdateGas": { + "message": "Cập nhập hạn mức phí gas" + }, + "alertActionUpdateGasFee": { + "message": "Cập nhật phí" + }, + "alertActionUpdateGasFeeLevel": { + "message": "Cập nhật tùy chọn phí gas" + }, "alertBannerMultipleAlertsDescription": { "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo có thể lấy hết tài sản của bạn." }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "Bạn có thể thay đổi trong phần \"Cài đặt > Cảnh báo\"" }, + "alertMessageGasEstimateFailed": { + "message": "Chúng tôi không thể cung cấp phí chính xác và ước tính này có thể cao. Chúng tôi khuyên bạn nên nhập hạn mức phí gas tùy chỉnh, nhưng vẫn có rủi ro giao dịch sẽ thất bại." + }, + "alertMessageGasFeeLow": { + "message": "Khi chọn phí thấp, hãy lưu ý giao dịch sẽ chậm hơn và thời gian chờ đợi lâu hơn. Để giao dịch nhanh hơn, hãy chọn các tùy chọn phí Thị trường hoặc Cao." + }, + "alertMessageGasTooLow": { + "message": "Để tiếp tục giao dịch này, bạn cần tăng giới hạn phí gas lên 21000 hoặc cao hơn." + }, + "alertMessageInsufficientBalance": { + "message": "Bạn không có đủ ETH trong tài khoản để thanh toán phí giao dịch." + }, + "alertMessageNetworkBusy": { + "message": "Phí gas cao và ước tính kém chính xác hơn." + }, + "alertMessageNoGasPrice": { + "message": "Chúng tôi không thể tiếp tục giao dịch này cho đến khi bạn cập nhật phí thủ công." + }, + "alertMessagePendingTransactions": { + "message": "Giao dịch này sẽ không được thực hiện cho đến khi giao dịch trước đó hoàn tất. Tìm hiểu cách hủy hoặc đẩy nhanh giao dịch." + }, + "alertMessageSignInDomainMismatch": { + "message": "Trang web đưa ra yêu cầu không phải là trang web bạn đang đăng nhập. Đây có thể là một nỗ lực đánh cắp thông tin đăng nhập của bạn." + }, + "alertMessageSignInWrongAccount": { + "message": "Trang web này yêu cầu bạn đăng nhập bằng tài khoản không đúng." + }, + "alertMessageSigningOrSubmitting": { + "message": "Giao dịch này sẽ chỉ được thực hiện sau khi giao dịch trước đó của bạn hoàn tất." + }, "alertModalAcknowledge": { "message": "Tôi đã nhận thức được rủi ro và vẫn muốn tiếp tục" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "Xem lại tất cả cảnh báo" }, + "alertReasonGasEstimateFailed": { + "message": "Phí không chính xác" + }, + "alertReasonGasFeeLow": { + "message": "Tốc độ chậm" + }, + "alertReasonGasTooLow": { + "message": "Hạn mức phí gas thấp" + }, + "alertReasonInsufficientBalance": { + "message": "Không đủ tiền" + }, + "alertReasonNetworkBusy": { + "message": "Mạng đang bận" + }, + "alertReasonNoGasPrice": { + "message": "Ước tính phí không có sẵn" + }, + "alertReasonPendingTransactions": { + "message": "Giao dịch đang chờ xử lý" + }, + "alertReasonSignIn": { + "message": "Yêu cầu đăng nhập đáng ngờ" + }, + "alertReasonWrongAccount": { + "message": "Tài khoản không đúng" + }, "alertSettingsUnconnectedAccount": { "message": "Đang duyệt trang web khi chọn một tài khoản không được kết nối" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "Tất cả các quyền" }, + "allTimeHigh": { + "message": "Cao nhất lịch sử" + }, + "allTimeLow": { + "message": "Thấp nhất lịch sử" + }, "allYourNFTsOf": { "message": "Tất cả NFT của bạn từ $1", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 và $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "Thông báo" - }, "appDescription": { "message": "Ví Ethereum trên trình duyệt của bạn", "description": "The description of the application" @@ -516,7 +621,7 @@ "message": "Tùy chọn tài sản" }, "attemptSendingAssets": { - "message": "Nếu bạn cố gắng gửi tài sản trực tiếp từ mạng này sang mạng khác, bạn có thể bị mất tài sản vĩnh viễn. Hãy nhớ sử dụng cầu nối." + "message": "Bạn có thể bị mất tài sản nếu cố gắng gửi tài sản từ một mạng khác. Chuyển tiền an toàn giữa các mạng bằng cách sử dụng cầu nối." }, "attemptSendingAssetsWithPortfolio": { "message": "Bạn có thể bị mất tài sản nếu cố gắng gửi tài sản từ một mạng khác. Chuyển tiền an toàn giữa các mạng bằng cách sử dụng cầu nối, chẳng hạn như $1" @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "Cố gắng hủy hoán đổi miễn phí" }, + "attributes": { + "message": "Thuộc tính" + }, "attributions": { "message": "Ghi nhận đóng góp" }, + "auroraRpcDeprecationMessage": { + "message": "URL Infura RPC không còn hỗ trợ Aurora nữa." + }, "authorizedPermissions": { "message": "Bạn đã cấp các quyền sau đây" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "Chức năng cơ bản đã tắt" }, + "basicConfigurationDescription": { + "message": "MetaMask cung cấp các tính năng cơ bản như chi tiết token và cài đặt gas thông qua các dịch vụ Internet. Khi bạn sử dụng các dịch vụ Internet, địa chỉ IP của bạn sẽ được chia sẻ, trong trường hợp này là với MetaMask. Điều này giống như khi bạn truy cập bất kỳ trang web nào. MetaMask sử dụng dữ liệu này tạm thời và không bao giờ bán dữ liệu của bạn. Bạn có thể sử dụng VPN hoặc tắt các dịch vụ này, nhưng nó có thể ảnh hưởng đến trải nghiệm sử dụng MetaMask của bạn. Để tìm hiểu thêm, vui lòng đọc $1.", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "Chức năng cơ bản" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask Beta sẽ không bao giờ hỏi về Cụm từ khôi phục bí mật của bạn." }, + "billionAbbreviation": { + "message": "Tỷ", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "Hoạt động Bitcoin không được hỗ trợ" + }, + "bitcoinSupportSectionTitle": { + "message": "Bitcoin" + }, + "bitcoinSupportToggleDescription": { + "message": "Bật tính năng này sẽ cung cấp cho bạn tùy chọn thêm Tài khoản Bitcoin vào Tiện ích mở rộng MetaMask bắt nguồn từ Cụm từ khôi phục bí mật hiện có của bạn. Đây là một tính năng Beta thử nghiệm, nên bạn phải tự chịu rủi ro khi sử dụng nó. Để cung cấp phản hồi cho chúng tôi về trải nghiệm Bitcoin mới này, vui lòng điền vào $1 này.", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "Bật \"Thêm tài khoản Bitcoin mới (Beta)\"" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "Bật tính năng này sẽ cung cấp cho bạn tùy chọn thêm Tài khoản Bitcoin cho mạng thử nghiệm." + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "Bật \"Thêm tài khoản Bitcoin mới (Mạng thử nghiệm)\"" + }, "blockExplorerAccountAction": { "message": "Tài khoản", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "Chúng tôi khuyên bạn không nên tiếp tục với yêu cầu này." + }, "blockaidDescriptionApproveFarming": { "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo có thể lấy hết tài sản của bạn." }, @@ -666,7 +807,7 @@ "message": "Nếu bạn chấp thuận yêu cầu này, người khác có thể đánh cắp tài sản được niêm yết trên Blur của bạn." }, "blockaidDescriptionErrored": { - "message": "Do có lỗi, yêu cầu này đã không được nhà cung cấp dịch vụ bảo mật xác minh. Hãy thực hiện cẩn thận." + "message": "Do có lỗi, chúng tôi không thể kiểm tra các cảnh báo bảo mật. Chỉ tiếp tục nếu bạn tin tưởng tất cả các địa chỉ liên quan." }, "blockaidDescriptionMaliciousDomain": { "message": "Bạn đang tương tác với một tên miền độc hại. Nếu bạn chấp thuận yêu cầu này, bạn có thể mất tài sản của mình." @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo sẽ lấy hết tài sản của bạn." }, + "blockaidDescriptionWarning": { + "message": "Đây có thể là một yêu cầu lừa đảo. Chỉ tiếp tục nếu bạn tin tưởng mọi địa chỉ liên quan." + }, "blockaidMessage": { "message": "Bảo vệ quyền riêng tư - không có dữ liệu nào được chia sẻ với các bên thứ ba. Có sẵn trên Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Base và Sepolia." }, @@ -687,7 +831,7 @@ "message": "Đây là một yêu cầu lừa đảo" }, "blockaidTitleMayNotBeSafe": { - "message": "Yêu cầu có thể không an toàn" + "message": "Hãy cẩn thận" }, "blockaidTitleSuspicious": { "message": "Đây là một yêu cầu đáng ngờ" @@ -695,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "Đã mua với giá" + }, "bridge": { "message": "Cầu nối" }, @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "Bạn cần sử dụng MetaMask trên Google Chrome để kết nối với Ví cứng của bạn." }, + "circulatingSupply": { + "message": "Cung lưu hành" + }, "clear": { "message": "Xóa" }, @@ -799,7 +949,7 @@ "description": "Text that can be clicked to open a browser popup for connecting the ledger device via webhid" }, "clickToManuallyAdd": { - "message": "Nhấp vào đây để thêm token theo cách thủ công." + "message": "Bạn luôn có thể thêm token theo cách thủ công." }, "close": { "message": "Đóng" @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "Tên bộ sưu tập" + }, "comboNoOptions": { "message": "Không tìm thấy tùy chọn nào", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "Tôi đã hiểu rõ các cảnh báo và vẫn muốn tiếp tục" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "Tôi đã hiểu rõ cảnh báo và vẫn muốn tiếp tục" + }, "confirmAlertModalDetails": { "message": "Nếu bạn đăng nhập, một bên thứ ba nổi tiếng là lừa đảo có thể lấy tất cả tài sản của bạn. Vui lòng xem lại các cảnh báo trước khi tiếp tục." }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "Xác nhận kết nối với $1" }, + "confirmDeletion": { + "message": "Xác nhận xóa" + }, + "confirmFieldPaymaster": { + "message": "Phí được thanh toán bởi" + }, + "confirmFieldTooltipPaymaster": { + "message": "Phí cho giao dịch này sẽ được thanh toán bởi hợp đồng thông minh Paymaster." + }, "confirmPassword": { "message": "Xác nhận mật khẩu" }, "confirmRecoveryPhrase": { "message": "Xác nhận Cụm từ khôi phục bí mật" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "Chỉ xác nhận giao dịch này nếu bạn hoàn toàn hiểu nội dung và tin tưởng trang web yêu cầu." + "confirmRpcUrlDeletionMessage": { + "message": "Bạn có chắc chắn muốn xóa URL RPC? Thông tin của bạn sẽ không được lưu cho mạng này." + }, + "confirmTitleDescPermitSignature": { + "message": "Trang web này muốn được cấp quyền để chi tiêu số token của bạn." + }, + "confirmTitleDescSIWESignature": { + "message": "Một trang web yêu cầu bạn đăng nhập để chứng minh quyền sở hữu tài khoản này." }, "confirmTitleDescSignature": { "message": "Chỉ xác nhận thông báo này nếu bạn chấp thuận nội dung và tin tưởng trang web yêu cầu." }, + "confirmTitlePermitSignature": { + "message": "Yêu cầu hạn mức chi tiêu" + }, + "confirmTitleSIWESignature": { + "message": "Yêu cầu đăng nhập" + }, "confirmTitleSignature": { "message": "Yêu cầu chữ ký" }, @@ -958,7 +1135,7 @@ "message": "Đã kết nối với" }, "connecting": { - "message": "Đang kết nối..." + "message": "Đang kết nối" }, "connectingTo": { "message": "Đang kết nối với $1" @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "Tạo tài khoản" }, + "creatorAddress": { + "message": "Địa chỉ của người tạo" + }, "crossChainSwapsLink": { "message": "Hoán đổi trên các mạng với MetaMask Portfolio" }, @@ -1260,9 +1440,27 @@ "data": { "message": "Dữ liệu" }, + "dataCollectionForMarketing": { + "message": "Thu thập dữ liệu cho mục đích tiếp thị" + }, + "dataCollectionForMarketingDescription": { + "message": "Chúng tôi sẽ sử dụng MetaMetrics để tìm hiểu cách bạn tương tác với các thông tin tiếp thị. Chúng tôi có thể chia sẻ tin tức liên quan (chẳng hạn như tính năng sản phẩm và các tài liệu khác)." + }, + "dataCollectionWarningPopoverButton": { + "message": "Đồng ý" + }, + "dataCollectionWarningPopoverDescription": { + "message": "Bạn đã tắt tùy chọn thu thập dữ liệu cho mục đích tiếp thị của chúng tôi. Thao tác này chỉ áp dụng cho thiết bị này. Nếu bạn sử dụng MetaMask trên các thiết bị khác, nhớ chọn không tham gia trên các thiết bị đó." + }, "dataHex": { "message": "Thập lục phân" }, + "dataUnavailable": { + "message": "dữ liệu không khả dụng" + }, + "dateCreated": { + "message": "Ngày tạo" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "Giải mã yêu cầu" }, + "defaultRpcUrl": { + "message": "URL RPC mặc định" + }, "delete": { "message": "Xóa" }, @@ -1308,6 +1509,9 @@ "message": "Xóa mạng $1?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Xóa URL RPC" + }, "deposit": { "message": "Nạp" }, @@ -1333,18 +1537,6 @@ "details": { "message": "Chi tiết" }, - "developerOptions": { - "message": "Tùy chọn Nhà phát triển" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "Đặt lại giá trị isShown thành \"false\" cho tất cả thông báo. Thông báo là các nội dung hiển thị trong cửa sổ bật lên \"Xem tính năng mới\"." - }, - "developerOptionsResetStatesOnboarding": { - "message": "Đặt lại các trạng thái khác nhau liên quan đến phần giới thiệu và chuyển hướng đến trang giới thiệu \"Bảo mật ví của bạn\"." - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "Kết quả trong dấu thời gian được lưu liên tục vào session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1” bị vô hiệu hóa vì không đạt mức tăng tối thiểu 10% so với phí gas ban đầu.", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "Thời gian xử lý không rõ" }, + "editNetworkLink": { + "message": "chỉnh sửa mạng gốc" + }, "editNonceField": { "message": "Chỉnh sửa số nonce" }, @@ -1549,12 +1744,12 @@ "message": "bật $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "Bật tính năng tự động phát hiện token" - }, "enabled": { "message": "Đã bật" }, + "enabledNetworks": { + "message": "Mạng được bật" + }, "encryptionPublicKeyNotice": { "message": "$1 muốn biết khóa mã hóa công khai của bạn. Bằng việc đồng ý, trang web này sẽ có thể gửi thông báo được mã hóa cho bạn.", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "Phí ước tính" }, + "estimatedFeeTooltip": { + "message": "Số tiền được chi trả để xử lý giao dịch trên mạng." + }, "ethGasPriceFetchWarning": { "message": "Giá gas dự phòng được cung cấp vì dịch vụ ước tính giá gas chính hiện không hoạt động." }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "Xem trên Etherscan" }, + "existingChainId": { + "message": "Thông tin bạn đã nhập được liên kết với một ID chuỗi hiện có." + }, + "existingRpcUrl": { + "message": "URL này được liên kết với một ID chuỗi khác." + }, "expandView": { "message": "Chế độ xem mở rộng" }, @@ -1732,6 +1936,9 @@ "message": "Tính năng nhập tập tin không hoạt động? Nhấp vào đây!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "Tìm ID chuỗi đúng trên:" + }, "flaskWelcomeUninstall": { "message": "bạn nên gỡ cài đặt tiện ích mở rộng này", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "Quên mật khẩu?" }, + "form": { + "message": "mẫu" + }, "from": { "message": "Từ" }, @@ -1901,7 +2111,7 @@ "message": "Mạng thử nghiệm Goerli" }, "gotIt": { - "message": "Đã hiểu!" + "message": "Đã hiểu" }, "grantedToWithColon": { "message": "Cấp cho:" @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "cao" }, + "highestCurrentBid": { + "message": "Giá thầu hiện tại cao nhất" + }, + "highestFloorPrice": { + "message": "Giá sàn cao nhất" + }, "history": { "message": "Lịch sử" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "Hành động này sẽ chỉnh sửa các token đã niêm yết trong ví của bạn, kẻ xấu có thể lợi dụng việc này để lừa đảo bạn. Chỉ chấp thuận nếu bạn chắc chắn rằng bạn muốn thay đổi giá trị mà những token này đại diện cho. Tìm hiểu thêm về $1" }, + "l1Fee": { + "message": "Phí L1" + }, + "l1FeeTooltip": { + "message": "Phí gas L1" + }, + "l2Fee": { + "message": "Phí L2" + }, + "l2FeeTooltip": { + "message": "Phí gas L2" + }, "lastConnected": { "message": "Đã kết nối lần cuối" }, - "lastPriceSold": { - "message": "Giá bán gần nhất" - }, "lastSold": { "message": "Đã bán gần nhất" }, @@ -2495,6 +2720,12 @@ "message": "Đảm bảo không có ai đang nhìn", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "Vốn hóa thị trường" + }, + "marketDetails": { + "message": "Chi tiết thị trường" + }, "max": { "message": "Tối đa" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "Phí tối đa" }, + "maxFeeTooltip": { + "message": "Một khoản phí tối đa được cung cấp để thanh toán cho giao dịch." + }, "maxPriorityFee": { "message": "Phí ưu tiên tối đa" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "Phương thức" }, - "methodDataTransactionDescription": { - "message": "Đây là hành động cụ thể sẽ được thực hiện. Dữ liệu này có thể bị giả mạo, vì vậy hãy đảm bảo rằng bạn tin tưởng trang web ở đầu bên kia." + "methodDataTransactionDesc": { + "message": "Chức năng được thực hiện dựa trên dữ liệu đầu vào đã giải mã." }, "methodNotSupported": { "message": "Không được hỗ trợ với tài khoản này." @@ -2560,6 +2794,10 @@ "metrics": { "message": "Chỉ số" }, + "millionAbbreviation": { + "message": "Triệu", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "Tài khoản bạn đã chọn ($1) khác với tài khoản sử dụng để ký ($2)" }, @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "Base" }, + "networkNameBitcoin": { + "message": "Bitcoin" + }, "networkNameDefinition": { "message": "Tên được liên kết với mạng này." }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Tùy chọn mạng" + }, "networkProvider": { "message": "Nhà cung cấp mạng" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "“$1” đã được thêm thành công!" }, + "newNetworkEdited": { + "message": "“$1” đã được chỉnh sửa thành công!" + }, "newNftAddedMessage": { "message": "NFT đã được thêm thành công!" }, @@ -2869,8 +3116,11 @@ "nftAlreadyAdded": { "message": "NFT đã được thêm vào." }, + "nftAutoDetectionEnabled": { + "message": "Tính năng tự động phát hiện NFT đã được bật" + }, "nftDisclaimer": { - "message": "Tuyên bố miễn trừ trách nhiệm: MetaMask lấy tập tin phương tiện từ URL nguồn. URL này đôi khi bị thay đổi bởi thị trường mà NFT được đào." + "message": "Tuyên bố miễn trừ trách nhiệm: MetaMask lấy tập tin đa phương tiện từ URL nguồn. URL này đôi khi bị thay đổi bởi thị trường mà NFT được đào." }, "nftOptions": { "message": "Tùy chọn NFT" @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "Không có nội dung phân giải cho tên miền được cung cấp." }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snap và hầu hết các ví cứng sẽ không hoạt động với phiên bản trình duyệt hiện tại của bạn." + }, "noNFTs": { "message": "Chưa có NFT" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "Tùy chỉnh số nonce của giao dịch" }, - "nonceFieldDescription": { - "message": "Bật tùy chọn này để thay đổi số nonce (số giao dịch) trên màn hình xác nhận. Đây là tính năng nâng cao, hãy dùng một cách thận trọng." + "nonceFieldDesc": { + "message": "Bật tính năng này để thay đổi số nonce (số giao dịch) khi gửi tài sản. Đây là một tính năng nâng cao, hãy sử dụng thận trọng." }, "nonceFieldHeading": { "message": "Số nonce tùy chỉnh" @@ -2998,7 +3251,7 @@ "message": "Phí ưu tiên (GWEI)" }, "notificationItemCheckBlockExplorer": { - "message": "Kiểm tra trên BlockExplorer" + "message": "Kiểm tra trên Block Explorer" }, "notificationItemCollection": { "message": "Bộ sưu tập" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "Tìm thấy 1 token mới trong tài khoản này" }, + "numberOfTokens": { + "message": "Số lượng token" + }, "ofTextNofM": { "message": "trên" }, @@ -3167,8 +3423,36 @@ "on": { "message": "Bật" }, - "onboarding": { - "message": "Giới thiệu" + "onboardedMetametricsAccept": { + "message": "Tôi đồng ý" + }, + "onboardedMetametricsDisagree": { + "message": "Không, cảm ơn" + }, + "onboardedMetametricsKey1": { + "message": "Bước tiến mới nhất" + }, + "onboardedMetametricsKey2": { + "message": "Tính năng sản phẩm" + }, + "onboardedMetametricsKey3": { + "message": "Các tài liệu khuyến mại liên quan khác" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "Ngoài $1, chúng tôi muốn sử dụng dữ liệu để hiểu cách bạn tương tác với với các thông tin tiếp thị.", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "Điều này sẽ giúp chúng tôi cá nhân hóa nội dung mà chúng tôi chia sẻ với bạn, chẳng hạn như:" + }, + "onboardedMetametricsParagraph3": { + "message": "Hãy nhớ rằng chúng tôi không bán dữ liệu mà bạn cung cấp và bạn có thể chọn không tham gia bất kỳ lúc nào." + }, + "onboardedMetametricsTitle": { + "message": "Hãy giúp chúng tôi nâng cao trải nghiệm của bạn" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "Cổng IPFS cho phép truy cập và xem dữ liệu do bên thứ ba lưu trữ. Bạn có thể thêm cổng IPFS tùy chỉnh hoặc tiếp tục sử dụng cổng mặc định." @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "Khi thu thập số liệu, chúng tôi sẽ luôn cam kết điều này..." }, - "onboardingMetametricsDisagree": { - "message": "Không, cảm ơn" - }, "onboardingMetametricsInfuraTerms": { "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`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "Giúp chúng tôi cải thiện MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "Chúng tôi sẽ sử dụng dữ liệu này để tìm hiểu cách bạn tương tác với các thông tin tiếp thị. Chúng tôi có thể chia sẻ tin tức liên quan (chẳng hạn như tính năng sản phẩm)." + }, "onboardingPinExtensionBillboardAccess": { "message": "Toàn quyền truy cập" }, @@ -3286,6 +3570,22 @@ "message": "Thông báo phát hiện dấu hiệu lừa đảo tùy thuộc vào quá trình truyền tin với $1. jsDeliver sẽ có quyền truy cập vào địa chỉ IP của bạn. Xem $2.", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 Ngày", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 Tháng", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 Tuần", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 Năm", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "Khám phá các Snap" - }, - "openSeaToBlockaidDescription": { - "message": "Cảnh báo bảo mật không còn khả dụng trên mạng này. Cài đặt Snap có thể cải thiện khả năng bảo mật của bạn." - }, - "openSeaToBlockaidTitle": { - "message": "Chú ý!" - }, "operationFailed": { "message": "Thao tác thất bại" }, @@ -3392,7 +3683,7 @@ "description": "For importing an account from a private key" }, "paymasterInUse": { - "message": "Phí gas cho giao dịch này sẽ được Paymaster (tài khoản hợp đồng thông minh tài trợ cho các giao dịch) thanh toán.", + "message": "Phí gas cho giao dịch này sẽ được thanh toán bởi bên chi trả.", "description": "Alert shown in transaction confirmation if paymaster in use." }, "pending": { @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "Các trang web đã kết nối hiện đã được cấp quyền" }, + "permitSimulationDetailInfo": { + "message": "Bạn đang cấp cho người chi tiêu quyền chi tiêu số lượng token này từ tài khoản của bạn." + }, "personalAddressDetected": { "message": "Đã tìm thấy địa chỉ cá nhân. Nhập địa chỉ hợp đồng token." }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "Mạng tùy chỉnh phổ biến" }, + "popularNetworkAddToolTip": { + "message": "Một vài mạng trong số này phụ thuộc vào bên thứ ba. Kết nối có thể kém tin cậy hơn hoặc cho phép bên thứ ba theo dõi hoạt động. $1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Danh mục đầu tư" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "Trước" }, + "price": { + "message": "Giá" + }, + "priceUnavailable": { + "message": "giá không khả dụng" + }, "primaryCurrencySetting": { "message": "Tiền tệ chính" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "Tỷ giá báo giá" }, + "rank": { + "message": "Xếp hạng" + }, "reAddAccounts": { "message": "thêm lại bất kỳ tài khoản nào khác" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "Nhận" }, - "receiveTokensCamelCase": { - "message": "Nhận token" - }, "recipientAddressPlaceholder": { "message": "Nhập địa chỉ công khai (0x) hoặc tên ENS" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "Đặt lại" }, - "resetStates": { - "message": "Đặt lại trạng thái" - }, "resetWallet": { "message": "Đặt lại ví" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "Tìm kiếm tài khoản" }, + "searchNfts": { + "message": "Tìm kiếm NFT" + }, "searchTokens": { "message": "Tìm kiếm token" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "Bảo mật ví của tôi (khuyến khích)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "Viết ra và cất ở nhiều nơi bí mật." - }, "seedPhraseIntroSidebarBulletOne": { - "message": "Lưu trong một trình quản lý mật khẩu" + "message": "Viết ra và cất ở nhiều nơi bí mật." }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "Lưu giữ trong hộp ký gửi an toàn." }, "seedPhraseIntroSidebarCopyOne": { @@ -4270,10 +4571,10 @@ "message": "Chọn token" }, "selectNFTPrivacyPreference": { - "message": "Bật phát hiện NFT trong phần Cài Đặt" + "message": "Bật tính năng Tự động phát hiện NFT" }, "selectPathHelp": { - "message": "Nếu bạn không thấy các tài khoản như mong đợi, hãy chuyển sang đường dẫn HD." + "message": "Nếu bạn không thấy các tài khoản như mong đợi, hãy chuyển sang đường dẫn HD hoặc mạng đã chọn hiện tại." }, "selectType": { "message": "Chọn loại" @@ -4284,9 +4585,6 @@ "send": { "message": "Gửi" }, - "sendAToken": { - "message": "Gửi token" - }, "sendBugReport": { "message": "Gửi báo cáo lỗi." }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Mạng thử nghiệm Sepolia" }, - "serviceWorkerKeepAlive": { - "message": "Duy Trì Hoạt Động Service Worker" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask sử dụng các dịch vụ của bên thứ ba đáng tin cậy này để nâng cao sự hữu ích và an toàn của sản phẩm." }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "Điều này dựa trên các API khác nhau của bên thứ ba cho mỗi mạng, có thể làm lộ địa chỉ Ethereum và địa chỉ IP của bạn." }, + "showLess": { + "message": "Thu gọn" + }, "showMore": { "message": "Hiển thị thêm" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "Chỉ ký vào thông báo này nếu bạn hoàn toàn hiểu nội dung và tin tưởng trang web yêu cầu." }, - "signatureRequestWarning": { - "message": "Việc ký vào thông báo này có thể gây nguy hiểm. Bạn có thể trao toàn quyền kiểm soát tài khoản và tài sản của mình cho bên kia. Nghĩa là họ có thể tiêu hoặc rút hết tiền trong tài khoản của bạn bất cứ lúc nào. Hãy tiến hành thận trọng. $1." - }, "signed": { "message": "Đã ký" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "Đang ký" }, + "signingInWith": { + "message": "Đăng nhập bằng" + }, "simulationDetailsFailed": { "message": "Đã xảy ra lỗi khi tải kết quả ước tính." }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "Ước tính thay đổi số dư" }, + "siweIssued": { + "message": "Đã phát hành" + }, + "siweNetwork": { + "message": "Mạng" + }, + "siweRequestId": { + "message": "ID yêu cầu" + }, + "siweResources": { + "message": "Tài nguyên" + }, + "siweSignatureSimulationDetailInfo": { + "message": "Bạn đang đăng nhập vào một trang web và dự kiến không có thay đổi nào đối với tài khoản của bạn." + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "Bỏ qua" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "Tài khoản được kiểm soát bởi Snap bên thứ ba." }, + "snapConnectTo": { + "message": "Kết nối với $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "Cho phép $1 tự động kết nối với $2 mà không cần bạn chấp thuận.", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1 muốn sử dụng $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "Trang web" }, + "snapHomeMenu": { + "message": "Trình đơn Trang chủ Snap" + }, "snapInstallRequest": { "message": "Cài đặt $1 sẽ cấp cho nó các quyền sau.", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "Nguồn" }, + "speed": { + "message": "Tốc độ" + }, "speedUp": { "message": "Tăng tốc" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "Hạn mức chi tiêu quá lớn" }, + "spender": { + "message": "Người chi tiêu" + }, "spendingCap": { "message": "Hạn mức chi tiêu" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "Nhật ký trạng thái có chứa các địa chỉ tài khoản công khai của bạn và các giao dịch đã gửi." }, - "states": { - "message": "Trạng thái" - }, "status": { "message": "Trạng thái" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "Đã gửi" }, + "suggestedBySnap": { + "message": "Được đề xuất bởi $1", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "Tên đề xuất:" + }, "suggestedTokenSymbol": { "message": "Mã chứng khoán được đề xuất:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "Tìm nạp báo giá" + "message": "Tìm nạp báo giá..." }, "swapFetchingQuotesErrorDescription": { "message": "Rất tiếc... đã xảy ra sự cố. Hãy thử lại. Nếu lỗi vẫn tiếp diễn, hãy liên hệ với bộ phận hỗ trợ khách hàng." @@ -5434,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "Bạn đã chuyển sang" + "message": "Bạn hiện đang sử dụng" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Khi bạn chuyển mạng, mọi xác nhận đang chờ xử lý sẽ bị hủy" @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "bộ sưu tập này" }, + "threeMonthsAbbreviation": { + "message": "3 Tháng", + "description": "Shortened form of '3 months'" + }, "time": { "message": "Thời gian" }, @@ -5495,45 +5836,6 @@ "message": "Đến: $1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "Bạn có nguy cơ bị tấn công lừa đảo. Tự bảo vệ mình bằng cách tắt eth_sign." - }, - "toggleEthSignDescriptionField": { - "message": "Nếu bật chế độ cài đặt này, bạn có thể nhận các yêu cầu chữ ký mà bạn không đọc được. Khi ký vào một tin nhắn mà bạn không hiểu, bạn có thể đang đồng ý cho đi tiền và NFT của mình." - }, - "toggleEthSignField": { - "message": "Yêu cầu eth_sign" - }, - "toggleEthSignModalBannerBoldText": { - "message": " bạn có thể bị lừa đảo" - }, - "toggleEthSignModalBannerText": { - "message": "Nếu bạn được yêu cầu bật cài đặt này," - }, - "toggleEthSignModalCheckBox": { - "message": "Tôi hiểu rằng tôi có thể mất toàn bộ tiền và NFT của mình nếu tôi kích hoạt yêu cầu eth_sign. " - }, - "toggleEthSignModalDescription": { - "message": "Việc cho phép yêu cầu eth_sign có thể khiến bạn dễ bị tấn công lừa đảo. Nhớ luôn xem lại URL và cẩn thận khi ký các tin nhắn có chứa mã." - }, - "toggleEthSignModalFormError": { - "message": "Văn bản không đúng" - }, - "toggleEthSignModalFormLabel": { - "message": "Nhập “Tôi chỉ ký những gì tôi hiểu” để tiếp tục" - }, - "toggleEthSignModalFormValidation": { - "message": "Tôi chỉ ký những gì tôi hiểu" - }, - "toggleEthSignModalTitle": { - "message": "Bạn tự chịu rủi ro khi sử dụng" - }, - "toggleEthSignOff": { - "message": "TẮT (Khuyến khích)" - }, - "toggleEthSignOn": { - "message": "BẬT (Không khuyến khích)" - }, "toggleRequestQueueDescription": { "message": "Tính năng này cho phép bạn chọn mạng cho từng trang web thay vì một mạng duy nhất được chọn cho tất cả các trang web. Tính năng này sẽ ngăn bạn chuyển đổi mạng theo cách thủ công, điều này có thể ảnh hưởng đến trải nghiệm người dùng của bạn trên một số trang web." }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "Địa chỉ hợp đồng token" }, + "tokenDecimal": { + "message": "Số thập phân của token" + }, "tokenDecimalFetchFailed": { "message": "Yêu cầu số thập phân của token. Tìm trên: $1" }, @@ -5577,13 +5882,16 @@ "message": "ID Token" }, "tokenList": { - "message": "Danh sách token:" + "message": "Danh sách token" }, "tokenScamSecurityRisk": { "message": "rủi ro về bảo mật và lừa đảo token" }, "tokenShowUp": { - "message": "Các token có thể không tự động hiển thị trong ví của bạn." + "message": "Các token có thể không tự động hiển thị trong ví của bạn. " + }, + "tokenStandard": { + "message": "Tiêu chuẩn token" }, "tokenSymbol": { "message": "Ký hiệu token" @@ -5595,6 +5903,9 @@ "message": "Đã tìm thấy $1 token mới", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "Token trong bộ sưu tập" + }, "tooltipApproveButton": { "message": "Tôi đã hiểu" }, @@ -5610,6 +5921,9 @@ "total": { "message": "Tổng" }, + "totalVolume": { + "message": "Tổng khối lượng giao dịch" + }, "transaction": { "message": "giao dịch" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "Đã tạo giao dịch với giá trị $1 lúc $2." }, + "transactionDataFunction": { + "message": "Chức năng" + }, "transactionDetailDappGasMoreInfo": { "message": "Trang web gợi ý" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "Chuyển từ" }, + "trillionAbbreviation": { + "message": "Nghìn Tỷ", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "Chúng tôi đang gặp sự cố khi kết nối với Ledger của bạn. $1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "Theo hồ sơ của chúng tôi, URL này không khớp với nhà cung cấp đã biết cho ID chuỗi này." + }, "unapproved": { "message": "Chưa chấp thuận" }, @@ -5844,12 +6168,21 @@ "update": { "message": "Cập nhật" }, + "updateOrEditNetworkInformations": { + "message": "Cập nhật thông tin của bạn hoặc" + }, "updateRequest": { "message": "Yêu cầu cập nhật" }, "updatedWithDate": { "message": "Đã cập nhật $1" }, + "uploadDropFile": { + "message": "Thả tập tin của bạn vào đây" + }, + "uploadFile": { + "message": "Tải lên tập tin" + }, "urlErrorMsg": { "message": "URL phải có tiền tố HTTP/HTTPS phù hợp." }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "Đây là gì?" }, + "wrongChainId": { + "message": "ID chuỗi này không khớp với tên mạng." + }, + "wrongNetworkName": { + "message": "Theo hồ sơ của chúng tôi, tên mạng có thể không khớp chính xác với ID chuỗi này." + }, "xOfYPending": { "message": "$1/$2 đang chờ xử lý", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "Tài khoản của bạn" }, - "yourFundsMayBeAtRisk": { - "message": "Tiền của bạn có thể gặp rủi ro" + "yourActivity": { + "message": "Hoạt động của bạn" + }, + "yourBalance": { + "message": "Số dư của bạn" }, "yourNFTmayBeAtRisk": { "message": "NFT của bạn có thể gặp rủi ro" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e1696f3164c8..c834c8a18230 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -42,7 +42,7 @@ "message": "关联您的二维码硬件钱包" }, "QRHardwareWalletSteps2Description": { - "message": "Ngrave (即将上线)" + "message": "Ngrave Zero" }, "SIWEAddressInvalid": { "message": "登录请求中的地址与您用于登录的账户地址不匹配。" @@ -162,6 +162,9 @@ "accountSelectionRequired": { "message": "您需要选择一个账户!" }, + "accountTypeNotSupported": { + "message": "账户类型不受支持" + }, "accounts": { "message": "账户" }, @@ -277,6 +280,12 @@ "addNewAccount": { "message": "添加新账户" }, + "addNewBitcoinAccount": { + "message": "添加新的比特币账户(测试版)" + }, + "addNewBitcoinTestnetAccount": { + "message": "添加新的比特币账户(测试网)" + }, "addNewToken": { "message": "添加新代币" }, @@ -286,6 +295,9 @@ "addNfts": { "message": "添加 NFT" }, + "addRpcUrl": { + "message": "添加 RPC(远程过程调用)URL" + }, "addSnapAccountToggle": { "message": "启用“添加账户Snap(测试版)”" }, @@ -305,12 +317,21 @@ "message": "找不到代币?您可以通过粘贴其地址手动添加任何代币。代币合约地址可以在 $1 上找到", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "添加 URL" + }, "addingCustomNetwork": { "message": "正在添加网络" }, "addingTokens": { "message": "正在添加代币" }, + "additionalNetworks": { + "message": "其他网络" + }, + "additionalRpcUrl": { + "message": "其他 RPC(远程过程调用)URL" + }, "address": { "message": "地址" }, @@ -326,6 +347,18 @@ "advancedConfiguration": { "message": "高级配置" }, + "advancedDetailsDataDesc": { + "message": "数据" + }, + "advancedDetailsHexDesc": { + "message": "十六进制" + }, + "advancedDetailsNonceDesc": { + "message": "唯一交易标识号" + }, + "advancedDetailsNonceTooltip": { + "message": "这是一个账户的交易编号。第一笔交易的唯一交易标识号为 0,然后按顺序递增。" + }, "advancedGasFeeDefaultOptIn": { "message": "将这些值保存为 $1 网络的默认值。", "description": "$1 is the current network name." @@ -349,6 +382,18 @@ "alert": { "message": "提醒" }, + "alertActionBuy": { + "message": "买入 ETH" + }, + "alertActionUpdateGas": { + "message": "更新燃料限制" + }, + "alertActionUpdateGasFee": { + "message": "更新费用" + }, + "alertActionUpdateGasFeeLevel": { + "message": "更新燃料选项" + }, "alertBannerMultipleAlertsDescription": { "message": "如果您批准此请求,以欺诈闻名的第三方可能会拿走您的所有资产。" }, @@ -358,6 +403,36 @@ "alertDisableTooltip": { "message": "这可以在“设置 > 提醒”中进行更改" }, + "alertMessageGasEstimateFailed": { + "message": "我们无法提供准确的费用,估算可能较高。我们建议您输入自定义的燃料限制,但交易仍存在失败风险。" + }, + "alertMessageGasFeeLow": { + "message": "在选择低级型费用时,预计交易速度较慢,等待时间较长。如需更快交易,请选择市场型或激进型费用选项。" + }, + "alertMessageGasTooLow": { + "message": "要继续此交易,您需要将燃料限制提高到 21000 或更高。" + }, + "alertMessageInsufficientBalance": { + "message": "您的账户中没有足够的 ETH 来支付交易费用。" + }, + "alertMessageNetworkBusy": { + "message": "燃料价格很高,估算不太准确。" + }, + "alertMessageNoGasPrice": { + "message": "您手动更新费用后,我们才能继续进行此交易。" + }, + "alertMessagePendingTransactions": { + "message": "上一笔交易完成后,此交易才能继续进行。了解如何取消或加快交易。" + }, + "alertMessageSignInDomainMismatch": { + "message": "提出请求的网站不是您正在登录的网站。这可能试图窃取您的登录凭据。" + }, + "alertMessageSignInWrongAccount": { + "message": "此网站要求您使用错误的账户登录。" + }, + "alertMessageSigningOrSubmitting": { + "message": "您的上一笔交易完成后,此交易才能继续进行。" + }, "alertModalAcknowledge": { "message": "我已知晓风险并仍想继续" }, @@ -367,6 +442,33 @@ "alertModalReviewAllAlerts": { "message": "查看所有提醒" }, + "alertReasonGasEstimateFailed": { + "message": "费用不准确" + }, + "alertReasonGasFeeLow": { + "message": "速度慢" + }, + "alertReasonGasTooLow": { + "message": "燃料限制较低" + }, + "alertReasonInsufficientBalance": { + "message": "资金不足" + }, + "alertReasonNetworkBusy": { + "message": "网络繁忙" + }, + "alertReasonNoGasPrice": { + "message": "无法使用费用估算" + }, + "alertReasonPendingTransactions": { + "message": "待定交易" + }, + "alertReasonSignIn": { + "message": "可疑登录请求" + }, + "alertReasonWrongAccount": { + "message": "错误账户" + }, "alertSettingsUnconnectedAccount": { "message": "浏览网站时选择的账户未连接" }, @@ -398,6 +500,12 @@ "allPermissions": { "message": "所有许可" }, + "allTimeHigh": { + "message": "有史以来新高" + }, + "allTimeLow": { + "message": "有史以来新低" + }, "allYourNFTsOf": { "message": "您所有在$1的NFT", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" @@ -436,9 +544,6 @@ "message": "$1 和 $2", "description": "$1 is the first item, $2 is the second item. Used in Snap Install Warning modal." }, - "announcements": { - "message": "公告" - }, "appDescription": { "message": "浏览器中的以太坊钱包", "description": "The description of the application" @@ -524,9 +629,15 @@ "attemptToCancelSwapForFree": { "message": "尝试免费取消兑换" }, + "attributes": { + "message": "属性" + }, "attributions": { "message": "参与者" }, + "auroraRpcDeprecationMessage": { + "message": "Infura RPC(远程过程调用)URL 不再支持 Aurora。" + }, "authorizedPermissions": { "message": "您已授权以下权限" }, @@ -591,6 +702,10 @@ "basicConfigurationBannerTitle": { "message": "基本功能已关闭" }, + "basicConfigurationDescription": { + "message": "MetaMask 通过互联网服务提供代币详情和燃料设置等基本功能。当您使用互联网服务时,您的 IP 地址会共享,在本例中与 MetaMask 共享。这就像您访问任何网站一样。MetaMask 暂时使用这些数据,并且绝不会出卖您的数据。您可以使用 VPN 或关闭这些服务,但这可能会影响您的 MetaMask 体验。要了解详情,请参阅我们的 $1。", + "description": "$1 is to be replaced by the message for privacyMsg, and will link to https://consensys.io/privacy-policy" + }, "basicConfigurationLabel": { "message": "基本功能" }, @@ -634,6 +749,29 @@ "betaWalletCreationSuccessReminder2": { "message": "MetaMask 测试版绝对不会向您索要账户私钥助记词。" }, + "billionAbbreviation": { + "message": "十亿", + "description": "Shortened form of 'billion'" + }, + "bitcoinActivityNotSupported": { + "message": "比特币活动不受支持" + }, + "bitcoinSupportSectionTitle": { + "message": "比特币" + }, + "bitcoinSupportToggleDescription": { + "message": "启用此功能后,您可以选择将比特币账户添加到衍生自现有私钥助记词的 MetaMask Extension 中。这是一个实验性的测试版功能,因此您应自行承担使用风险。为了向我们提供有关此新比特币体验的反馈,请填写此 $1。", + "description": "$1 is the link to a product feedback form" + }, + "bitcoinSupportToggleTitle": { + "message": "启用“添加新的比特币账户(测试版)”" + }, + "bitcoinTestnetSupportToggleDescription": { + "message": "启用此功能后,您可以选择为测试网络添加比特币账户。" + }, + "bitcoinTestnetSupportToggleTitle": { + "message": "启用“添加新的比特币账户(测试网)”" + }, "blockExplorerAccountAction": { "message": "账户", "description": "This is used with viewOnEtherscan and viewInExplorer e.g View Account in Explorer" @@ -659,6 +797,9 @@ "blockaid": { "message": "Blockaid" }, + "blockaidAlertInfo": { + "message": "我们不建议继续处理此请求。" + }, "blockaidDescriptionApproveFarming": { "message": "如果您批准此请求,以欺诈闻名的第三方可能会拿走您的所有资产。" }, @@ -680,6 +821,9 @@ "blockaidDescriptionTransferFarming": { "message": "如果您批准此请求,以欺诈闻名的第三方将会拿走您的所有资产。" }, + "blockaidDescriptionWarning": { + "message": "这可能是欺骗性请求。仅在您信任所涉及的每个地址时才能继续。" + }, "blockaidMessage": { "message": "隐私保护 - 不会与第三方共享任何数据。适用于 Arbitrum、Avalanche、BNB Chain、以太坊主网、Linea、Optimism、Polygon、Base 和 Sepolia。" }, @@ -695,6 +839,9 @@ "blockies": { "message": "Blockies" }, + "boughtFor": { + "message": "购买用于" + }, "bridge": { "message": "跨链桥" }, @@ -714,7 +861,7 @@ "message": "忙碌中" }, "buyAndSell": { - "message": "买入和卖出" + "message": "出入金" }, "buyAsset": { "message": "购买$1", @@ -779,6 +926,9 @@ "chromeRequiredForHardwareWallets": { "message": "您需要在 Google Chrome 上使用 MetaMask 以连接到您的硬件钱包。" }, + "circulatingSupply": { + "message": "循环供应" + }, "clear": { "message": "清除" }, @@ -813,6 +963,9 @@ "coingecko": { "message": "CoinGecko" }, + "collectionName": { + "message": "收藏品名称" + }, "comboNoOptions": { "message": "找不到任何选项", "description": "Default text shown in the combo field dropdown if no options." @@ -838,6 +991,9 @@ "confirmAlertModalAcknowledgeMultiple": { "message": "我已知晓提醒并仍想继续" }, + "confirmAlertModalAcknowledgeSingle": { + "message": "我已知晓提醒并仍想继续" + }, "confirmAlertModalDetails": { "message": "如果您登录,以欺诈闻名的第三方可能会拿走您的所有资产。在继续操作之前,请查看提醒。" }, @@ -853,18 +1009,39 @@ "confirmConnectionTitle": { "message": "确认连接到$1" }, + "confirmDeletion": { + "message": "确认删除" + }, + "confirmFieldPaymaster": { + "message": "费用支付方" + }, + "confirmFieldTooltipPaymaster": { + "message": "此交易的费用将由 paymaster 智能合约支付。" + }, "confirmPassword": { "message": "确认密码" }, "confirmRecoveryPhrase": { "message": "确认私钥助记词" }, - "confirmTitleDescContractInteractionTransaction": { - "message": "仅在您完全理解内容并信任请求网站的情况下,才能确认此交易。" + "confirmRpcUrlDeletionMessage": { + "message": "您确定要删除 RPC(远程过程调用)URL 吗?您的信息将不会保存到此网络。" + }, + "confirmTitleDescPermitSignature": { + "message": "此网站需要获得许可来使用您的代币。" + }, + "confirmTitleDescSIWESignature": { + "message": "某个网站要求您登录以证明您拥有该账户。" }, "confirmTitleDescSignature": { "message": "仅在您批准该内容并信任请求网站的情况下,才能确认此消息。" }, + "confirmTitlePermitSignature": { + "message": "支出上限请求" + }, + "confirmTitleSIWESignature": { + "message": "登录请求" + }, "confirmTitleSignature": { "message": "签名请求" }, @@ -1091,6 +1268,9 @@ "createSnapAccountTitle": { "message": "创建账户" }, + "creatorAddress": { + "message": "创建者地址" + }, "crossChainSwapsLink": { "message": "使用 MetaMask Portfolio 跨网络兑换" }, @@ -1260,9 +1440,27 @@ "data": { "message": "数据" }, + "dataCollectionForMarketing": { + "message": "用于营销目的的数据收集" + }, + "dataCollectionForMarketingDescription": { + "message": "我们将使用 MetaMetrics 来了解您如何与我们的营销通信交互。我们可能会分享相关资讯(例如产品特点和其他内容)。" + }, + "dataCollectionWarningPopoverButton": { + "message": "好的" + }, + "dataCollectionWarningPopoverDescription": { + "message": "您关闭了我们用于营销目的的数据收集。此操作仅适用于此设备。如果您在其他设备上使用 MetaMask,请务必也在其他设备上选择关闭。" + }, "dataHex": { "message": "十六进制" }, + "dataUnavailable": { + "message": "数据不可用" + }, + "dateCreated": { + "message": "创建日期" + }, "dcent": { "message": "D'Cent" }, @@ -1292,6 +1490,9 @@ "decryptRequest": { "message": "解密请求" }, + "defaultRpcUrl": { + "message": "默认 RPC(远程过程调用)URL" + }, "delete": { "message": "删除" }, @@ -1308,6 +1509,9 @@ "message": "要删除$1网络吗?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "删除 RPC(远程过程调用)URL" + }, "deposit": { "message": "保证金" }, @@ -1333,18 +1537,6 @@ "details": { "message": "详细信息" }, - "developerOptions": { - "message": "开发者选项" - }, - "developerOptionsResetStatesAnnouncementsDescription": { - "message": "将所有公告的布尔值显示重设为 false。公告是显示在“最新资讯”(What's New)弹出模式中的通知。" - }, - "developerOptionsResetStatesOnboarding": { - "message": "重设与入门相关的各种状态,并重定向到“保护您的钱包”入门页面。" - }, - "developerOptionsServiceWorkerKeepAlive": { - "message": "导致时间戳持续保存到 session.storage" - }, "disabledGasOptionToolTipMessage": { "message": "“$1”已被禁用,因为它不满足在原来的燃料费用基础上至少增加10%的要求。", "description": "$1 is gas estimate type which can be market or aggressive" @@ -1521,6 +1713,9 @@ "editGasTooLow": { "message": "处理时间未知" }, + "editNetworkLink": { + "message": "编辑原始网络" + }, "editNonceField": { "message": "编辑 nonce" }, @@ -1549,12 +1744,12 @@ "message": "启用 $1", "description": "$1 is a token symbol, e.g. ETH" }, - "enableTokenAutoDetection": { - "message": "启用代币自动检测" - }, "enabled": { "message": "已启用" }, + "enabledNetworks": { + "message": "启用的网络" + }, "encryptionPublicKeyNotice": { "message": "$1 想要您的加密公钥。同意后,该网站将可以向您发送加密消息。", "description": "$1 is the web3 site name" @@ -1659,6 +1854,9 @@ "estimatedFee": { "message": "预估费用" }, + "estimatedFeeTooltip": { + "message": "为在网络上处理交易而支付的金额。" + }, "ethGasPriceFetchWarning": { "message": "由于目前主要的燃料估算服务不可用,因此提供了备用燃料价格。" }, @@ -1678,6 +1876,12 @@ "etherscanViewOn": { "message": "在 Etherscan 上查看" }, + "existingChainId": { + "message": "您输入的信息与现有的链 ID 相关联。" + }, + "existingRpcUrl": { + "message": "此 URL 与另一个链 ID 相关联。" + }, "expandView": { "message": "展开视图" }, @@ -1732,6 +1936,9 @@ "message": "文件导入失败?点击这里!", "description": "Helps user import their account from a JSON file" }, + "findTheRightChainId": { + "message": "在下方找到合适的:" + }, "flaskWelcomeUninstall": { "message": "您应该卸载此扩展程序", "description": "This request is shown on the Flask Welcome screen. It is intended for non-developers, and will be bolded." @@ -1771,6 +1978,9 @@ "forgotPassword": { "message": "忘记密码了?" }, + "form": { + "message": "表格" + }, "from": { "message": "自" }, @@ -1901,7 +2111,7 @@ "message": "Goerli 测试网络" }, "gotIt": { - "message": "知道了!" + "message": "知道了" }, "grantedToWithColon": { "message": "授予:" @@ -1979,6 +2189,12 @@ "highLowercase": { "message": "高" }, + "highestCurrentBid": { + "message": "当前最高出价" + }, + "highestFloorPrice": { + "message": "最高底价" + }, "history": { "message": "历史记录" }, @@ -2318,12 +2534,21 @@ "knownTokenWarning": { "message": "此操作将编辑已经在您的钱包中列出的代币,有肯能被用来欺骗您。只有确定要更改这些代币的内容时,才通过此操作。了解更多关于 $1" }, + "l1Fee": { + "message": "L1 费用" + }, + "l1FeeTooltip": { + "message": "L1 燃料费" + }, + "l2Fee": { + "message": "L2 费用" + }, + "l2FeeTooltip": { + "message": "L2 燃料费" + }, "lastConnected": { "message": "最后连接" }, - "lastPriceSold": { - "message": "最后售价" - }, "lastSold": { "message": "最后售出" }, @@ -2495,6 +2720,12 @@ "message": "请确保没有人在看您的屏幕", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, + "marketCap": { + "message": "市值" + }, + "marketDetails": { + "message": "市场详情" + }, "max": { "message": "最大" }, @@ -2504,6 +2735,9 @@ "maxFee": { "message": "最大费用" }, + "maxFeeTooltip": { + "message": "为支付交易而提供的最高费用。" + }, "maxPriorityFee": { "message": "最大优先费用" }, @@ -2551,8 +2785,8 @@ "methodData": { "message": "方法" }, - "methodDataTransactionDescription": { - "message": "这是将要采取的具体行动。该数据可能是伪造的,因此请确保您信任另一端的网站。" + "methodDataTransactionDesc": { + "message": "基于解码输入数据而执行的功能。" }, "methodNotSupported": { "message": "不支持此账户。" @@ -2560,6 +2794,10 @@ "metrics": { "message": "指标" }, + "millionAbbreviation": { + "message": "百万", + "description": "Shortened form of 'million'" + }, "mismatchAccount": { "message": "您选中的账户($1)与尝试登录的账户($2)不同" }, @@ -2672,7 +2910,7 @@ "description": "Description below header used on Permission Connect screen for native permissions." }, "nativeToken": { - "message": "此网络上的原生代币为$1。它是用于燃料费的代币。", + "message": "此网络上的原生代币为 $1。它是用于燃料费的代币。 ", "description": "$1 represents the name of the native token on the current network" }, "nativeTokenScamWarningConversion": { @@ -2741,6 +2979,9 @@ "networkNameBase": { "message": "基础" }, + "networkNameBitcoin": { + "message": "比特币" + }, "networkNameDefinition": { "message": "与此网络关联的名称。" }, @@ -2765,6 +3006,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "网络选项" + }, "networkProvider": { "message": "网络提供商" }, @@ -2833,6 +3077,9 @@ "newNetworkAdded": { "message": "成功添加了 “$1”!" }, + "newNetworkEdited": { + "message": "“$1” 已经成功编辑!" + }, "newNftAddedMessage": { "message": "NFT已成功添加!" }, @@ -2869,6 +3116,9 @@ "nftAlreadyAdded": { "message": "此NFT已添加。" }, + "nftAutoDetectionEnabled": { + "message": "已启用 NFT 自动检测" + }, "nftDisclaimer": { "message": "免责声明:MetaMask 从源网址中提取媒体文件。该网址有时会因铸造 NFT 的市场而改变。" }, @@ -2919,6 +3169,9 @@ "noDomainResolution": { "message": "没有提供域名解析。" }, + "noHardwareWalletOrSnapsSupport": { + "message": "Snaps 和大多数硬件钱包与您的当前浏览器版本不兼容。" + }, "noNFTs": { "message": "尚无 NFT" }, @@ -2949,8 +3202,8 @@ "nonceField": { "message": "自定义交易 nonce" }, - "nonceFieldDescription": { - "message": "打开这个功能可以改变确认屏幕上的 nonce(交易号码)。这是一个高级功能,请谨慎使用。" + "nonceFieldDesc": { + "message": "启用此功能可更改发送资产时的唯一交易标识号(交易编号)。这是一个高级功能,请谨慎使用。" }, "nonceFieldHeading": { "message": "自定义 nonce" @@ -3152,6 +3405,9 @@ "numberOfNewTokensDetectedSingular": { "message": "在此账户中找到1枚新代币" }, + "numberOfTokens": { + "message": "代币数量" + }, "ofTextNofM": { "message": "/" }, @@ -3167,8 +3423,36 @@ "on": { "message": "开" }, - "onboarding": { - "message": "入门" + "onboardedMetametricsAccept": { + "message": "我同意" + }, + "onboardedMetametricsDisagree": { + "message": "不,谢谢" + }, + "onboardedMetametricsKey1": { + "message": "最新发展动态" + }, + "onboardedMetametricsKey2": { + "message": "产品特点" + }, + "onboardedMetametricsKey3": { + "message": "其他相关宣传材料" + }, + "onboardedMetametricsLink": { + "message": "MetaMetrics" + }, + "onboardedMetametricsParagraph1": { + "message": "除了 $1,我们还想使用数据来了解您如何与营销通信交互。", + "description": "$1 represents the 'onboardedMetametricsLink' locale string" + }, + "onboardedMetametricsParagraph2": { + "message": "这可以帮助我们个性化与您分享的内容,例如:" + }, + "onboardedMetametricsParagraph3": { + "message": "请记住,我们绝不会出卖您提供的数据,而且您可以随时选择退出。" + }, + "onboardedMetametricsTitle": { + "message": "帮助我们改善您的体验" }, "onboardingAdvancedPrivacyIPFSDescription": { "message": "IPFS 网关使访问和查看第三方托管的数据成为可能。您可以添加自定义 IPFS 网关或继续使用默认网关。" @@ -3206,9 +3490,6 @@ "onboardingMetametricsDescription2": { "message": "当我们收集指标时,总是..." }, - "onboardingMetametricsDisagree": { - "message": "不,谢谢" - }, "onboardingMetametricsInfuraTerms": { "message": "如果我们决定将这些数据用于其他目的,我们会通知您。您可以查看我们的 $1 以了解更多信息。请记住,您可以随时转到设置并选择退出。", "description": "$1 represents `onboardingMetametricsInfuraTermsPolicy`" @@ -3243,6 +3524,9 @@ "onboardingMetametricsTitle": { "message": "请帮助我们改进 MetaMask" }, + "onboardingMetametricsUseDataCheckbox": { + "message": "我们将使用此数据来了解您如何与我们的营销通信交互。我们可能会分享相关资讯(例如产品特点)。" + }, "onboardingPinExtensionBillboardAccess": { "message": "完全访问权限" }, @@ -3286,6 +3570,22 @@ "message": "网络钓鱼检测提醒依赖于与 $1 的通信。jsDeliver 将有权访问您的 IP 地址。查看 $2。", "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" }, + "oneDayAbbreviation": { + "message": "1 天", + "description": "Shortened form of '1 day'" + }, + "oneMonthAbbreviation": { + "message": "1 个月", + "description": "Shortened form of '1 month'" + }, + "oneWeekAbbreviation": { + "message": "1 周", + "description": "Shortened form of '1 week'" + }, + "oneYearAbbreviation": { + "message": "1 年", + "description": "Shortened form of '1 year'" + }, "onekey": { "message": "OneKey" }, @@ -3310,15 +3610,6 @@ "openSeaNew": { "message": "OpenSea" }, - "openSeaToBlockaidBtnLabel": { - "message": "探索 Snap" - }, - "openSeaToBlockaidDescription": { - "message": "此网络不再提供安全提醒。安装 Snap 可能提高安全性。" - }, - "openSeaToBlockaidTitle": { - "message": "注意!" - }, "operationFailed": { "message": "操作失败" }, @@ -3647,6 +3938,9 @@ "permissionsPageTourTitle": { "message": "已连接的站点现已获得许可" }, + "permitSimulationDetailInfo": { + "message": "您将授予该消费者许可从您的账户中支出这些代币。" + }, "personalAddressDetected": { "message": "检测到个人地址。请输入代币合约地址。" }, @@ -3679,6 +3973,10 @@ "popularCustomNetworks": { "message": "流行自定义网络" }, + "popularNetworkAddToolTip": { + "message": "这些网络中的其中一些依赖于第三方。此连接可能不太可靠,或使第三方可进行活动跟踪。$1", + "description": "$1 is Learn more link" + }, "portfolio": { "message": "Portfolio" }, @@ -3691,6 +3989,12 @@ "prev": { "message": "上一个" }, + "price": { + "message": "价格" + }, + "priceUnavailable": { + "message": "价格不可用" + }, "primaryCurrencySetting": { "message": "主要货币" }, @@ -3843,6 +4147,9 @@ "quoteRate": { "message": "报价" }, + "rank": { + "message": "排名" + }, "reAddAccounts": { "message": "重新添加任何其他账户" }, @@ -3855,9 +4162,6 @@ "receive": { "message": "收款" }, - "receiveTokensCamelCase": { - "message": "收取代币" - }, "recipientAddressPlaceholder": { "message": "输入公钥 (0x) 或 ENS 名称" }, @@ -4012,9 +4316,6 @@ "reset": { "message": "重置" }, - "resetStates": { - "message": "重设状态" - }, "resetWallet": { "message": "重置钱包" }, @@ -4150,6 +4451,9 @@ "searchAccounts": { "message": "搜索账户" }, + "searchNfts": { + "message": "搜索 NFT" + }, "searchTokens": { "message": "搜索代币" }, @@ -4194,13 +4498,10 @@ "seedPhraseIntroRecommendedButtonCopy": { "message": "保护我的钱包(推荐)" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "写下并存储在多个秘密位置。" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "保存到密码管理工具" + "message": "写下并存储在多个秘密位置。" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "安全存放在保险箱内。" }, "seedPhraseIntroSidebarCopyOne": { @@ -4284,9 +4585,6 @@ "send": { "message": "发送" }, - "sendAToken": { - "message": "发送代币" - }, "sendBugReport": { "message": "向我们发送错误报告。" }, @@ -4337,9 +4635,6 @@ "sepolia": { "message": "Sepolia测试网络" }, - "serviceWorkerKeepAlive": { - "message": "Service Worker 保持活跃" - }, "setAdvancedPrivacySettingsDetails": { "message": "MetaMask 使用这些可信的第三方服务来提高产品可用性和安全性。" }, @@ -4396,6 +4691,9 @@ "showIncomingTransactionsExplainer": { "message": "这依赖于每个网络的不同第三方 API,这些 API 会导致您的以太坊地址和 IP 地址被获悉。" }, + "showLess": { + "message": "收起" + }, "showMore": { "message": "展开" }, @@ -4426,9 +4724,6 @@ "signatureRequestGuidance": { "message": "只有在您完全理解内容并信任请求网站的情况下,才能签署此消息。" }, - "signatureRequestWarning": { - "message": "签署此消息可能会很危险。您可能会将您的账户和资产的全部控制权交给此消息另一端的一方。这意味着他们可以随时耗尽您的账户。请谨慎行事。$1。" - }, "signed": { "message": "已签名" }, @@ -4438,6 +4733,9 @@ "signing": { "message": "签名" }, + "signingInWith": { + "message": "使用以下登录方式" + }, "simulationDetailsFailed": { "message": "加载估算时出错。" }, @@ -4475,6 +4773,24 @@ "simulationsSettingSubHeader": { "message": "预计余额变化" }, + "siweIssued": { + "message": "已签发" + }, + "siweNetwork": { + "message": "网络" + }, + "siweRequestId": { + "message": "请求 ID" + }, + "siweResources": { + "message": "资源" + }, + "siweSignatureSimulationDetailInfo": { + "message": "您正在登录某个网站,并且您的账户没有预期变化。" + }, + "siweURI": { + "message": "URL" + }, "skip": { "message": "跳过" }, @@ -4578,6 +4894,14 @@ "snapAccountsDescription": { "message": "由第三方 Snap 控制的账户。" }, + "snapConnectTo": { + "message": "连接到 $1", + "description": "$1 is the website URL or a Snap name. Used for Snaps pre-approved connections." + }, + "snapConnectionPermissionDescription": { + "message": "让 $1 自动连接到 $2,无需您的批准。", + "description": "Used for Snap pre-approved connections. $1 is the Snap name, $2 is a website URL." + }, "snapConnectionWarning": { "message": "$1想连接$2。", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." @@ -4589,6 +4913,9 @@ "snapDetailWebsite": { "message": "网站" }, + "snapHomeMenu": { + "message": "Snap 主菜单" + }, "snapInstallRequest": { "message": "安装 $1 即赋予其以下许可。", "description": "$1 is the snap name." @@ -4711,6 +5038,9 @@ "source": { "message": "来源" }, + "speed": { + "message": "速度" + }, "speedUp": { "message": "加速" }, @@ -4745,6 +5075,9 @@ "spendLimitTooLarge": { "message": "消费限额过大" }, + "spender": { + "message": "消费者" + }, "spendingCap": { "message": "支出上限" }, @@ -4865,9 +5198,6 @@ "stateLogsDescription": { "message": "状态日志包含您的公共账户地址和已发送的交易。" }, - "states": { - "message": "状态" - }, "status": { "message": "状态" }, @@ -4972,6 +5302,13 @@ "submitted": { "message": "已提交" }, + "suggestedBySnap": { + "message": "由 $1 建议", + "description": "$1 is the snap name" + }, + "suggestedTokenName": { + "message": "建议名称:" + }, "suggestedTokenSymbol": { "message": "建议的股票代码:" }, @@ -5086,7 +5423,7 @@ "description": "A count of possible quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of resources that we check for quotes. Keep in mind that not all resources will have a quote for a particular swap." }, "swapFetchingQuotes": { - "message": "取得报价中" + "message": "获取报价中……" }, "swapFetchingQuotesErrorDescription": { "message": "呃……出错了。再试一次,如果错误仍存在,请联系客服。" @@ -5434,7 +5771,7 @@ "description": "$1 represents the account name, $2 represents the network name" }, "switchedTo": { - "message": "您已切换到" + "message": "您现在使用的是" }, "switchingNetworksCancelsPendingConfirmations": { "message": "切换网络将取消所有待处理的确认" @@ -5482,6 +5819,10 @@ "thisCollection": { "message": "这个收藏品" }, + "threeMonthsAbbreviation": { + "message": "3 个月", + "description": "Shortened form of '3 months'" + }, "time": { "message": "时间" }, @@ -5495,45 +5836,6 @@ "message": "至:$1", "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" }, - "toggleEthSignBannerDescription": { - "message": "您面临网络钓鱼攻击的风险。通过关闭 eth_sign 保护自己。" - }, - "toggleEthSignDescriptionField": { - "message": "如果启用此设置,您可能会收到不可读的签名请求。通过签署一条您不理解的信息,您可能同意放弃您的资金和 NFT。" - }, - "toggleEthSignField": { - "message": "Eth_sign 请求" - }, - "toggleEthSignModalBannerBoldText": { - "message": " 您可能遭受了欺诈" - }, - "toggleEthSignModalBannerText": { - "message": "如果要求您打开此设置," - }, - "toggleEthSignModalCheckBox": { - "message": "我明白,如果我启用 eth_sign 请求,我可能失去所有资金和 NFT。 " - }, - "toggleEthSignModalDescription": { - "message": "允许 eth_sign 请求可能会使您易于受到网络钓鱼攻击。始终检查 URL,并且在签署包含代码的消息时保持谨慎。" - }, - "toggleEthSignModalFormError": { - "message": "文本不正确" - }, - "toggleEthSignModalFormLabel": { - "message": "输入“我只签署我理解的内容”以继续" - }, - "toggleEthSignModalFormValidation": { - "message": "我只签署我理解的内容" - }, - "toggleEthSignModalTitle": { - "message": "使用风险自负" - }, - "toggleEthSignOff": { - "message": "关闭(推荐)" - }, - "toggleEthSignOn": { - "message": "开启(不推荐)" - }, "toggleRequestQueueDescription": { "message": "这使您可以为每个网站选择网络,而不是为所有网站选择同一个网络。此功能将阻止您手动切换网络,这可能会破坏您在某些网站上的用户体验。" }, @@ -5561,6 +5863,9 @@ "tokenContractAddress": { "message": "代币合约地址" }, + "tokenDecimal": { + "message": "代币小数位" + }, "tokenDecimalFetchFailed": { "message": "需要代币小数位。请在下方查找:$1" }, @@ -5585,6 +5890,9 @@ "tokenShowUp": { "message": "您的代币可能不会自动显示在您的钱包中。" }, + "tokenStandard": { + "message": "代币标准" + }, "tokenSymbol": { "message": "代币符号" }, @@ -5595,6 +5903,9 @@ "message": "发现$1新代币", "description": "$1 is the number of new tokens detected" }, + "tokensInCollection": { + "message": "收藏品中的代币" + }, "tooltipApproveButton": { "message": "我理解" }, @@ -5610,6 +5921,9 @@ "total": { "message": "共计" }, + "totalVolume": { + "message": "总交易额" + }, "transaction": { "message": "交易" }, @@ -5625,6 +5939,9 @@ "transactionCreated": { "message": "在 $2 创建了值为 $1 的交易。" }, + "transactionDataFunction": { + "message": "功能" + }, "transactionDetailDappGasMoreInfo": { "message": "建议的网站" }, @@ -5715,6 +6032,10 @@ "transferFrom": { "message": "转账自" }, + "trillionAbbreviation": { + "message": "万亿", + "description": "Shortened form of 'trillion'" + }, "troubleConnectingToLedgerU2FOnFirefox": { "message": "我们在连接您的 Ledger 时遇到问题。$1", "description": "$1 is a link to the wallet connection guide;" @@ -5793,6 +6114,9 @@ "message": "U2F", "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." }, + "unMatchedChain": { + "message": "根据我们的记录,此 URL 与此链 ID 的已知提供者不匹配。" + }, "unapproved": { "message": "未批准" }, @@ -5844,12 +6168,21 @@ "update": { "message": "更新" }, + "updateOrEditNetworkInformations": { + "message": "更新您的信息或者" + }, "updateRequest": { "message": "更新请求" }, "updatedWithDate": { "message": "已于 $1 更新" }, + "uploadDropFile": { + "message": "将您的文件放在此处" + }, + "uploadFile": { + "message": "上传文件" + }, "urlErrorMsg": { "message": "URL 需要相应的 HTTP/HTTPS 前缀。" }, @@ -6065,6 +6398,12 @@ "whatsThis": { "message": "这是什么?" }, + "wrongChainId": { + "message": "此链 ID 与网络名称不匹配。" + }, + "wrongNetworkName": { + "message": "根据我们的记录,该网络名称可能与此链 ID 不匹配。" + }, "xOfYPending": { "message": "$1 / $2 待处理", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" @@ -6088,8 +6427,11 @@ "yourAccounts": { "message": "您的账户" }, - "yourFundsMayBeAtRisk": { - "message": "您的资金可能面临风险" + "yourActivity": { + "message": "您的活动" + }, + "yourBalance": { + "message": "您的余额" }, "yourNFTmayBeAtRisk": { "message": "您的 NFT 可能面临风险" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index d91d749ccffa..83c0fc09e565 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -893,9 +893,6 @@ "nonceField": { "message": "自訂交易 nonce" }, - "nonceFieldDescription": { - "message": "打開此選項來在交易確認視窗中更改 nonce (交易編號)。這是一個進階功能,請小心使用。" - }, "nonceFieldHeading": { "message": "自訂 nonce" }, @@ -1087,13 +1084,10 @@ "securityAndPrivacy": { "message": "安全&隱私" }, - "seedPhraseIntroSidebarBulletFour": { - "message": "寫下來並且存放在不同的秘密地點。" - }, "seedPhraseIntroSidebarBulletOne": { - "message": "儲存在密碼管理器裡" + "message": "寫下來並且存放在不同的秘密地點。" }, - "seedPhraseIntroSidebarBulletThree": { + "seedPhraseIntroSidebarBulletTwo": { "message": "存在安全保管箱裡。" }, "seedPhraseIntroSidebarCopyOne": { diff --git a/app/background.html b/app/background.html index c0068295d730..9892810f3ffa 100644 --- a/app/background.html +++ b/app/background.html @@ -4,7 +4,6 @@ - - + diff --git a/app/home.html b/app/home.html index b94800b13d78..7308064505b0 100644 --- a/app/home.html +++ b/app/home.html @@ -2,13 +2,13 @@ - + <% if (it.isMMI && !it.isTest) { %> MetaMask Institutional <% } else { %> MetaMask <% } %> - +
@@ -16,6 +16,6 @@
- + diff --git a/app/images/bnb.png b/app/images/bnb.png index 64ef1c940b82..0fc84b2c6e71 100644 Binary files a/app/images/bnb.png and b/app/images/bnb.png differ diff --git a/app/images/bnb.svg b/app/images/bnb.svg index f39dba1be163..b1f1841b84e0 100644 --- a/app/images/bnb.svg +++ b/app/images/bnb.svg @@ -1,20 +1,3 @@ - - - - - - - - - - + + diff --git a/app/images/flare-mainnet.svg b/app/images/flare-mainnet.svg index f2ec35a35668..d1d42c278f25 100644 --- a/app/images/flare-mainnet.svg +++ b/app/images/flare-mainnet.svg @@ -1 +1,13 @@ - \ No newline at end of file + + + + + + + + + + + + + diff --git a/app/images/icons/arrow-left.svg b/app/images/icons/arrow-left.svg index fafa71811f79..b725fa4acb15 100644 --- a/app/images/icons/arrow-left.svg +++ b/app/images/icons/arrow-left.svg @@ -1,3 +1,3 @@ - + diff --git a/app/images/icons/arrow-right.svg b/app/images/icons/arrow-right.svg index 95f99af30a16..ff2bd612f662 100644 --- a/app/images/icons/arrow-right.svg +++ b/app/images/icons/arrow-right.svg @@ -1,3 +1,3 @@ - + diff --git a/app/images/icons/more-horizontal.svg b/app/images/icons/more-horizontal.svg index 504ec123ad24..844ac6276f2f 100644 --- a/app/images/icons/more-horizontal.svg +++ b/app/images/icons/more-horizontal.svg @@ -1,3 +1,3 @@ - + diff --git a/app/images/icons/more-vertical.svg b/app/images/icons/more-vertical.svg index 72d8ba1647b9..d295a71996ed 100644 --- a/app/images/icons/more-vertical.svg +++ b/app/images/icons/more-vertical.svg @@ -1,3 +1,3 @@ - + diff --git a/app/images/iotex-token.svg b/app/images/iotex-token.svg new file mode 100644 index 000000000000..49cba53d5d40 --- /dev/null +++ b/app/images/iotex-token.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/iotex.svg b/app/images/iotex.svg new file mode 100644 index 000000000000..7594ba1bd24b --- /dev/null +++ b/app/images/iotex.svg @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/matic-token.png b/app/images/matic-token.png deleted file mode 100644 index b154c46ea543..000000000000 Binary files a/app/images/matic-token.png and /dev/null differ diff --git a/app/images/moonbeam-token.svg b/app/images/moonbeam-token.svg new file mode 100644 index 000000000000..ecb8ebc3c4f5 --- /dev/null +++ b/app/images/moonbeam-token.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/moonbeam.svg b/app/images/moonbeam.svg index 106338e4f1e0..85b8b5f8e0de 100644 --- a/app/images/moonbeam.svg +++ b/app/images/moonbeam.svg @@ -1,16 +1,45 @@ - - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/app/images/moonriver-token.svg b/app/images/moonriver-token.svg new file mode 100644 index 000000000000..852a04de84d2 --- /dev/null +++ b/app/images/moonriver-token.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/moonriver.svg b/app/images/moonriver.svg index b52ac55335e1..3e80c62c032a 100644 --- a/app/images/moonriver.svg +++ b/app/images/moonriver.svg @@ -1,48 +1,62 @@ - - - - - - - - - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/app/images/near.svg b/app/images/near.svg new file mode 100644 index 000000000000..215c3ba06135 --- /dev/null +++ b/app/images/near.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/images/matic-token.svg b/app/images/pol-token.svg similarity index 100% rename from app/images/matic-token.svg rename to app/images/pol-token.svg diff --git a/app/images/ramps-card-btc-illustration.png b/app/images/ramps-card-btc-illustration.png new file mode 100644 index 000000000000..4c5cee4e40ac Binary files /dev/null and b/app/images/ramps-card-btc-illustration.png differ diff --git a/app/images/songbird.svg b/app/images/songbird.svg index 3adced1d1d69..e82207af073d 100644 --- a/app/images/songbird.svg +++ b/app/images/songbird.svg @@ -1 +1,20 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/wallet-alpha.png b/app/images/wallet-alpha.png deleted file mode 100644 index bc88bcfb5f2d..000000000000 Binary files a/app/images/wallet-alpha.png and /dev/null differ diff --git a/app/loading.html b/app/loading.html index 1c85bd296cc6..6319f760fb23 100644 --- a/app/loading.html +++ b/app/loading.html @@ -2,13 +2,13 @@ - <% if (it.shouldIncludeSnow) { %> - - - <% } %> MetaMask Loading + <% if (it.shouldIncludeSnow) { %> + + + <% } %> - +
@@ -50,6 +50,6 @@ />
- + diff --git a/app/popup-init.html b/app/popup-init.html new file mode 100644 index 000000000000..2fb79fefcfde --- /dev/null +++ b/app/popup-init.html @@ -0,0 +1,39 @@ + + + + + + + MetaMask + + + diff --git a/app/popup.html b/app/popup.html index 296b0ceae711..92806e42047a 100644 --- a/app/popup.html +++ b/app/popup.html @@ -2,9 +2,9 @@ - + MetaMask - +
@@ -12,6 +12,6 @@
- + diff --git a/app/scripts/background.js b/app/scripts/background.js index c6a572601bb2..45fcd0315c10 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -19,6 +19,9 @@ import { ApprovalType } from '@metamask/controller-utils'; import PortStream from 'extension-port-stream'; import { ethErrors } from 'eth-rpc-errors'; +import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; +import { NotificationServicesController } from '@metamask/notification-services-controller'; + import { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, @@ -41,6 +44,16 @@ import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.ut import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { maskObject } from '../../shared/modules/object.utils'; import { FIXTURE_STATE_METADATA_VERSION } from '../../test/e2e/default-fixture'; +import { getSocketBackgroundToMocha } from '../../test/e2e/background-socket/socket-background-to-mocha'; +import { + OffscreenCommunicationTarget, + OffscreenCommunicationEvents, +} from '../../shared/constants/offscreen-communication'; +import { + FakeLedgerBridge, + FakeTrezorBridge, +} from '../../test/stub/keyring-bridge'; +import { getCurrentChainId } from '../../ui/selectors'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -69,8 +82,6 @@ import { createOffscreen } from './offscreen'; /* eslint-enable import/first */ -import { TRIGGER_TYPES } from './controllers/metamask-notifications/constants/notification-schema'; - // eslint-disable-next-line @metamask/design-tokens/color-no-hex const BADGE_COLOR_APPROVAL = '#0376C9'; // eslint-disable-next-line @metamask/design-tokens/color-no-hex @@ -113,6 +124,9 @@ if (inTest || process.env.METAMASK_DEBUG) { } const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL); +// normalized (adds a trailing slash to the end of the domain if it's missing) +// the URL once and reuse it: +const phishingPageHref = phishingPageUrl.toString(); const ONE_SECOND_IN_MILLISECONDS = 1_000; // Timeout for initializing phishing warning page. @@ -183,6 +197,131 @@ const sendReadyMessageToTabs = async () => { } }; +/** + * Detects known phishing pages as soon as the browser begins to load the + * page. If the page is a known phishing page, the user is redirected to the + * phishing warning page. + * + * This detection works even if the phishing page is now a redirect to a new + * domain that our phishing detection system is not aware of. + * + * @param {MetamaskController} theController + */ +function maybeDetectPhishing(theController) { + async function redirectTab(tabId, url) { + try { + return await browser.tabs.update(tabId, { + url, + }); + } catch (error) { + return sentry?.captureException(error); + } + } + // we can use the blocking API in MV2, but not in MV3 + const isManifestV2 = !isManifestV3; + browser.webRequest.onBeforeRequest.addListener( + (details) => { + if (details.tabId === browser.tabs.TAB_ID_NONE) { + return {}; + } + + const onboardState = theController.onboardingController.store.getState(); + if (!onboardState.completedOnboarding) { + return {}; + } + + const prefState = theController.preferencesController.store.getState(); + if (!prefState.usePhishDetect) { + return {}; + } + + // ignore requests that come from our phishing warning page, as + // the requests may come from the "continue to site" link, so we'll + // actually _want_ to bypass the phishing detection. We shouldn't have to + // do this, because the phishing site does tell the extension that the + // domain it blocked it now "safe", but it does this _after_ the request + // begins (which would get blocked by this listener). So we have to bail + // on detection here. + // This check can be removed once https://github.com/MetaMask/phishing-warning/issues/160 + // is shipped. + if ( + details.initiator && + // compare normalized URLs + new URL(details.initiator).host === phishingPageUrl.host + ) { + return {}; + } + + const { hostname, href, searchParams } = new URL(details.url); + if (inTest) { + if (searchParams.has('IN_TEST_BYPASS_EARLY_PHISHING_DETECTION')) { + // this is a test page that needs to bypass early phishing detection + return {}; + } + } + + theController.phishingController.maybeUpdateState(); + const phishingTestResponse = theController.phishingController.test( + details.url, + ); + + const blockedRequestResponse = + theController.phishingController.isBlockedRequest(details.url); + + // if the request is not blocked, and the phishing test is not blocked, return and don't show the phishing screen + if (!phishingTestResponse?.result && !blockedRequestResponse.result) { + return {}; + } + + // Determine the block reason based on the type + let blockReason; + if (phishingTestResponse?.result && blockedRequestResponse.result) { + blockReason = `${phishingTestResponse.type} and ${blockedRequestResponse.type}`; + } else if (phishingTestResponse?.result) { + blockReason = phishingTestResponse.type; + } else { + blockReason = blockedRequestResponse.type; + } + + theController.metaMetricsController.trackEvent({ + // should we differentiate between background redirection and content script redirection? + event: MetaMetricsEventName.PhishingPageDisplayed, + category: MetaMetricsEventCategory.Phishing, + properties: { + url: hostname, + reason: blockReason, + }, + }); + const querystring = new URLSearchParams({ hostname, href }); + const redirectUrl = new URL(phishingPageHref); + redirectUrl.hash = querystring.toString(); + const redirectHref = redirectUrl.toString(); + + // blocking is better than tab redirection, as blocking will prevent + // the browser from loading the page at all + if (isManifestV2) { + if (details.type === 'sub_frame') { + // redirect the entire tab to the + // phishing warning page instead. + redirectTab(details.tabId, redirectHref); + // don't let the sub_frame load at all + return { cancel: true }; + } + // redirect the whole tab + return { redirectUrl: redirectHref }; + } + // redirect the whole tab (even if it's a sub_frame request) + redirectTab(details.tabId, redirectHref); + return {}; + }, + { + types: ['main_frame', 'sub_frame', 'xmlhttprequest'], + urls: ['http://*/*', 'https://*/*'], + }, + isManifestV2 ? ['blocking'] : [], + ); +} + // These are set after initialization let connectRemote; let connectExternalExtension; @@ -236,24 +375,19 @@ function saveTimestamp() { * @property {object} featureFlags - An object for optional feature flags. * @property {boolean} welcomeScreen - True if welcome screen should be shown. * @property {string} currentLocale - A locale string matching the user's preferred display language. - * @property {object} providerConfig - The current selected network provider. - * @property {string} providerConfig.rpcUrl - The address for the RPC API, if using an RPC API. - * @property {string} providerConfig.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks. * @property {string} networkStatus - Either "unknown", "available", "unavailable", or "blocked", depending on the status of the currently selected network. * @property {object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values. * @property {object} accountsByChainId - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values keyed by chain id. * @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string. * @property {object} currentBlockGasLimitByChainId - The most recently seen block gas limit, in a lower case hex prefixed string keyed by chain id. - * @property {object} unapprovedMsgs - An object of messages pending approval, mapping a unique ID to the options. - * @property {number} unapprovedMsgCount - The number of messages in unapprovedMsgs. * @property {object} unapprovedPersonalMsgs - An object of messages pending approval, mapping a unique ID to the options. * @property {number} unapprovedPersonalMsgCount - The number of messages in unapprovedPersonalMsgs. * @property {object} unapprovedEncryptionPublicKeyMsgs - An object of messages pending approval, mapping a unique ID to the options. * @property {number} unapprovedEncryptionPublicKeyMsgCount - The number of messages in EncryptionPublicKeyMsgs. * @property {object} unapprovedDecryptMsgs - An object of messages pending approval, mapping a unique ID to the options. * @property {number} unapprovedDecryptMsgCount - The number of messages in unapprovedDecryptMsgs. - * @property {object} unapprovedTypedMsgs - An object of messages pending approval, mapping a unique ID to the options. - * @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs. + * @property {object} unapprovedTypedMessages - An object of messages pending approval, mapping a unique ID to the options. + * @property {number} unapprovedTypedMessagesCount - The number of messages in unapprovedTypedMessages. * @property {number} pendingApprovalCount - The number of pending request in the approval controller. * @property {Keyring[]} keyrings - An array of keyring descriptions, summarizing the accounts that are available for use, and what keyrings they belong to. * @property {string} selectedAddress - A lower case hex string of the currently selected address. @@ -284,6 +418,14 @@ async function initialize() { let isFirstMetaMaskControllerSetup; + // We only want to start this if we are running a test build, not for the release build. + // `navigator.webdriver` is true if Selenium, Puppeteer, or Playwright are running. + // In MV3, the Service Worker sees `navigator.webdriver` as `undefined`, so this will trigger from + // an Offscreen Document message instead. Because it's a singleton class, it's safe to start multiple times. + if (process.env.IN_TEST && window.navigator?.webdriver) { + getSocketBackgroundToMocha(); + } + if (isManifestV3) { // Save the timestamp immediately and then every `SAVE_TIMESTAMP_INTERVAL` // miliseconds. This keeps the service worker alive. @@ -303,14 +445,27 @@ async function initialize() { await browser.storage.session.set({ isFirstMetaMaskControllerSetup }); } + const overrides = inTest + ? { + keyrings: { + trezorBridge: FakeTrezorBridge, + ledgerBridge: FakeLedgerBridge, + }, + } + : {}; + setupController( initState, initLangCode, - {}, + overrides, isFirstMetaMaskControllerSetup, initData.meta, offscreenPromise, ); + + // `setupController` sets up the `controller` object, so we can use it now: + maybeDetectPhishing(controller); + if (!isManifestV3) { await loadPhishingWarningPage(); } @@ -339,9 +494,7 @@ class PhishingWarningPageTimeoutError extends Error { async function loadPhishingWarningPage() { let iframe; try { - const extensionStartupPhishingPageUrl = new URL( - process.env.PHISHING_WARNING_PAGE_URL, - ); + const extensionStartupPhishingPageUrl = new URL(phishingPageHref); // The `extensionStartup` hash signals to the phishing warning page that it should not bother // setting up streams for user interaction. Otherwise this page load would cause a console // error. @@ -595,7 +748,7 @@ export function setupController( setupEnsIpfsResolver({ getCurrentChainId: () => - controller.networkController.state.providerConfig.chainId, + getCurrentChainId({ metamask: controller.networkController.state }), getIpfsGateway: controller.preferencesController.getIpfsGateway.bind( controller.preferencesController, ), @@ -903,26 +1056,30 @@ export function setupController( function getUnreadNotificationsCount() { try { - const { isMetamaskNotificationsEnabled, isFeatureAnnouncementsEnabled } = - controller.metamaskNotificationsController.state; + const { isNotificationServicesEnabled, isFeatureAnnouncementsEnabled } = + controller.notificationServicesController.state; const snapNotificationCount = Object.values( controller.notificationController.state.notifications, ).filter((notification) => notification.readDate === null).length; const featureAnnouncementCount = isFeatureAnnouncementsEnabled - ? controller.metamaskNotificationsController.state.metamaskNotificationsList.filter( + ? controller.notificationServicesController.state.metamaskNotificationsList.filter( (notification) => !notification.isRead && - notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, + notification.type === + NotificationServicesController.Constants.TRIGGER_TYPES + .FEATURES_ANNOUNCEMENT, ).length : 0; - const walletNotificationCount = isMetamaskNotificationsEnabled - ? controller.metamaskNotificationsController.state.metamaskNotificationsList.filter( + const walletNotificationCount = isNotificationServicesEnabled + ? controller.notificationServicesController.state.metamaskNotificationsList.filter( (notification) => !notification.isRead && - notification.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, + notification.type !== + NotificationServicesController.Constants.TRIGGER_TYPES + .FEATURES_ANNOUNCEMENT, ).length : 0; @@ -968,6 +1125,7 @@ export function setupController( switch (type) { case ApprovalType.SnapDialogAlert: case ApprovalType.SnapDialogPrompt: + case DIALOG_APPROVAL_TYPES.default: controller.approvalController.accept(id, null); break; case ApprovalType.SnapDialogConfirmation: @@ -1102,9 +1260,24 @@ function setupSentryGetStateGlobal(store) { async function initBackground() { await onInstall(); - initialize().catch(log.error); + try { + await initialize(); + if (process.env.IN_TEST) { + // Send message to offscreen document + if (browser.offscreen) { + browser.runtime.sendMessage({ + target: OffscreenCommunicationTarget.extension, + event: OffscreenCommunicationEvents.metamaskBackgroundReady, + }); + } else { + window.document?.documentElement?.classList.add('controller-loaded'); + } + } + localStore.cleanUpMostRecentRetrievedState(); + } catch (error) { + log.error(error); + } } - if (!process.env.SKIP_BACKGROUND_INITIALIZATION) { initBackground(); } diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 134f1dbe2313..df0238210d96 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -55,7 +55,6 @@ export const SENTRY_BACKGROUND_STATE = { currentMigrationVersion: true, previousAppVersion: true, previousMigrationVersion: true, - showTokenAutodetectModalOnUpgrade: false, }, ApprovalController: { approvalFlows: false, @@ -96,6 +95,15 @@ export const SENTRY_BACKGROUND_STATE = { MultichainBalancesController: { balances: false, }, + BridgeController: { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + destNetworkAllowlist: [], + srcNetworkAllowlist: [], + }, + }, + }, CronjobController: { jobs: false, }, @@ -129,10 +137,10 @@ export const SENTRY_BACKGROUND_STATE = { LoggingController: { logs: false, }, - MetamaskNotificationsController: { + NotificationServicesController: { subscriptionAccountsSeen: false, isMetamaskNotificationsFeatureSeen: false, - isMetamaskNotificationsEnabled: false, + isNotificationServicesEnabled: false, isFeatureAnnouncementsEnabled: false, metamaskNotificationsList: false, metamaskNotificationsReadList: false, @@ -159,15 +167,6 @@ export const SENTRY_BACKGROUND_STATE = { NetworkController: { networkConfigurations: false, networksMetadata: true, - providerConfig: { - chainId: true, - id: true, - nickname: true, - rpcPrefs: false, - rpcUrl: false, - ticker: true, - type: true, - }, selectedNetworkClientId: false, }, NftController: { @@ -200,7 +199,6 @@ export const SENTRY_BACKGROUND_STATE = { PreferencesController: { advancedGasFee: true, currentLocale: true, - disabledRpcMethodPreferences: true, dismissSeedBackUpReminder: true, featureFlags: true, forgottenPassword: true, @@ -215,13 +213,16 @@ export const SENTRY_BACKGROUND_STATE = { preferences: { autoLockTimeLimit: true, hideZeroBalanceTokens: true, + redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: false, + isRedesignedConfirmationsDeveloperEnabled: false, showExtensionInFullSizeView: true, showFiatInTestnets: true, showTestNetworks: true, smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, - showTokenAutodetectModal: false, + showConfirmationAdvancedDetails: true, }, useExternalServices: false, selectedAddress: false, @@ -240,9 +241,8 @@ export const SENTRY_BACKGROUND_STATE = { useRequestQueue: true, useTransactionSimulations: true, enableMV3TimestampSave: true, - hasDismissedOpenSeaToBlockaidBanner: true, }, - PushPlatformNotificationsController: { + NotificationServicesPushController: { fcmToken: false, }, MultichainRatesController: { @@ -255,8 +255,6 @@ export const SENTRY_BACKGROUND_STATE = { }, SelectedNetworkController: { domains: false }, SignatureController: { - unapprovedMsgCount: true, - unapprovedMsgs: false, unapprovedPersonalMsgCount: true, unapprovedPersonalMsgs: false, unapprovedTypedMessages: false, @@ -279,9 +277,12 @@ export const SENTRY_BACKGROUND_STATE = { snapStates: false, snaps: false, }, - SnapInterface: { + SnapInterfaceController: { interfaces: false, }, + SnapInsightsController: { + insights: false, + }, SnapsRegistry: { database: false, lastUpdated: false, @@ -395,6 +396,9 @@ export const SENTRY_UI_STATE = { welcomeScreenSeen: true, confirmationExchangeRates: true, useSafeChainsListValidation: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) addSnapAccountEnabled: false, snapsAddSnapAccountModalDismissed: false, diff --git a/app/scripts/controllers/app-metadata.test.ts b/app/scripts/controllers/app-metadata.test.ts index c5041aad623a..cd9f87e238a0 100644 --- a/app/scripts/controllers/app-metadata.test.ts +++ b/app/scripts/controllers/app-metadata.test.ts @@ -5,7 +5,6 @@ const EXPECTED_DEFAULT_STATE = { previousAppVersion: '', previousMigrationVersion: 0, currentMigrationVersion: 0, - showTokenAutodetectModalOnUpgrade: false, }; describe('AppMetadataController', () => { @@ -16,7 +15,6 @@ describe('AppMetadataController', () => { previousAppVersion: '1', previousMigrationVersion: 1, currentMigrationVersion: 1, - showTokenAutodetectModalOnUpgrade: false, }; const appMetadataController = new AppMetadataController({ state: initState, @@ -55,7 +53,6 @@ describe('AppMetadataController', () => { expect(appMetadataController.store.getState()).toStrictEqual({ ...EXPECTED_DEFAULT_STATE, currentAppVersion: '1', - showTokenAutodetectModalOnUpgrade: null, }); }); @@ -72,7 +69,6 @@ describe('AppMetadataController', () => { ...EXPECTED_DEFAULT_STATE, currentAppVersion: '3', previousAppVersion: '2', - showTokenAutodetectModalOnUpgrade: null, }); }); diff --git a/app/scripts/controllers/app-metadata.ts b/app/scripts/controllers/app-metadata.ts index 79d346c8412d..0d745730d0c0 100644 --- a/app/scripts/controllers/app-metadata.ts +++ b/app/scripts/controllers/app-metadata.ts @@ -9,7 +9,6 @@ export type AppMetadataControllerState = { previousAppVersion: string; previousMigrationVersion: number; currentMigrationVersion: number; - showTokenAutodetectModalOnUpgrade: boolean | null; }; /** @@ -26,7 +25,6 @@ const defaultState: AppMetadataControllerState = { previousAppVersion: '', previousMigrationVersion: 0, currentMigrationVersion: 0, - showTokenAutodetectModalOnUpgrade: false, }; /** @@ -78,7 +76,6 @@ export default class AppMetadataController extends EventEmitter { this.store.updateState({ currentAppVersion: maybeNewAppVersion, previousAppVersion: oldCurrentAppVersion, - showTokenAutodetectModalOnUpgrade: null, }); } } @@ -99,13 +96,4 @@ export default class AppMetadataController extends EventEmitter { }); } } - - /** - * Setter for the `showTokenAutodetectModalOnUpgrade` property - * - * @param val - Indicates the value of showTokenAutodetectModalOnUpgrade - */ - setShowTokenAutodetectModalOnUpgrade(val: boolean): void { - this.store.updateState({ showTokenAutodetectModalOnUpgrade: val }); - } } diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 1a511523778f..08b11facf9ca 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -15,6 +15,8 @@ import { } from '../../../shared/constants/app'; import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; +/** @typedef {import('../../../shared/types/confirm').LastInteractedConfirmationInfo} LastInteractedConfirmationInfo */ + export default class AppStateController extends EventEmitter { /** * @param {object} opts @@ -73,6 +75,7 @@ export default class AppStateController extends EventEmitter { switchedNetworkDetails: null, switchedNetworkNeverShowMessage: false, currentExtensionPopupId: 0, + lastInteractedConfirmationInfo: undefined, }); this.timer = null; @@ -558,6 +561,26 @@ export default class AppStateController extends EventEmitter { }); } + /** + * The function returns information about the last confirmation user interacted with + * + * @type {LastInteractedConfirmationInfo}: Information about the last confirmation user interacted with. + */ + getLastInteractedConfirmationInfo() { + return this.store.getState().lastInteractedConfirmationInfo; + } + + /** + * Update the information about the last confirmation user interacted with + * + * @type {LastInteractedConfirmationInfo} - information about transaction user last interacted with. + */ + setLastInteractedConfirmationInfo(lastInteractedConfirmationInfo) { + this.store.updateState({ + lastInteractedConfirmationInfo, + }); + } + /** * A getter to retrieve currentPopupId saved in the appState */ diff --git a/app/scripts/controllers/app-state.test.js b/app/scripts/controllers/app-state.test.js index 82c42d46d314..c4a24a258f02 100644 --- a/app/scripts/controllers/app-state.test.js +++ b/app/scripts/controllers/app-state.test.js @@ -296,6 +296,27 @@ describe('AppStateController', () => { }); }); + describe('setLastInteractedConfirmationInfo', () => { + it('sets information about last confirmation user has interacted with', () => { + const lastInteractedConfirmationInfo = { + id: '123', + chainId: '0x1', + timestamp: new Date().getTime(), + }; + appStateController.setLastInteractedConfirmationInfo( + lastInteractedConfirmationInfo, + ); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + lastInteractedConfirmationInfo, + ); + + appStateController.setLastInteractedConfirmationInfo(undefined); + expect(appStateController.getLastInteractedConfirmationInfo()).toBe( + undefined, + ); + }); + }); + describe('setSnapsInstallPrivacyWarningShownStatus', () => { it('updates the status of snaps install privacy warning', () => { appStateController = createAppStateController(); diff --git a/app/scripts/controllers/authentication/auth-snap-requests.ts b/app/scripts/controllers/authentication/auth-snap-requests.ts deleted file mode 100644 index 81c8beaafd40..000000000000 --- a/app/scripts/controllers/authentication/auth-snap-requests.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { SnapId } from '@metamask/snaps-sdk'; -import type { HandleSnapRequest } from '@metamask/snaps-controllers'; -import { HandlerType } from '@metamask/snaps-utils'; - -type SnapRPCRequest = Parameters[0]; - -const snapId = 'npm:@metamask/message-signing-snap' as SnapId; - -export function createSnapPublicKeyRequest(): SnapRPCRequest { - return { - snapId, - origin: '', - handler: HandlerType.OnRpcRequest, - request: { - method: 'getPublicKey', - }, - }; -} - -export function createSnapSignMessageRequest( - message: `metamask:${string}`, -): SnapRPCRequest { - return { - snapId, - origin: '', - handler: HandlerType.OnRpcRequest, - request: { - method: 'signMessage', - params: { message }, - }, - }; -} diff --git a/app/scripts/controllers/authentication/authentication-controller.test.ts b/app/scripts/controllers/authentication/authentication-controller.test.ts deleted file mode 100644 index 1cd01dfb74fc..000000000000 --- a/app/scripts/controllers/authentication/authentication-controller.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { ControllerMessenger } from '@metamask/base-controller'; -import AuthenticationController, { - AllowedActions, - AllowedEvents, - AuthenticationControllerState, -} from './authentication-controller'; -import { - mockEndpointAccessToken, - mockEndpointGetNonce, - mockEndpointLogin, -} from './mocks/mockServices'; -import { MOCK_ACCESS_TOKEN, MOCK_LOGIN_RESPONSE } from './mocks/mockResponses'; - -const mockSignedInState = (): AuthenticationControllerState => ({ - isSignedIn: true, - sessionData: { - accessToken: 'MOCK_ACCESS_TOKEN', - expiresIn: new Date().toString(), - profile: { - identifierId: MOCK_LOGIN_RESPONSE.profile.identifier_id, - profileId: MOCK_LOGIN_RESPONSE.profile.profile_id, - }, - }, -}); - -describe('authentication/authentication-controller - constructor() tests', () => { - test('should initialize with default state', () => { - const metametrics = createMockAuthMetaMetrics(); - const controller = new AuthenticationController({ - messenger: createMockAuthenticationMessenger().messenger, - metametrics, - }); - - expect(controller.state.isSignedIn).toBe(false); - expect(controller.state.sessionData).toBeUndefined(); - }); - - test('should initialize with override state', () => { - const metametrics = createMockAuthMetaMetrics(); - const controller = new AuthenticationController({ - messenger: createMockAuthenticationMessenger().messenger, - state: mockSignedInState(), - metametrics, - }); - - expect(controller.state.isSignedIn).toBe(true); - expect(controller.state.sessionData).toBeDefined(); - }); -}); - -describe('authentication/authentication-controller - performSignIn() tests', () => { - test('Should create access token and update state', async () => { - const metametrics = createMockAuthMetaMetrics(); - const mockEndpoints = mockAuthenticationFlowEndpoints(); - const { messenger, mockSnapGetPublicKey, mockSnapSignMessage } = - createMockAuthenticationMessenger(); - - const controller = new AuthenticationController({ messenger, metametrics }); - - const result = await controller.performSignIn(); - expect(mockSnapGetPublicKey).toBeCalled(); - expect(mockSnapSignMessage).toBeCalled(); - mockEndpoints.mockGetNonceEndpoint.done(); - mockEndpoints.mockLoginEndpoint.done(); - mockEndpoints.mockAccessTokenEndpoint.done(); - expect(result).toBe(MOCK_ACCESS_TOKEN); - - // Assert - state shows user is logged in - expect(controller.state.isSignedIn).toBe(true); - expect(controller.state.sessionData).toBeDefined(); - }); - - test('Should error when nonce endpoint fails', async () => { - await testAndAssertFailingEndpoints('nonce'); - }); - - test('Should error when login endpoint fails', async () => { - await testAndAssertFailingEndpoints('login'); - }); - - test('Should error when tokens endpoint fails', async () => { - await testAndAssertFailingEndpoints('token'); - }); - - // When the wallet is locked, we are unable to call the snap - test('Should error when wallet is locked', async () => { - const { messenger, mockKeyringControllerGetState } = - createMockAuthenticationMessenger(); - const metametrics = createMockAuthMetaMetrics(); - - // Mock wallet is locked - mockKeyringControllerGetState.mockReturnValue({ isUnlocked: false }); - - const controller = new AuthenticationController({ messenger, metametrics }); - - await expect(controller.performSignIn()).rejects.toThrow(); - }); - - async function testAndAssertFailingEndpoints( - endpointFail: 'nonce' | 'login' | 'token', - ) { - const mockEndpoints = mockAuthenticationFlowEndpoints({ - endpointFail, - }); - const { messenger } = createMockAuthenticationMessenger(); - const metametrics = createMockAuthMetaMetrics(); - const controller = new AuthenticationController({ messenger, metametrics }); - - await expect(controller.performSignIn()).rejects.toThrow(); - expect(controller.state.isSignedIn).toBe(false); - - const endpointsCalled = [ - mockEndpoints.mockGetNonceEndpoint.isDone(), - mockEndpoints.mockLoginEndpoint.isDone(), - mockEndpoints.mockAccessTokenEndpoint.isDone(), - ]; - if (endpointFail === 'nonce') { - expect(endpointsCalled).toEqual([true, false, false]); - } - - if (endpointFail === 'login') { - expect(endpointsCalled).toEqual([true, true, false]); - } - - if (endpointFail === 'token') { - expect(endpointsCalled).toEqual([true, true, true]); - } - } -}); - -describe('authentication/authentication-controller - performSignOut() tests', () => { - test('Should remove signed in user and any access tokens', () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const controller = new AuthenticationController({ - messenger, - state: mockSignedInState(), - metametrics, - }); - - controller.performSignOut(); - expect(controller.state.isSignedIn).toBe(false); - expect(controller.state.sessionData).toBeUndefined(); - }); - - test('Should throw error if attempting to sign out when user is not logged in', () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const controller = new AuthenticationController({ - messenger, - state: { isSignedIn: false }, - metametrics, - }); - - expect(() => controller.performSignOut()).toThrow(); - }); -}); - -describe('authentication/authentication-controller - getBearerToken() tests', () => { - test('Should throw error if not logged in', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const controller = new AuthenticationController({ - messenger, - state: { isSignedIn: false }, - metametrics, - }); - - await expect(controller.getBearerToken()).rejects.toThrow(); - }); - - test('Should return original access token in state', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const originalState = mockSignedInState(); - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - const result = await controller.getBearerToken(); - expect(result).toBeDefined(); - expect(result).toBe(originalState.sessionData?.accessToken); - }); - - test('Should return new access token if state is invalid', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - mockAuthenticationFlowEndpoints(); - - // Invalid/old state - const originalState = mockSignedInState(); - if (originalState.sessionData) { - originalState.sessionData.accessToken = 'ACCESS_TOKEN_1'; - - const d = new Date(); - d.setMinutes(d.getMinutes() - 31); // expires at 30 mins - originalState.sessionData.expiresIn = d.toString(); - } - - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - const result = await controller.getBearerToken(); - expect(result).toBeDefined(); - expect(result).toBe(MOCK_ACCESS_TOKEN); - }); - - // If the state is invalid, we need to re-login. - // But as wallet is locked, we will not be able to call the snap - test('Should throw error if wallet is locked', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger, mockKeyringControllerGetState } = - createMockAuthenticationMessenger(); - mockAuthenticationFlowEndpoints(); - - // Invalid/old state - const originalState = mockSignedInState(); - if (originalState.sessionData) { - originalState.sessionData.accessToken = 'ACCESS_TOKEN_1'; - - const d = new Date(); - d.setMinutes(d.getMinutes() - 31); // expires at 30 mins - originalState.sessionData.expiresIn = d.toString(); - } - - // Mock wallet is locked - mockKeyringControllerGetState.mockReturnValue({ isUnlocked: false }); - - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - await expect(controller.getBearerToken()).rejects.toThrow(); - }); -}); - -describe('authentication/authentication-controller - getSessionProfile() tests', () => { - test('Should throw error if not logged in', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const controller = new AuthenticationController({ - messenger, - state: { isSignedIn: false }, - metametrics, - }); - - await expect(controller.getSessionProfile()).rejects.toThrow(); - }); - - test('Should return original access token in state', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - const originalState = mockSignedInState(); - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - const result = await controller.getSessionProfile(); - expect(result).toBeDefined(); - expect(result).toEqual(originalState.sessionData?.profile); - }); - - test('Should return new access token if state is invalid', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger } = createMockAuthenticationMessenger(); - mockAuthenticationFlowEndpoints(); - - // Invalid/old state - const originalState = mockSignedInState(); - if (originalState.sessionData) { - originalState.sessionData.profile.identifierId = 'ID_1'; - - const d = new Date(); - d.setMinutes(d.getMinutes() - 31); // expires at 30 mins - originalState.sessionData.expiresIn = d.toString(); - } - - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - const result = await controller.getSessionProfile(); - expect(result).toBeDefined(); - expect(result.identifierId).toBe(MOCK_LOGIN_RESPONSE.profile.identifier_id); - expect(result.profileId).toBe(MOCK_LOGIN_RESPONSE.profile.profile_id); - }); - - // If the state is invalid, we need to re-login. - // But as wallet is locked, we will not be able to call the snap - test('Should throw error if wallet is locked', async () => { - const metametrics = createMockAuthMetaMetrics(); - const { messenger, mockKeyringControllerGetState } = - createMockAuthenticationMessenger(); - mockAuthenticationFlowEndpoints(); - - // Invalid/old state - const originalState = mockSignedInState(); - if (originalState.sessionData) { - originalState.sessionData.profile.identifierId = 'ID_1'; - - const d = new Date(); - d.setMinutes(d.getMinutes() - 31); // expires at 30 mins - originalState.sessionData.expiresIn = d.toString(); - } - - // Mock wallet is locked - mockKeyringControllerGetState.mockReturnValue({ isUnlocked: false }); - - const controller = new AuthenticationController({ - messenger, - state: originalState, - metametrics, - }); - - await expect(controller.getSessionProfile()).rejects.toThrow(); - }); -}); - -function createAuthenticationMessenger() { - const messenger = new ControllerMessenger(); - return messenger.getRestricted({ - name: 'AuthenticationController', - allowedActions: [ - `SnapController:handleRequest`, - 'KeyringController:getState', - ], - allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], - }); -} - -function createMockAuthenticationMessenger() { - const messenger = createAuthenticationMessenger(); - const mockCall = jest.spyOn(messenger, 'call'); - const mockSnapGetPublicKey = jest.fn().mockResolvedValue('MOCK_PUBLIC_KEY'); - const mockSnapSignMessage = jest - .fn() - .mockResolvedValue('MOCK_SIGNED_MESSAGE'); - - const mockKeyringControllerGetState = jest - .fn() - .mockReturnValue({ isUnlocked: true }); - - mockCall.mockImplementation((...args) => { - const [actionType, params] = args; - if (actionType === 'SnapController:handleRequest') { - if (params?.request.method === 'getPublicKey') { - return mockSnapGetPublicKey(); - } - - if (params?.request.method === 'signMessage') { - return mockSnapSignMessage(); - } - - throw new Error( - `MOCK_FAIL - unsupported SnapController:handleRequest call: ${params?.request.method}`, - ); - } - - if (actionType === 'KeyringController:getState') { - return mockKeyringControllerGetState(); - } - - throw new Error(`MOCK_FAIL - unsupported messenger call: ${actionType}`); - }); - - return { - messenger, - mockSnapGetPublicKey, - mockSnapSignMessage, - mockKeyringControllerGetState, - }; -} - -function mockAuthenticationFlowEndpoints(params?: { - endpointFail: 'nonce' | 'login' | 'token'; -}) { - const mockGetNonceEndpoint = mockEndpointGetNonce( - params?.endpointFail === 'nonce' ? { status: 500 } : undefined, - ); - const mockLoginEndpoint = mockEndpointLogin( - params?.endpointFail === 'login' ? { status: 500 } : undefined, - ); - const mockAccessTokenEndpoint = mockEndpointAccessToken( - params?.endpointFail === 'token' ? { status: 500 } : undefined, - ); - - return { - mockGetNonceEndpoint, - mockLoginEndpoint, - mockAccessTokenEndpoint, - }; -} - -function createMockAuthMetaMetrics() { - const getMetaMetricsId = jest.fn().mockReturnValue('MOCK_METAMETRICS_ID'); - - return { getMetaMetricsId }; -} diff --git a/app/scripts/controllers/authentication/authentication-controller.ts b/app/scripts/controllers/authentication/authentication-controller.ts deleted file mode 100644 index 4bcb1528c323..000000000000 --- a/app/scripts/controllers/authentication/authentication-controller.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { - BaseController, - RestrictedControllerMessenger, - StateMetadata, -} from '@metamask/base-controller'; -import type { - KeyringControllerGetStateAction, - KeyringControllerLockEvent, - KeyringControllerUnlockEvent, -} from '@metamask/keyring-controller'; -import { HandleSnapRequest } from '@metamask/snaps-controllers'; -import { - createSnapPublicKeyRequest, - createSnapSignMessageRequest, -} from './auth-snap-requests'; -import { - createLoginRawMessage, - getAccessToken, - getNonce, - login, -} from './services'; - -const THIRTY_MIN_MS = 1000 * 60 * 30; - -const controllerName = 'AuthenticationController'; - -// State -type SessionProfile = { - identifierId: string; - profileId: string; -}; - -type SessionData = { - /** profile - anonymous profile data for the given logged in user */ - profile: SessionProfile; - /** accessToken - used to make requests authorized endpoints */ - accessToken: string; - /** expiresIn - string date to determine if new access token is required */ - expiresIn: string; -}; - -type MetaMetricsAuth = { - getMetaMetricsId: () => string; -}; - -export type AuthenticationControllerState = { - /** - * Global isSignedIn state. - * Can be used to determine if "Profile Syncing" is enabled. - */ - isSignedIn: boolean; - sessionData?: SessionData; -}; -const defaultState: AuthenticationControllerState = { isSignedIn: false }; -const metadata: StateMetadata = { - isSignedIn: { - persist: true, - anonymous: true, - }, - sessionData: { - persist: true, - anonymous: false, - }, -}; - -// Messenger Actions -type CreateActionsObj = { - [K in T]: { - type: `${typeof controllerName}:${K}`; - handler: AuthenticationController[K]; - }; -}; -type ActionsObj = CreateActionsObj< - | 'performSignIn' - | 'performSignOut' - | 'getBearerToken' - | 'getSessionProfile' - | 'isSignedIn' ->; -export type Actions = ActionsObj[keyof ActionsObj]; -export type AuthenticationControllerPerformSignIn = ActionsObj['performSignIn']; -export type AuthenticationControllerPerformSignOut = - ActionsObj['performSignOut']; -export type AuthenticationControllerGetBearerToken = - ActionsObj['getBearerToken']; -export type AuthenticationControllerGetSessionProfile = - ActionsObj['getSessionProfile']; -export type AuthenticationControllerIsSignedIn = ActionsObj['isSignedIn']; - -// Allowed Actions -export type AllowedActions = - | HandleSnapRequest - | KeyringControllerGetStateAction; - -export type AllowedEvents = - | KeyringControllerLockEvent - | KeyringControllerUnlockEvent; - -// Messenger -export type AuthenticationControllerMessenger = RestrictedControllerMessenger< - typeof controllerName, - Actions | AllowedActions, - AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] ->; - -/** - * Controller that enables authentication for restricted endpoints. - * Used for Global Profile Syncing and Notifications - */ -export default class AuthenticationController extends BaseController< - typeof controllerName, - AuthenticationControllerState, - AuthenticationControllerMessenger -> { - #metametrics: MetaMetricsAuth; - - #isUnlocked = false; - - #keyringController = { - setupLockedStateSubscriptions: () => { - const { isUnlocked } = this.messagingSystem.call( - 'KeyringController:getState', - ); - this.#isUnlocked = isUnlocked; - - this.messagingSystem.subscribe('KeyringController:unlock', () => { - this.#isUnlocked = true; - }); - - this.messagingSystem.subscribe('KeyringController:lock', () => { - this.#isUnlocked = false; - }); - }, - }; - - constructor({ - messenger, - state, - metametrics, - }: { - messenger: AuthenticationControllerMessenger; - state?: AuthenticationControllerState; - /** - * Not using the Messaging System as we - * do not want to tie this strictly to extension - */ - metametrics: MetaMetricsAuth; - }) { - super({ - messenger, - metadata, - name: controllerName, - state: { ...defaultState, ...state }, - }); - - this.#metametrics = metametrics; - - this.#keyringController.setupLockedStateSubscriptions(); - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering this controller's messaging system - * actions. - */ - #registerMessageHandlers(): void { - this.messagingSystem.registerActionHandler( - 'AuthenticationController:getBearerToken', - this.getBearerToken.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'AuthenticationController:getSessionProfile', - this.getSessionProfile.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'AuthenticationController:isSignedIn', - this.isSignedIn.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'AuthenticationController:performSignIn', - this.performSignIn.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'AuthenticationController:performSignOut', - this.performSignOut.bind(this), - ); - } - - public async performSignIn(): Promise { - const { accessToken } = await this.#performAuthenticationFlow(); - return accessToken; - } - - public performSignOut(): void { - this.#assertLoggedIn(); - - this.update((state) => { - state.isSignedIn = false; - state.sessionData = undefined; - }); - } - - public async getBearerToken(): Promise { - this.#assertLoggedIn(); - - if (this.#hasValidSession(this.state.sessionData)) { - return this.state.sessionData.accessToken; - } - - const { accessToken } = await this.#performAuthenticationFlow(); - return accessToken; - } - - /** - * Will return a session profile. - * Throws if a user is not logged in. - * - * @returns profile for the session. - */ - public async getSessionProfile(): Promise { - this.#assertLoggedIn(); - - if (this.#hasValidSession(this.state.sessionData)) { - return this.state.sessionData.profile; - } - - const { profile } = await this.#performAuthenticationFlow(); - return profile; - } - - public isSignedIn(): boolean { - return this.state.isSignedIn; - } - - #assertLoggedIn(): void { - if (!this.state.isSignedIn) { - throw new Error( - `${controllerName}: Unable to call method, user is not authenticated`, - ); - } - } - - async #performAuthenticationFlow(): Promise<{ - profile: SessionProfile; - accessToken: string; - }> { - try { - // 1. Nonce - const publicKey = await this.#snapGetPublicKey(); - const nonce = await getNonce(publicKey); - if (!nonce) { - throw new Error(`Unable to get nonce`); - } - - // 2. Login - const rawMessage = createLoginRawMessage(nonce, publicKey); - const signature = await this.#snapSignMessage(rawMessage); - const loginResponse = await login( - rawMessage, - signature, - this.#metametrics.getMetaMetricsId(), - ); - if (!loginResponse?.token) { - throw new Error(`Unable to login`); - } - - const profile: SessionProfile = { - identifierId: loginResponse.profile.identifier_id, - profileId: loginResponse.profile.profile_id, - }; - - // 3. Trade for Access Token - const accessToken = await getAccessToken(loginResponse.token); - if (!accessToken) { - throw new Error(`Unable to get Access Token`); - } - - // Update Internal State - this.update((state) => { - state.isSignedIn = true; - const expiresIn = new Date(); - expiresIn.setTime(expiresIn.getTime() + THIRTY_MIN_MS); - state.sessionData = { - profile, - accessToken, - expiresIn: expiresIn.toString(), - }; - }); - - return { - profile, - accessToken, - }; - } catch (e) { - console.error('Failed to authenticate', e); - const errorMessage = - e instanceof Error ? e.message : JSON.stringify(e ?? ''); - throw new Error( - `${controllerName}: Failed to authenticate - ${errorMessage}`, - ); - } - } - - #hasValidSession( - sessionData: SessionData | undefined, - ): sessionData is SessionData { - if (!sessionData) { - return false; - } - - const prevDate = Date.parse(sessionData.expiresIn); - if (isNaN(prevDate)) { - return false; - } - - const currentDate = new Date(); - const diffMs = Math.abs(currentDate.getTime() - prevDate); - - return THIRTY_MIN_MS > diffMs; - } - - #_snapPublicKeyCache: string | undefined; - - /** - * Returns the auth snap public key. - * - * @returns The snap public key. - */ - async #snapGetPublicKey(): Promise { - if (this.#_snapPublicKeyCache) { - return this.#_snapPublicKeyCache; - } - - if (!this.#isUnlocked) { - throw new Error( - '#snapGetPublicKey - unable to call snap, wallet is locked', - ); - } - - const result = (await this.messagingSystem.call( - 'SnapController:handleRequest', - createSnapPublicKeyRequest(), - )) as string; - - this.#_snapPublicKeyCache = result; - - return result; - } - - #_snapSignMessageCache: Record<`metamask:${string}`, string> = {}; - - /** - * Signs a specific message using an underlying auth snap. - * - * @param message - A specific tagged message to sign. - * @returns A Signature created by the snap. - */ - async #snapSignMessage(message: `metamask:${string}`): Promise { - if (this.#_snapSignMessageCache[message]) { - return this.#_snapSignMessageCache[message]; - } - - if (!this.#isUnlocked) { - throw new Error( - '#snapSignMessage - unable to call snap, wallet is locked', - ); - } - - const result = (await this.messagingSystem.call( - 'SnapController:handleRequest', - createSnapSignMessageRequest(message), - )) as string; - - this.#_snapSignMessageCache[message] = result; - - return result; - } -} diff --git a/app/scripts/controllers/authentication/mocks/mockResponses.ts b/app/scripts/controllers/authentication/mocks/mockResponses.ts deleted file mode 100644 index 4882bd81d812..000000000000 --- a/app/scripts/controllers/authentication/mocks/mockResponses.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - AUTH_LOGIN_ENDPOINT, - AUTH_NONCE_ENDPOINT, - LoginResponse, - NonceResponse, - OAuthTokenResponse, - OIDC_TOKENS_ENDPOINT, -} from '../services'; - -type MockResponse = { - url: string; - requestMethod: 'GET' | 'POST' | 'PUT'; - response: unknown; -}; - -export const MOCK_NONCE = '4cbfqzoQpcNxVImGv'; -export const MOCK_NONCE_RESPONSE: NonceResponse = { - nonce: MOCK_NONCE, -}; - -export function getMockAuthNonceResponse() { - return { - url: AUTH_NONCE_ENDPOINT, - requestMethod: 'GET', - response: MOCK_NONCE_RESPONSE, - } satisfies MockResponse; -} - -export const MOCK_JWT = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; -export const MOCK_LOGIN_RESPONSE: LoginResponse = { - token: MOCK_JWT, - expires_in: new Date().toString(), - profile: { - identifier_id: 'MOCK_IDENTIFIER', - profile_id: 'MOCK_PROFILE_ID', - }, -}; - -export function getMockAuthLoginResponse() { - return { - url: AUTH_LOGIN_ENDPOINT, - requestMethod: 'POST', - response: MOCK_LOGIN_RESPONSE, - } satisfies MockResponse; -} - -export const MOCK_ACCESS_TOKEN = `MOCK_ACCESS_TOKEN-${MOCK_JWT}`; -export const MOCK_OATH_TOKEN_RESPONSE: OAuthTokenResponse = { - access_token: MOCK_ACCESS_TOKEN, - expires_in: new Date().getTime(), -}; - -export function getMockAuthAccessTokenResponse() { - return { - url: OIDC_TOKENS_ENDPOINT, - requestMethod: 'POST', - response: MOCK_OATH_TOKEN_RESPONSE, - } satisfies MockResponse; -} diff --git a/app/scripts/controllers/authentication/mocks/mockServices.ts b/app/scripts/controllers/authentication/mocks/mockServices.ts deleted file mode 100644 index 69d3cf56c5d5..000000000000 --- a/app/scripts/controllers/authentication/mocks/mockServices.ts +++ /dev/null @@ -1,42 +0,0 @@ -import nock from 'nock'; -import { - getMockAuthAccessTokenResponse, - getMockAuthLoginResponse, - getMockAuthNonceResponse, -} from './mockResponses'; - -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockEndpointGetNonce(mockReply?: MockReply) { - const mockResponse = getMockAuthNonceResponse(); - const reply = mockReply ?? { status: 200, body: mockResponse.response }; - const mockNonceEndpoint = nock(mockResponse.url) - .get('') - .query(true) - .reply(reply.status, reply.body); - - return mockNonceEndpoint; -} - -export function mockEndpointLogin(mockReply?: MockReply) { - const mockResponse = getMockAuthLoginResponse(); - const reply = mockReply ?? { status: 200, body: mockResponse.response }; - const mockLoginEndpoint = nock(mockResponse.url) - .post('') - .reply(reply.status, reply.body); - - return mockLoginEndpoint; -} - -export function mockEndpointAccessToken(mockReply?: MockReply) { - const mockResponse = getMockAuthAccessTokenResponse(); - const reply = mockReply ?? { status: 200, body: mockResponse.response }; - const mockOidcTokensEndpoint = nock(mockResponse.url) - .post('') - .reply(reply.status, reply.body); - - return mockOidcTokensEndpoint; -} diff --git a/app/scripts/controllers/authentication/services.test.ts b/app/scripts/controllers/authentication/services.test.ts deleted file mode 100644 index 759b3c535936..000000000000 --- a/app/scripts/controllers/authentication/services.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { MOCK_ACCESS_TOKEN, MOCK_JWT, MOCK_NONCE } from './mocks/mockResponses'; -import { - mockEndpointAccessToken, - mockEndpointGetNonce, - mockEndpointLogin, -} from './mocks/mockServices'; -import { - createLoginRawMessage, - getAccessToken, - getNonce, - login, -} from './services'; - -const MOCK_METAMETRICS_ID = '0x123'; - -describe('authentication/services.ts - getNonce() tests', () => { - test('returns nonce on valid request', async () => { - const mockNonceEndpoint = mockEndpointGetNonce(); - const response = await getNonce('MOCK_PUBLIC_KEY'); - - mockNonceEndpoint.done(); - expect(response).toBe(MOCK_NONCE); - }); - - test('returns null if request is invalid', async () => { - async function testInvalidResponse( - status: number, - body: Record, - ) { - const mockNonceEndpoint = mockEndpointGetNonce({ status, body }); - const response = await getNonce('MOCK_PUBLIC_KEY'); - - mockNonceEndpoint.done(); - expect(response).toBe(null); - } - - await testInvalidResponse(500, { error: 'mock server error' }); - await testInvalidResponse(400, { error: 'mock bad request' }); - }); -}); - -describe('authentication/services.ts - login() tests', () => { - test('returns single-use jwt if successful login', async () => { - const mockLoginEndpoint = mockEndpointLogin(); - const response = await login( - 'mock raw message', - 'mock signature', - MOCK_METAMETRICS_ID, - ); - - mockLoginEndpoint.done(); - expect(response?.token).toBe(MOCK_JWT); - expect(response?.profile).toBeDefined(); - }); - - test('returns null if request is invalid', async () => { - async function testInvalidResponse( - status: number, - body: Record, - ) { - const mockLoginEndpoint = mockEndpointLogin({ status, body }); - const response = await login( - 'mock raw message', - 'mock signature', - MOCK_METAMETRICS_ID, - ); - - mockLoginEndpoint.done(); - expect(response).toBe(null); - } - - await testInvalidResponse(500, { error: 'mock server error' }); - await testInvalidResponse(400, { error: 'mock bad request' }); - }); -}); - -describe('authentication/services.ts - getAccessToken() tests', () => { - test('returns access token jwt if successful OIDC token request', async () => { - const mockLoginEndpoint = mockEndpointAccessToken(); - const response = await getAccessToken('mock single-use jwt'); - - mockLoginEndpoint.done(); - expect(response).toBe(MOCK_ACCESS_TOKEN); - }); - - test('returns null if request is invalid', async () => { - async function testInvalidResponse( - status: number, - body: Record, - ) { - const mockLoginEndpoint = mockEndpointAccessToken({ status, body }); - const response = await getAccessToken('mock single-use jwt'); - - mockLoginEndpoint.done(); - expect(response).toBe(null); - } - - await testInvalidResponse(500, { error: 'mock server error' }); - await testInvalidResponse(400, { error: 'mock bad request' }); - }); -}); - -describe('authentication/services.ts - createLoginRawMessage() tests', () => { - test('creates the raw message format for login request', () => { - const message = createLoginRawMessage('NONCE', 'PUBLIC_KEY'); - expect(message).toBe('metamask:NONCE:PUBLIC_KEY'); - }); -}); diff --git a/app/scripts/controllers/authentication/services.ts b/app/scripts/controllers/authentication/services.ts deleted file mode 100644 index a4bbbf2f11cb..000000000000 --- a/app/scripts/controllers/authentication/services.ts +++ /dev/null @@ -1,120 +0,0 @@ -const AUTH_ENDPOINT = process.env.AUTH_API || ''; -export const AUTH_NONCE_ENDPOINT = `${AUTH_ENDPOINT}/api/v2/nonce`; -export const AUTH_LOGIN_ENDPOINT = `${AUTH_ENDPOINT}/api/v2/srp/login`; - -const OIDC_ENDPOINT = process.env.OIDC_API || ''; -export const OIDC_TOKENS_ENDPOINT = `${OIDC_ENDPOINT}/oauth2/token`; -const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ''; -const OIDC_GRANT_TYPE = process.env.OIDC_GRANT_TYPE || ''; - -export type NonceResponse = { - nonce: string; -}; -export async function getNonce(publicKey: string): Promise { - const nonceUrl = new URL(AUTH_NONCE_ENDPOINT); - nonceUrl.searchParams.set('identifier', publicKey); - - try { - const nonceResponse = await fetch(nonceUrl.toString()); - if (!nonceResponse.ok) { - return null; - } - - const nonceJson: NonceResponse = await nonceResponse.json(); - return nonceJson?.nonce ?? null; - } catch (e) { - console.error('authentication-controller/services: unable to get nonce', e); - return null; - } -} - -export type LoginResponse = { - token: string; - expires_in: string; - /** - * Contains anonymous information about the logged in profile. - * - * @property identifier_id - a deterministic unique identifier on the method used to sign in - * @property profile_id - a unique id for a given profile - * @property metametrics_id - an anonymous server id - */ - profile: { - identifier_id: string; - profile_id: string; - }; -}; -export async function login( - rawMessage: string, - signature: string, - clientMetaMetricsId: string, -): Promise { - try { - const response = await fetch(AUTH_LOGIN_ENDPOINT, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - signature, - raw_message: rawMessage, - metametrics: { - metametrics_id: clientMetaMetricsId, - agent: 'extension', - }, - }), - }); - - if (!response.ok) { - return null; - } - - const loginResponse: LoginResponse = await response.json(); - return loginResponse ?? null; - } catch (e) { - console.error('authentication-controller/services: unable to login', e); - return null; - } -} - -export type OAuthTokenResponse = { - access_token: string; - expires_in: number; -}; -export async function getAccessToken(jwtToken: string): Promise { - const headers = new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded', - }); - - const urlEncodedBody = new URLSearchParams(); - urlEncodedBody.append('grant_type', OIDC_GRANT_TYPE); - urlEncodedBody.append('client_id', OIDC_CLIENT_ID); - urlEncodedBody.append('assertion', jwtToken); - - try { - const response = await fetch(OIDC_TOKENS_ENDPOINT, { - method: 'POST', - headers, - body: urlEncodedBody.toString(), - }); - - if (!response.ok) { - return null; - } - - const accessTokenResponse: OAuthTokenResponse = await response.json(); - return accessTokenResponse?.access_token ?? null; - } catch (e) { - console.error( - 'authentication-controller/services: unable to get access token', - e, - ); - return null; - } -} - -export function createLoginRawMessage( - nonce: string, - publicKey: string, -): `metamask:${string}:${string}` { - return `metamask:${nonce}:${publicKey}` as const; -} diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts new file mode 100644 index 000000000000..9c9036b87f7b --- /dev/null +++ b/app/scripts/controllers/bridge/bridge-controller.test.ts @@ -0,0 +1,54 @@ +import nock from 'nock'; +import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import BridgeController from './bridge-controller'; +import { BridgeControllerMessenger } from './types'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants'; + +const EMPTY_INIT_STATE = { + bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE, +}; + +const messengerMock = { + call: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + publish: jest.fn(), +} as unknown as jest.Mocked; + +describe('BridgeController', function () { + let bridgeController: BridgeController; + + beforeAll(function () { + bridgeController = new BridgeController({ messenger: messengerMock }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + nock(BRIDGE_API_BASE_URL) + .get('/getAllFeatureFlags') + .reply(200, { + 'extension-support': true, + 'src-network-allowlist': [10, 534352], + 'dest-network-allowlist': [137, 42161], + }); + }); + + it('constructor should setup correctly', function () { + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + }); + + it('setBridgeFeatureFlags should fetch and set the bridge feature flags', async function () { + const expectedFeatureFlagsResponse = { + extensionSupport: true, + destNetworkAllowlist: [CHAIN_IDS.POLYGON, CHAIN_IDS.ARBITRUM], + srcNetworkAllowlist: [CHAIN_IDS.OPTIMISM, CHAIN_IDS.SCROLL], + }; + expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); + + await bridgeController.setBridgeFeatureFlags(); + expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( + expectedFeatureFlagsResponse, + ); + }); +}); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts new file mode 100644 index 000000000000..f899879605cf --- /dev/null +++ b/app/scripts/controllers/bridge/bridge-controller.ts @@ -0,0 +1,50 @@ +import { BaseController, StateMetadata } from '@metamask/base-controller'; +import { fetchBridgeFeatureFlags } from '../../../../ui/pages/bridge/bridge.util'; +import { + BRIDGE_CONTROLLER_NAME, + DEFAULT_BRIDGE_CONTROLLER_STATE, +} from './constants'; +import { BridgeControllerState, BridgeControllerMessenger } from './types'; + +const metadata: StateMetadata<{ bridgeState: BridgeControllerState }> = { + bridgeState: { + persist: false, + anonymous: false, + }, +}; + +export default class BridgeController extends BaseController< + typeof BRIDGE_CONTROLLER_NAME, + { bridgeState: BridgeControllerState }, + BridgeControllerMessenger +> { + constructor({ messenger }: { messenger: BridgeControllerMessenger }) { + super({ + name: BRIDGE_CONTROLLER_NAME, + metadata, + messenger, + state: { bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE }, + }); + + this.messagingSystem.registerActionHandler( + `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, + this.setBridgeFeatureFlags.bind(this), + ); + } + + resetState = () => { + this.update((_state) => { + _state.bridgeState = { + ...DEFAULT_BRIDGE_CONTROLLER_STATE, + }; + }); + }; + + setBridgeFeatureFlags = async () => { + const { bridgeState } = this.state; + const bridgeFeatureFlags = await fetchBridgeFeatureFlags(); + this.update((_state) => { + _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; + }); + }; +} diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts new file mode 100644 index 000000000000..f2932120f98d --- /dev/null +++ b/app/scripts/controllers/bridge/constants.ts @@ -0,0 +1,11 @@ +import { BridgeControllerState, BridgeFeatureFlagsKey } from './types'; + +export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; + +export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: false, + [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: [], + [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: [], + }, +}; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts new file mode 100644 index 000000000000..aa92a6597c69 --- /dev/null +++ b/app/scripts/controllers/bridge/types.ts @@ -0,0 +1,52 @@ +import { + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { Hex } from '@metamask/utils'; +import BridgeController from './bridge-controller'; +import { BRIDGE_CONTROLLER_NAME } from './constants'; + +export enum BridgeFeatureFlagsKey { + EXTENSION_SUPPORT = 'extensionSupport', + NETWORK_SRC_ALLOWLIST = 'srcNetworkAllowlist', + NETWORK_DEST_ALLOWLIST = 'destNetworkAllowlist', +} + +export type BridgeFeatureFlags = { + [BridgeFeatureFlagsKey.EXTENSION_SUPPORT]: boolean; + [BridgeFeatureFlagsKey.NETWORK_SRC_ALLOWLIST]: Hex[]; + [BridgeFeatureFlagsKey.NETWORK_DEST_ALLOWLIST]: Hex[]; +}; + +export type BridgeControllerState = { + bridgeFeatureFlags: BridgeFeatureFlags; +}; + +export enum BridgeBackgroundAction { + SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', +} + +type BridgeControllerAction = { + type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; + handler: BridgeController[FunctionName]; +}; + +// Maps to BridgeController function names +type BridgeControllerActions = + BridgeControllerAction; + +type BridgeControllerEvents = ControllerStateChangeEvent< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerState +>; + +/** + * The messenger for the BridgeController. + */ +export type BridgeControllerMessenger = RestrictedControllerMessenger< + typeof BRIDGE_CONTROLLER_NAME, + BridgeControllerActions, + BridgeControllerEvents, + never, + never +>; diff --git a/app/scripts/controllers/metamask-notifications/constants/constants.ts b/app/scripts/controllers/metamask-notifications/constants/constants.ts deleted file mode 100644 index 516b63b96fe3..000000000000 --- a/app/scripts/controllers/metamask-notifications/constants/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const USER_STORAGE_VERSION = '1'; - -// Force cast. We don't really care about the type here since we treat it as a unique symbol -export const USER_STORAGE_VERSION_KEY: unique symbol = 'v' as never; diff --git a/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts b/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts deleted file mode 100644 index eac0d4891980..000000000000 --- a/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts +++ /dev/null @@ -1,172 +0,0 @@ -export enum TRIGGER_TYPES { - FEATURES_ANNOUNCEMENT = 'features_announcement', - METAMASK_SWAP_COMPLETED = 'metamask_swap_completed', - ERC20_SENT = 'erc20_sent', - ERC20_RECEIVED = 'erc20_received', - ETH_SENT = 'eth_sent', - ETH_RECEIVED = 'eth_received', - ROCKETPOOL_STAKE_COMPLETED = 'rocketpool_stake_completed', - ROCKETPOOL_UNSTAKE_COMPLETED = 'rocketpool_unstake_completed', - LIDO_STAKE_COMPLETED = 'lido_stake_completed', - LIDO_WITHDRAWAL_REQUESTED = 'lido_withdrawal_requested', - LIDO_WITHDRAWAL_COMPLETED = 'lido_withdrawal_completed', - LIDO_STAKE_READY_TO_BE_WITHDRAWN = 'lido_stake_ready_to_be_withdrawn', - ERC721_SENT = 'erc721_sent', - ERC721_RECEIVED = 'erc721_received', - ERC1155_SENT = 'erc1155_sent', - ERC1155_RECEIVED = 'erc1155_received', -} - -export const TRIGGER_TYPES_WALLET_SET: Set = new Set([ - TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - TRIGGER_TYPES.ERC20_SENT, - TRIGGER_TYPES.ERC20_RECEIVED, - TRIGGER_TYPES.ETH_SENT, - TRIGGER_TYPES.ETH_RECEIVED, - TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, - TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, - TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, - TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, - TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, - TRIGGER_TYPES.ERC721_SENT, - TRIGGER_TYPES.ERC721_RECEIVED, - TRIGGER_TYPES.ERC1155_SENT, - TRIGGER_TYPES.ERC1155_RECEIVED, -]) satisfies Set>; - -export enum TRIGGER_TYPES_GROUPS { - RECEIVED = 'received', - SENT = 'sent', - DEFI = 'defi', -} - -export const NOTIFICATION_CHAINS = { - ETHEREUM: '1', - OPTIMISM: '10', - BSC: '56', - POLYGON: '137', - ARBITRUM: '42161', - AVALANCHE: '43114', - LINEA: '59144', -}; - -export const CHAIN_SYMBOLS = { - [NOTIFICATION_CHAINS.ETHEREUM]: 'ETH', - [NOTIFICATION_CHAINS.OPTIMISM]: 'ETH', - [NOTIFICATION_CHAINS.BSC]: 'BNB', - [NOTIFICATION_CHAINS.POLYGON]: 'MATIC', - [NOTIFICATION_CHAINS.ARBITRUM]: 'ETH', - [NOTIFICATION_CHAINS.AVALANCHE]: 'AVAX', - [NOTIFICATION_CHAINS.LINEA]: 'ETH', -}; - -export const SUPPORTED_CHAINS = [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - NOTIFICATION_CHAINS.LINEA, -]; - -export type Trigger = { - supported_chains: (typeof SUPPORTED_CHAINS)[number][]; -}; - -export const TRIGGERS: Partial> = { - [TRIGGER_TYPES.METAMASK_SWAP_COMPLETED]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - ], - }, - [TRIGGER_TYPES.ERC20_SENT]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - NOTIFICATION_CHAINS.LINEA, - ], - }, - [TRIGGER_TYPES.ERC20_RECEIVED]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - NOTIFICATION_CHAINS.LINEA, - ], - }, - [TRIGGER_TYPES.ERC721_SENT]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.POLYGON, - ], - }, - [TRIGGER_TYPES.ERC721_RECEIVED]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.POLYGON, - ], - }, - [TRIGGER_TYPES.ERC1155_SENT]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.POLYGON, - ], - }, - [TRIGGER_TYPES.ERC1155_RECEIVED]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.POLYGON, - ], - }, - [TRIGGER_TYPES.ETH_SENT]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - NOTIFICATION_CHAINS.LINEA, - ], - }, - [TRIGGER_TYPES.ETH_RECEIVED]: { - supported_chains: [ - NOTIFICATION_CHAINS.ETHEREUM, - NOTIFICATION_CHAINS.OPTIMISM, - NOTIFICATION_CHAINS.BSC, - NOTIFICATION_CHAINS.POLYGON, - NOTIFICATION_CHAINS.ARBITRUM, - NOTIFICATION_CHAINS.AVALANCHE, - NOTIFICATION_CHAINS.LINEA, - ], - }, - [TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED]: { - supported_chains: [NOTIFICATION_CHAINS.ETHEREUM], - }, - [TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED]: { - supported_chains: [NOTIFICATION_CHAINS.ETHEREUM], - }, - [TRIGGER_TYPES.LIDO_STAKE_COMPLETED]: { - supported_chains: [NOTIFICATION_CHAINS.ETHEREUM], - }, - [TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED]: { - supported_chains: [NOTIFICATION_CHAINS.ETHEREUM], - }, - [TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED]: { - supported_chains: [NOTIFICATION_CHAINS.ETHEREUM], - }, -}; diff --git a/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts b/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts deleted file mode 100644 index c96ccfe2e989..000000000000 --- a/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts +++ /dev/null @@ -1,815 +0,0 @@ -import { ControllerMessenger } from '@metamask/base-controller'; -import * as ControllerUtils from '@metamask/controller-utils'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerState, - KeyringControllerStateChangeEvent, -} from '@metamask/keyring-controller'; -import { waitFor } from '@testing-library/react'; -import { - AuthenticationControllerGetBearerToken, - AuthenticationControllerIsSignedIn, -} from '../authentication/authentication-controller'; -import { MOCK_ACCESS_TOKEN } from '../authentication/mocks/mockResponses'; -import { - UserStorageControllerGetStorageKey, - UserStorageControllerPerformGetStorage, - UserStorageControllerPerformSetStorage, - UserStorageControllerEnableProfileSyncing, -} from '../user-storage/user-storage-controller'; -import { - PushPlatformNotificationsControllerEnablePushNotifications, - PushPlatformNotificationsControllerDisablePushNotifications, - PushPlatformNotificationsControllerUpdateTriggerPushNotifications, -} from '../push-platform-notifications/push-platform-notifications'; -import { - AllowedActions, - AllowedEvents, - MetamaskNotificationsController, - defaultState, -} from './metamask-notifications'; -import { - createMockFeatureAnnouncementAPIResult, - createMockFeatureAnnouncementRaw, -} from './mocks/mock-feature-announcements'; -import { - MOCK_USER_STORAGE_ACCOUNT, - createMockFullUserStorage, - createMockUserStorageWithTriggers, -} from './mocks/mock-notification-user-storage'; -import { - mockFetchFeatureAnnouncementNotifications, - mockBatchCreateTriggers, - mockBatchDeleteTriggers, - mockListNotifications, - mockMarkNotificationsAsRead, -} from './mocks/mockServices'; -import { createMockNotificationEthSent } from './mocks/mock-raw-notifications'; -import { processNotification } from './processors/process-notifications'; -import * as OnChainNotifications from './services/onchain-notifications'; -import { UserStorage } from './types/user-storage/user-storage'; -import * as MetamaskNotificationsUtils from './utils/utils'; - -// Mock type used for testing purposes -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type MockVar = any; - -describe('metamask-notifications - constructor()', () => { - test('initializes state & override state', () => { - const controller1 = new MetamaskNotificationsController({ - messenger: mockNotificationMessenger().messenger, - }); - expect(controller1.state).toEqual(defaultState); - - const controller2 = new MetamaskNotificationsController({ - messenger: mockNotificationMessenger().messenger, - state: { - ...defaultState, - isFeatureAnnouncementsEnabled: true, - isMetamaskNotificationsEnabled: true, - }, - }); - expect(controller2.state.isFeatureAnnouncementsEnabled).toBe(true); - expect(controller2.state.isMetamaskNotificationsEnabled).toBe(true); - }); - - test('Keyring Change Event but feature not enabled will not add or remove triggers', async () => { - const { messenger, globalMessenger, mockListAccounts } = arrangeMocks(); - - // initialize controller with 1 address - mockListAccounts.mockResolvedValueOnce(['addr1']); - const controller = new MetamaskNotificationsController({ messenger }); - - const mockUpdate = jest - .spyOn(controller, 'updateOnChainTriggersByAccount') - .mockResolvedValue({} as UserStorage); - const mockDelete = jest - .spyOn(controller, 'deleteOnChainTriggersByAccount') - .mockResolvedValue({} as UserStorage); - - // listAccounts has a new address - mockListAccounts.mockResolvedValueOnce(['addr1', 'addr2']); - await actPublishKeyringStateChange(globalMessenger); - - expect(mockUpdate).not.toBeCalled(); - expect(mockDelete).not.toBeCalled(); - }); - - test('Keyring Change Event with new triggers will update triggers correctly', async () => { - const { messenger, globalMessenger, mockListAccounts } = arrangeMocks(); - - // initialize controller with 1 address - const controller = new MetamaskNotificationsController({ - messenger, - state: { - isMetamaskNotificationsEnabled: true, - subscriptionAccountsSeen: ['addr1'], - }, - }); - - const mockUpdate = jest - .spyOn(controller, 'updateOnChainTriggersByAccount') - .mockResolvedValue({} as UserStorage); - const mockDelete = jest - .spyOn(controller, 'deleteOnChainTriggersByAccount') - .mockResolvedValue({} as UserStorage); - - async function act( - addresses: string[], - assertion: () => Promise | void, - ) { - mockListAccounts.mockResolvedValueOnce(addresses); - await actPublishKeyringStateChange(globalMessenger); - await assertion(); - - // Clear mocks for next act/assert - mockUpdate.mockClear(); - mockDelete.mockClear(); - } - - // Act - if list accounts has been seen, then will not update - await act(['addr1'], () => { - expect(mockUpdate).not.toBeCalled(); - expect(mockDelete).not.toBeCalled(); - }); - - // Act - if a new address in list, then will update - await act(['addr1', 'addr2'], () => { - expect(mockUpdate).toBeCalled(); - expect(mockDelete).not.toBeCalled(); - }); - - // Act - if the list doesn't have an address, then we need to delete - await act(['addr2'], () => { - expect(mockUpdate).not.toBeCalled(); - expect(mockDelete).toBeCalled(); - }); - - // If the address is added back to the list, because it is seen we won't update - await act(['addr1', 'addr2'], () => { - expect(mockUpdate).not.toBeCalled(); - expect(mockDelete).not.toBeCalled(); - }); - }); - - test('Initializes push notifications', async () => { - const { messenger, mockEnablePushNotifications } = arrangeMocks(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _controller = new MetamaskNotificationsController({ - messenger, - state: { isMetamaskNotificationsEnabled: true }, - }); - - await waitFor(() => { - expect(mockEnablePushNotifications).toBeCalled(); - }); - }); - - test('Fails to initialize push notifications', async () => { - const { messenger, mockPerformGetStorage, mockEnablePushNotifications } = - arrangeMocks(); - - // test when user storage is empty - mockPerformGetStorage.mockResolvedValue(null); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _controller = new MetamaskNotificationsController({ - messenger, - state: { isMetamaskNotificationsEnabled: true }, - }); - - await waitFor(() => { - expect(mockPerformGetStorage).toBeCalled(); - }); - - expect(mockEnablePushNotifications).not.toBeCalled(); - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - jest - .spyOn(ControllerUtils, 'toChecksumHexAddress') - .mockImplementation((x) => x); - - return messengerMocks; - } - - async function actPublishKeyringStateChange( - messenger: ControllerMessenger, - ) { - await messenger.publish( - 'KeyringController:stateChange', - {} as KeyringControllerState, - [], - ); - } -}); - -// See /utils for more in-depth testing -describe('metamask-notifications - checkAccountsPresence()', () => { - test('Returns Record with accounts that have notifications enabled', async () => { - const { messenger, mockPerformGetStorage } = mockNotificationMessenger(); - mockPerformGetStorage.mockResolvedValue( - JSON.stringify(createMockFullUserStorage()), - ); - - const controller = new MetamaskNotificationsController({ messenger }); - const result = await controller.checkAccountsPresence([ - MOCK_USER_STORAGE_ACCOUNT, - 'fake_account', - ]); - expect(result).toEqual({ - [MOCK_USER_STORAGE_ACCOUNT]: true, - fake_account: false, - }); - }); -}); - -describe('metamask-notifications - setFeatureAnnouncementsEnabled()', () => { - test('flips state when the method is called', async () => { - const { messenger, mockIsSignedIn } = mockNotificationMessenger(); - mockIsSignedIn.mockReturnValue(true); - - const controller = new MetamaskNotificationsController({ - messenger, - state: { ...defaultState, isFeatureAnnouncementsEnabled: false }, - }); - - await controller.setFeatureAnnouncementsEnabled(true); - - expect(controller.state.isFeatureAnnouncementsEnabled).toBe(true); - }); -}); - -describe('metamask-notifications - createOnChainTriggers()', () => { - test('Create new triggers and push notifications if there is no User Storage (login for new user)', async () => { - const { - messenger, - mockInitializeUserStorage, - mockEnablePushNotifications, - mockCreateOnChainTriggers, - mockPerformGetStorage, - } = arrangeMocks(); - const controller = new MetamaskNotificationsController({ messenger }); - mockPerformGetStorage.mockResolvedValue(null); // Mock no storage found. - - const result = await controller.createOnChainTriggers(); - expect(result).toBeDefined(); - expect(mockInitializeUserStorage).toBeCalled(); // called since no user storage (this is an existing user) - expect(mockCreateOnChainTriggers).toBeCalled(); - expect(mockEnablePushNotifications).toBeCalled(); - }); - - test('Throws if not given a valid auth & storage key', async () => { - const mocks = arrangeMocks(); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - }); - - const testScenarios = { - ...arrangeFailureAuthAssertions(mocks), - ...arrangeFailureUserStorageKeyAssertions(mocks), - }; - - for (const mockFailureAction of Object.values(testScenarios)) { - mockFailureAction(); - await expect(controller.createOnChainTriggers()).rejects.toThrow(); - } - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - const mockCreateOnChainTriggers = jest - .spyOn(OnChainNotifications, 'createOnChainTriggers') - .mockResolvedValue(); - const mockInitializeUserStorage = jest - .spyOn(MetamaskNotificationsUtils, 'initializeUserStorage') - .mockReturnValue(createMockUserStorageWithTriggers(['t1', 't2'])); - return { - ...messengerMocks, - mockCreateOnChainTriggers, - mockInitializeUserStorage, - }; - } -}); - -describe('metamask-notifications - deleteOnChainTriggersByAccount', () => { - test('Deletes and disables push notifications for a given account', async () => { - const { - messenger, - nockMockDeleteTriggersAPI, - mockDisablePushNotifications, - } = arrangeMocks(); - const controller = new MetamaskNotificationsController({ messenger }); - const result = await controller.deleteOnChainTriggersByAccount([ - MOCK_USER_STORAGE_ACCOUNT, - ]); - expect( - MetamaskNotificationsUtils.traverseUserStorageTriggers(result).length, - ).toBe(0); - expect(nockMockDeleteTriggersAPI.isDone()).toBe(true); - expect(mockDisablePushNotifications).toBeCalled(); - }); - - test('Does nothing if account does not exist in storage', async () => { - const { messenger, mockDisablePushNotifications } = arrangeMocks(); - const controller = new MetamaskNotificationsController({ messenger }); - const result = await controller.deleteOnChainTriggersByAccount([ - 'UNKNOWN_ACCOUNT', - ]); - expect( - MetamaskNotificationsUtils.traverseUserStorageTriggers(result).length, - ).not.toBe(0); - - expect(mockDisablePushNotifications).not.toBeCalled(); - }); - - test('Throws errors when invalid auth and storage', async () => { - const mocks = arrangeMocks(); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - }); - - const testScenarios = { - ...arrangeFailureAuthAssertions(mocks), - ...arrangeFailureUserStorageKeyAssertions(mocks), - ...arrangeFailureUserStorageAssertions(mocks), - }; - - for (const mockFailureAction of Object.values(testScenarios)) { - mockFailureAction(); - await expect( - controller.deleteOnChainTriggersByAccount([MOCK_USER_STORAGE_ACCOUNT]), - ).rejects.toThrow(); - } - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - const nockMockDeleteTriggersAPI = mockBatchDeleteTriggers(); - return { ...messengerMocks, nockMockDeleteTriggersAPI }; - } -}); - -describe('metamask-notifications - updateOnChainTriggersByAccount()', () => { - test('Creates Triggers and Push Notification Links for a new account', async () => { - const { - messenger, - mockUpdateTriggerPushNotifications, - mockPerformSetStorage, - } = arrangeMocks(); - const MOCK_ACCOUNT = 'MOCK_ACCOUNT2'; - const controller = new MetamaskNotificationsController({ messenger }); - - const result = await controller.updateOnChainTriggersByAccount([ - MOCK_ACCOUNT, - ]); - expect( - MetamaskNotificationsUtils.traverseUserStorageTriggers(result, { - address: MOCK_ACCOUNT.toLowerCase(), - }).length > 0, - ).toBe(true); - - expect(mockUpdateTriggerPushNotifications).toBeCalled(); - expect(mockPerformSetStorage).toBeCalled(); - }); - - test('Throws errors when invalid auth and storage', async () => { - const mocks = arrangeMocks(); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - }); - - const testScenarios = { - ...arrangeFailureAuthAssertions(mocks), - ...arrangeFailureUserStorageKeyAssertions(mocks), - ...arrangeFailureUserStorageAssertions(mocks), - }; - - for (const mockFailureAction of Object.values(testScenarios)) { - mockFailureAction(); - await expect( - controller.deleteOnChainTriggersByAccount([MOCK_USER_STORAGE_ACCOUNT]), - ).rejects.toThrow(); - } - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - const mockBatchTriggersAPI = mockBatchCreateTriggers(); - return { ...messengerMocks, mockBatchTriggersAPI }; - } -}); - -describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () => { - test('Processes and shows feature announcements and wallet notifications', async () => { - const { - messenger, - mockFeatureAnnouncementAPIResult, - mockListNotificationsAPIResult, - } = arrangeMocks(); - - const controller = new MetamaskNotificationsController({ - messenger, - state: { ...defaultState, isFeatureAnnouncementsEnabled: true }, - }); - - const result = await controller.fetchAndUpdateMetamaskNotifications(); - - // Should have 1 feature announcement and 1 wallet notification - expect(result.length).toBe(2); - expect( - result.find( - (n) => n.id === mockFeatureAnnouncementAPIResult.items?.[0].fields.id, - ), - ).toBeDefined(); - expect(result.find((n) => n.id === mockListNotificationsAPIResult[0].id)); - - // State is also updated - expect(controller.state.metamaskNotificationsList.length).toBe(2); - }); - - test('Only fetches and processes feature announcements if not authenticated', async () => { - const { messenger, mockGetBearerToken, mockFeatureAnnouncementAPIResult } = - arrangeMocks(); - mockGetBearerToken.mockRejectedValue( - new Error('MOCK - failed to get access token'), - ); - - const controller = new MetamaskNotificationsController({ - messenger, - state: { ...defaultState, isFeatureAnnouncementsEnabled: true }, - }); - - // Should only have feature announcement - const result = await controller.fetchAndUpdateMetamaskNotifications(); - expect(result.length).toBe(1); - expect( - result.find( - (n) => n.id === mockFeatureAnnouncementAPIResult.items?.[0].fields.id, - ), - ).toBeDefined(); - - // State is also updated - expect(controller.state.metamaskNotificationsList.length).toBe(1); - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - - const mockFeatureAnnouncementAPIResult = - createMockFeatureAnnouncementAPIResult(); - const mockFeatureAnnouncementsAPI = - mockFetchFeatureAnnouncementNotifications({ - status: 200, - body: mockFeatureAnnouncementAPIResult, - }); - - const mockListNotificationsAPIResult = [createMockNotificationEthSent()]; - const mockListNotificationsAPI = mockListNotifications({ - status: 200, - body: mockListNotificationsAPIResult, - }); - return { - ...messengerMocks, - mockFeatureAnnouncementAPIResult, - mockFeatureAnnouncementsAPI, - mockListNotificationsAPIResult, - mockListNotificationsAPI, - }; - } -}); - -describe('metamask-notifications - markMetamaskNotificationsAsRead()', () => { - test('updates feature announcements as read', async () => { - const { messenger } = arrangeMocks(); - const controller = new MetamaskNotificationsController({ messenger }); - - await controller.markMetamaskNotificationsAsRead([ - processNotification(createMockFeatureAnnouncementRaw()), - processNotification(createMockNotificationEthSent()), - ]); - - // Should see 2 items in controller read state - expect(controller.state.metamaskNotificationsReadList.length).toBe(1); - }); - - test('should at least mark feature announcements locally if external updates fail', async () => { - const { messenger } = arrangeMocks({ onChainMarkAsReadFails: true }); - const controller = new MetamaskNotificationsController({ messenger }); - - await controller.markMetamaskNotificationsAsRead([ - processNotification(createMockFeatureAnnouncementRaw()), - processNotification(createMockNotificationEthSent()), - ]); - - // Should see 1 item in controller read state. - // This is because on-chain failed. - // We can debate & change implementation if it makes sense to mark as read locally if external APIs fail. - expect(controller.state.metamaskNotificationsReadList.length).toBe(1); - }); - - function arrangeMocks(options?: { onChainMarkAsReadFails: boolean }) { - const messengerMocks = mockNotificationMessenger(); - - const mockMarkAsReadAPI = mockMarkNotificationsAsRead({ - status: options?.onChainMarkAsReadFails ? 500 : 200, - }); - - return { - ...messengerMocks, - mockMarkAsReadAPI, - }; - } -}); - -describe('metamask-notifications - enableMetamaskNotifications()', () => { - it('create new notifications when switched on and no new notifications', async () => { - const mocks = arrangeMocks(); - mocks.mockListAccounts.mockResolvedValue(['0xAddr1']); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - }); - - const promise = controller.enableMetamaskNotifications(); - - // Act - intermediate state - expect(controller.state.isUpdatingMetamaskNotifications).toBe(true); - - await promise; - - // Act - final state - expect(controller.state.isUpdatingMetamaskNotifications).toBe(false); - expect(controller.state.isMetamaskNotificationsEnabled).toBe(true); - - // Act - services called - expect(mocks.mockCreateOnChainTriggers).toBeCalled(); - }); - - it('not create new notifications when enabling an account already in storage', async () => { - const mocks = arrangeMocks(); - mocks.mockListAccounts.mockResolvedValue(['0xAddr1']); - const userStorage = createMockFullUserStorage({ address: '0xAddr1' }); - mocks.mockPerformGetStorage.mockResolvedValue(JSON.stringify(userStorage)); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - }); - - await controller.enableMetamaskNotifications(); - - const existingTriggers = - MetamaskNotificationsUtils.getAllUUIDs(userStorage); - const upsertedTriggers = - mocks.mockCreateOnChainTriggers.mock.calls[0][3].map((t) => t.id); - - expect(existingTriggers).toEqual(upsertedTriggers); - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - - const mockCreateOnChainTriggers = jest - .spyOn(OnChainNotifications, 'createOnChainTriggers') - .mockResolvedValue(); - - return { ...messengerMocks, mockCreateOnChainTriggers }; - } -}); - -describe('metamask-notifications - disableMetamaskNotifications()', () => { - it('disable notifications and turn off push notifications', async () => { - const mocks = arrangeMocks(); - const controller = new MetamaskNotificationsController({ - messenger: mocks.messenger, - state: { isMetamaskNotificationsEnabled: true }, - }); - - const promise = controller.disableMetamaskNotifications(); - - // Act - intermediate state - expect(controller.state.isUpdatingMetamaskNotifications).toBe(true); - - await promise; - - // Act - final state - expect(controller.state.isUpdatingMetamaskNotifications).toBe(false); - expect(controller.state.isMetamaskNotificationsEnabled).toBe(false); - - expect(mocks.mockDisablePushNotifications).toBeCalled(); - - // We do not delete triggers when disabling notifications - // As other devices might be using those triggers to receive notifications - expect(mocks.mockDeleteOnChainTriggers).not.toBeCalled(); - }); - - function arrangeMocks() { - const messengerMocks = mockNotificationMessenger(); - - const mockDeleteOnChainTriggers = jest - .spyOn(OnChainNotifications, 'deleteOnChainTriggers') - .mockResolvedValue({} as UserStorage); - - return { ...messengerMocks, mockDeleteOnChainTriggers }; - } -}); - -// Type-Computation - we are extracting args and parameters from a generic type utility -// Thus this `AnyFunc` can be used to help constrain the generic parameters correctly -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyFunc = (...args: any[]) => any; -const typedMockAction = () => - jest.fn, Parameters>(); - -function mockNotificationMessenger() { - const globalMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >(); - - const messenger = globalMessenger.getRestricted({ - name: 'MetamaskNotificationsController', - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getState', - 'AuthenticationController:getBearerToken', - 'AuthenticationController:isSignedIn', - 'PushPlatformNotificationsController:disablePushNotifications', - 'PushPlatformNotificationsController:enablePushNotifications', - 'PushPlatformNotificationsController:updateTriggerPushNotifications', - 'UserStorageController:getStorageKey', - 'UserStorageController:performGetStorage', - 'UserStorageController:performSetStorage', - 'UserStorageController:enableProfileSyncing', - ], - allowedEvents: [ - 'KeyringController:stateChange', - 'KeyringController:lock', - 'KeyringController:unlock', - 'PushPlatformNotificationsController:onNewNotifications', - ], - }); - - const mockListAccounts = - typedMockAction().mockResolvedValue([]); - - const mockGetBearerToken = - typedMockAction().mockResolvedValue( - MOCK_ACCESS_TOKEN, - ); - - const mockIsSignedIn = - typedMockAction().mockReturnValue(true); - - const mockDisablePushNotifications = - typedMockAction(); - - const mockEnablePushNotifications = - typedMockAction(); - - const mockUpdateTriggerPushNotifications = - typedMockAction(); - - const mockGetStorageKey = - typedMockAction().mockResolvedValue( - 'MOCK_STORAGE_KEY', - ); - - const mockEnableProfileSyncing = - typedMockAction(); - - const mockPerformGetStorage = - typedMockAction().mockResolvedValue( - JSON.stringify(createMockFullUserStorage()), - ); - - const mockPerformSetStorage = - typedMockAction(); - - jest.spyOn(messenger, 'call').mockImplementation((...args) => { - const [actionType] = args; - - // This mock implementation does not have a nice discriminate union where types/parameters can be correctly inferred - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [, ...params]: any[] = args; - - if (actionType === 'KeyringController:getAccounts') { - return mockListAccounts(); - } - - if (actionType === 'AuthenticationController:getBearerToken') { - return mockGetBearerToken(); - } - - if (actionType === 'AuthenticationController:isSignedIn') { - return mockIsSignedIn(); - } - - if ( - actionType === - 'PushPlatformNotificationsController:disablePushNotifications' - ) { - return mockDisablePushNotifications(params[0]); - } - - if ( - actionType === - 'PushPlatformNotificationsController:enablePushNotifications' - ) { - return mockEnablePushNotifications(params[0]); - } - - if ( - actionType === - 'PushPlatformNotificationsController:updateTriggerPushNotifications' - ) { - return mockUpdateTriggerPushNotifications(params[0]); - } - - if (actionType === 'UserStorageController:getStorageKey') { - return mockGetStorageKey(); - } - - if (actionType === 'UserStorageController:enableProfileSyncing') { - return mockEnableProfileSyncing(); - } - - if (actionType === 'UserStorageController:performGetStorage') { - return mockPerformGetStorage(params[0]); - } - - if (actionType === 'UserStorageController:performSetStorage') { - return mockPerformSetStorage(params[0], params[1]); - } - - if (actionType === 'KeyringController:getState') { - return { isUnlocked: true } as MockVar; - } - - function exhaustedMessengerMocks(action: never) { - return new Error(`MOCK_FAIL - unsupported messenger call: ${action}`); - } - throw exhaustedMessengerMocks(actionType); - }); - - return { - globalMessenger, - messenger, - mockListAccounts, - mockGetBearerToken, - mockIsSignedIn, - mockDisablePushNotifications, - mockEnablePushNotifications, - mockUpdateTriggerPushNotifications, - mockGetStorageKey, - mockPerformGetStorage, - mockPerformSetStorage, - }; -} - -function arrangeFailureAuthAssertions( - mocks: ReturnType, -) { - const testScenarios = { - NotLoggedIn: () => mocks.mockIsSignedIn.mockReturnValue(false), - - // unlikely, but in case it returns null - NoBearerToken: () => - mocks.mockGetBearerToken.mockResolvedValueOnce(null as unknown as string), - - RejectedBearerToken: () => - mocks.mockGetBearerToken.mockRejectedValueOnce( - new Error('MOCK - no bearer token'), - ), - }; - - return testScenarios; -} - -function arrangeFailureUserStorageKeyAssertions( - mocks: ReturnType, -) { - const testScenarios = { - NoStorageKey: () => - mocks.mockGetStorageKey.mockResolvedValueOnce(null as unknown as string), // unlikely but in case it returns null - RejectedStorageKey: () => - mocks.mockGetStorageKey.mockRejectedValueOnce( - new Error('MOCK - no storage key'), - ), - }; - return testScenarios; -} - -function arrangeFailureUserStorageAssertions( - mocks: ReturnType, -) { - const testScenarios = { - NoUserStorage: () => - mocks.mockPerformGetStorage.mockResolvedValueOnce(null), - ThrowUserStorage: () => - mocks.mockPerformGetStorage.mockRejectedValueOnce( - new Error('MOCK - Unable to call storage api'), - ), - }; - return testScenarios; -} diff --git a/app/scripts/controllers/metamask-notifications/metamask-notifications.ts b/app/scripts/controllers/metamask-notifications/metamask-notifications.ts deleted file mode 100644 index 5e63669f6cd5..000000000000 --- a/app/scripts/controllers/metamask-notifications/metamask-notifications.ts +++ /dev/null @@ -1,1219 +0,0 @@ -import { - BaseController, - RestrictedControllerMessenger, - ControllerGetStateAction, - StateMetadata, -} from '@metamask/base-controller'; -import log from 'loglevel'; -import { toChecksumHexAddress } from '@metamask/controller-utils'; -import { - KeyringControllerGetAccountsAction, - KeyringControllerStateChangeEvent, - KeyringControllerGetStateAction, - KeyringControllerLockEvent, - KeyringControllerUnlockEvent, -} from '@metamask/keyring-controller'; -import { - AuthenticationControllerGetBearerToken, - AuthenticationControllerIsSignedIn, -} from '../authentication/authentication-controller'; -import { - PushPlatformNotificationsControllerEnablePushNotifications, - PushPlatformNotificationsControllerDisablePushNotifications, - PushPlatformNotificationsControllerUpdateTriggerPushNotifications, - PushPlatformNotificationsControllerOnNewNotificationEvent, -} from '../push-platform-notifications/push-platform-notifications'; -import { - UserStorageControllerEnableProfileSyncing, - UserStorageControllerGetStorageKey, - UserStorageControllerPerformGetStorage, - UserStorageControllerPerformSetStorage, -} from '../user-storage/user-storage-controller'; -import { - TRIGGER_TYPES, - TRIGGER_TYPES_GROUPS, -} from './constants/notification-schema'; -import { USER_STORAGE_VERSION_KEY } from './constants/constants'; -import type { UserStorage } from './types/user-storage/user-storage'; -import * as FeatureNotifications from './services/feature-announcements'; -import * as OnChainNotifications from './services/onchain-notifications'; -import type { - Notification, - MarkAsReadNotificationsParam, -} from './types/notification/notification'; -import { OnChainRawNotification } from './types/on-chain-notification/on-chain-notification'; -import { processNotification } from './processors/process-notifications'; -import * as MetamaskNotificationsUtils from './utils/utils'; -import type { NotificationUnion } from './types/types'; - -// Unique name for the controller -const controllerName = 'MetamaskNotificationsController'; - -/** - * State shape for MetamaskNotificationsController - */ -export type MetamaskNotificationsControllerState = { - /** - * We store and manage accounts that have been seen/visted through the - * account subscription. This allows us to track and add notifications for new accounts and not previous accounts added. - */ - subscriptionAccountsSeen: string[]; - - /** - * Flag that indicates if the metamask notifications feature has been seen - */ - isMetamaskNotificationsFeatureSeen: boolean; - - /** - * Flag that indicates if the metamask notifications are enabled - */ - isMetamaskNotificationsEnabled: boolean; - - /** - * Flag that indicates if the feature announcements are enabled - */ - isFeatureAnnouncementsEnabled: boolean; - - /** - * List of metamask notifications - */ - metamaskNotificationsList: Notification[]; - - /** - * List of read metamask notifications - */ - metamaskNotificationsReadList: string[]; - /** - * Flag that indicates that the creating notifications is in progress - */ - isUpdatingMetamaskNotifications: boolean; - /** - * Flag that indicates that the fetching notifications is in progress - * This is used to show a loading spinner in the UI - * when fetching notifications - */ - isFetchingMetamaskNotifications: boolean; - /** - * Flag that indicates that the updating notifications for a specific address is in progress - */ - isUpdatingMetamaskNotificationsAccount: string[]; - /** - * Flag that indicates that the checking accounts presence is in progress - */ - isCheckingAccountsPresence: boolean; -}; - -const metadata: StateMetadata = { - subscriptionAccountsSeen: { - persist: true, - anonymous: true, - }, - - isMetamaskNotificationsFeatureSeen: { - persist: true, - anonymous: false, - }, - isMetamaskNotificationsEnabled: { - persist: true, - anonymous: false, - }, - isFeatureAnnouncementsEnabled: { - persist: true, - anonymous: false, - }, - metamaskNotificationsList: { - persist: true, - anonymous: true, - }, - metamaskNotificationsReadList: { - persist: true, - anonymous: true, - }, - isUpdatingMetamaskNotifications: { - persist: false, - anonymous: false, - }, - isFetchingMetamaskNotifications: { - persist: false, - anonymous: false, - }, - isUpdatingMetamaskNotificationsAccount: { - persist: false, - anonymous: false, - }, - isCheckingAccountsPresence: { - persist: false, - anonymous: false, - }, -}; -export const defaultState: MetamaskNotificationsControllerState = { - subscriptionAccountsSeen: [], - isMetamaskNotificationsFeatureSeen: false, - isMetamaskNotificationsEnabled: false, - isFeatureAnnouncementsEnabled: false, - metamaskNotificationsList: [], - metamaskNotificationsReadList: [], - isUpdatingMetamaskNotifications: false, - isFetchingMetamaskNotifications: false, - isUpdatingMetamaskNotificationsAccount: [], - isCheckingAccountsPresence: false, -}; - -export declare type MetamaskNotificationsControllerUpdateMetamaskNotificationsList = - { - type: `${typeof controllerName}:updateMetamaskNotificationsList`; - handler: MetamaskNotificationsController['updateMetamaskNotificationsList']; - }; - -export declare type MetamaskNotificationsControllerDisableMetamaskNotifications = - { - type: `${typeof controllerName}:disableMetamaskNotifications`; - handler: MetamaskNotificationsController['disableMetamaskNotifications']; - }; - -export declare type MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled = - { - type: `${typeof controllerName}:selectIsMetamaskNotificationsEnabled`; - handler: MetamaskNotificationsController['selectIsMetamaskNotificationsEnabled']; - }; - -export type MetamaskNotificationsControllerNotificationsListUpdatedEvent = { - type: `${typeof controllerName}:notificationsListUpdated`; - payload: [Notification[]]; -}; - -export type MetamaskNotificationsControllerMarkNotificationsAsRead = { - type: `${typeof controllerName}:markNotificationsAsRead`; - payload: [Notification[]]; -}; - -// Messenger Actions -export type Actions = - | MetamaskNotificationsControllerUpdateMetamaskNotificationsList - | MetamaskNotificationsControllerDisableMetamaskNotifications - | MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled - | ControllerGetStateAction<'state', MetamaskNotificationsControllerState>; - -// Allowed Actions -export type AllowedActions = - // Keyring Controller Requests - | KeyringControllerGetAccountsAction - | KeyringControllerGetStateAction - // Auth Controller Requests - | AuthenticationControllerGetBearerToken - | AuthenticationControllerIsSignedIn - // User Storage Controller Requests - | UserStorageControllerEnableProfileSyncing - | UserStorageControllerGetStorageKey - | UserStorageControllerPerformGetStorage - | UserStorageControllerPerformSetStorage - // Push Notifications Controller Requests - | PushPlatformNotificationsControllerEnablePushNotifications - | PushPlatformNotificationsControllerDisablePushNotifications - | PushPlatformNotificationsControllerUpdateTriggerPushNotifications; - -export type Events = - | MetamaskNotificationsControllerNotificationsListUpdatedEvent - | MetamaskNotificationsControllerMarkNotificationsAsRead; - -// Allowed Events -export type AllowedEvents = - // Keyring Events - | KeyringControllerStateChangeEvent - | KeyringControllerLockEvent - | KeyringControllerUnlockEvent - // Push Notification Events - | PushPlatformNotificationsControllerOnNewNotificationEvent; - -// Type for the messenger of MetamaskNotificationsController -export type MetamaskNotificationsControllerMessenger = - RestrictedControllerMessenger< - typeof controllerName, - Actions | AllowedActions, - Events | AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] - >; - -/** - * Controller that enables wallet notifications and feature announcements - */ -export class MetamaskNotificationsController extends BaseController< - typeof controllerName, - MetamaskNotificationsControllerState, - MetamaskNotificationsControllerMessenger -> { - // Flag to check is notifications have been setup when the browser/extension is initialized. - // We want to re-initialize push notifications when the browser/extension is refreshed - // To ensure we subscribe to the most up-to-date notifications - #isPushNotificationsSetup = false; - - #isUnlocked = false; - - #keyringController = { - setupLockedStateSubscriptions: (onUnlock: () => Promise) => { - const { isUnlocked } = this.messagingSystem.call( - 'KeyringController:getState', - ); - this.#isUnlocked = isUnlocked; - - this.messagingSystem.subscribe('KeyringController:unlock', () => { - this.#isUnlocked = true; - // messaging system cannot await promises - // we don't need to wait for a result on this. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - onUnlock(); - }); - - this.messagingSystem.subscribe('KeyringController:lock', () => { - this.#isUnlocked = false; - }); - }, - }; - - #auth = { - getBearerToken: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:getBearerToken', - ); - }, - isSignedIn: () => { - return this.messagingSystem.call('AuthenticationController:isSignedIn'); - }, - }; - - #storage = { - enableProfileSyncing: async () => { - return await this.messagingSystem.call( - 'UserStorageController:enableProfileSyncing', - ); - }, - getStorageKey: () => { - return this.messagingSystem.call('UserStorageController:getStorageKey'); - }, - getNotificationStorage: async () => { - return await this.messagingSystem.call( - 'UserStorageController:performGetStorage', - 'notification_settings', - ); - }, - setNotificationStorage: async (state: string) => { - return await this.messagingSystem.call( - 'UserStorageController:performSetStorage', - 'notification_settings', - state, - ); - }, - }; - - #pushNotifications = { - enablePushNotifications: async (UUIDs: string[]) => { - try { - await this.messagingSystem.call( - 'PushPlatformNotificationsController:enablePushNotifications', - UUIDs, - ); - } catch (e) { - log.error('Silently failed to enable push notifications', e); - } - }, - disablePushNotifications: async (UUIDs: string[]) => { - try { - await this.messagingSystem.call( - 'PushPlatformNotificationsController:disablePushNotifications', - UUIDs, - ); - } catch (e) { - log.error('Silently failed to disable push notifications', e); - } - }, - updatePushNotifications: async (UUIDs: string[]) => { - try { - await this.messagingSystem.call( - 'PushPlatformNotificationsController:updateTriggerPushNotifications', - UUIDs, - ); - } catch (e) { - log.error('Silently failed to update push notifications', e); - } - }, - subscribe: () => { - this.messagingSystem.subscribe( - 'PushPlatformNotificationsController:onNewNotifications', - (notification) => { - this.updateMetamaskNotificationsList(notification); - }, - ); - }, - initializePushNotifications: async () => { - if (!this.state.isMetamaskNotificationsEnabled) { - return; - } - if (this.#isPushNotificationsSetup) { - return; - } - if (!this.#isUnlocked) { - return; - } - - const storage = await this.#getUserStorage(); - if (!storage) { - return; - } - - const uuids = MetamaskNotificationsUtils.getAllUUIDs(storage); - await this.#pushNotifications.enablePushNotifications(uuids); - this.#isPushNotificationsSetup = true; - }, - }; - - #accounts = { - /** - * Used to get list of addresses from keyring (wallet addresses) - * - * @returns addresses removed, added, and latest list of addresses - */ - listAccounts: async () => { - // Get previous and current account sets - const nonChecksumAccounts = await this.messagingSystem.call( - 'KeyringController:getAccounts', - ); - const accounts = nonChecksumAccounts.map((a) => toChecksumHexAddress(a)); - const currentAccountsSet = new Set(accounts); - const prevAccountsSet = new Set(this.state.subscriptionAccountsSeen); - - // Invalid value you cannot have zero accounts - // Only occurs when the Accounts controller is initializing. - if (accounts.length === 0) { - return { - accountsAdded: [], - accountsRemoved: [], - accounts: [], - }; - } - - // Calculate added and removed addresses - const accountsAdded = accounts.filter((a) => !prevAccountsSet.has(a)); - const accountsRemoved = [...prevAccountsSet.values()].filter( - (a) => !currentAccountsSet.has(a), - ); - - // Update accounts seen - this.update((state) => { - state.subscriptionAccountsSeen = [...prevAccountsSet, ...accountsAdded]; - }); - - return { - accountsAdded, - accountsRemoved, - accounts, - }; - }, - - /** - * Initializes the cache/previous list. This is handy so we have an accurate in-mem state of the previous list of accounts. - * - * @returns result from list accounts - */ - initialize: () => { - return this.#accounts.listAccounts(); - }, - - /** - * Subscription to any state change in the keyring controller (aka wallet accounts). - * We can call the `listAccounts` defined above to find out about any accounts added, removed - * And call effects to subscribe/unsubscribe to notifications. - */ - subscribe: () => { - this.messagingSystem.subscribe( - 'KeyringController:stateChange', - async () => { - if (!this.state.isMetamaskNotificationsEnabled) { - return; - } - - const { accountsAdded, accountsRemoved } = - await this.#accounts.listAccounts(); - - const promises: Promise[] = []; - if (accountsAdded.length > 0) { - promises.push(this.updateOnChainTriggersByAccount(accountsAdded)); - } - if (accountsRemoved.length > 0) { - promises.push(this.deleteOnChainTriggersByAccount(accountsRemoved)); - } - await Promise.all(promises); - }, - ); - }, - }; - - /** - * Creates a MetamaskNotificationsController instance. - * - * @param args - The arguments to this function. - * @param args.messenger - Messenger used to communicate with BaseV2 controller. - * @param args.state - Initial state to set on this controller. - */ - constructor({ - messenger, - state, - }: { - messenger: MetamaskNotificationsControllerMessenger; - state?: Partial; - }) { - super({ - messenger, - metadata, - name: controllerName, - state: { ...defaultState, ...state }, - }); - - this.#registerMessageHandlers(); - this.#clearLoadingStates(); - this.#keyringController.setupLockedStateSubscriptions( - this.#pushNotifications.initializePushNotifications, - ); - this.#accounts.initialize(); - this.#pushNotifications.initializePushNotifications(); - this.#accounts.subscribe(); - this.#pushNotifications.subscribe(); - } - - #registerMessageHandlers(): void { - this.messagingSystem.registerActionHandler( - `${controllerName}:updateMetamaskNotificationsList`, - this.updateMetamaskNotificationsList.bind(this), - ); - - this.messagingSystem.registerActionHandler( - `${controllerName}:disableMetamaskNotifications`, - this.disableMetamaskNotifications.bind(this), - ); - - this.messagingSystem.registerActionHandler( - `${controllerName}:selectIsMetamaskNotificationsEnabled`, - this.selectIsMetamaskNotificationsEnabled.bind(this), - ); - } - - #clearLoadingStates(): void { - this.update((state) => { - state.isUpdatingMetamaskNotifications = false; - state.isCheckingAccountsPresence = false; - state.isFetchingMetamaskNotifications = false; - state.isUpdatingMetamaskNotificationsAccount = []; - }); - } - - #assertAuthEnabled() { - if (!this.#auth.isSignedIn()) { - this.update((state) => { - state.isMetamaskNotificationsEnabled = false; - }); - throw new Error('User is not signed in.'); - } - } - - async #getValidStorageKeyAndBearerToken() { - this.#assertAuthEnabled(); - - const bearerToken = await this.#auth.getBearerToken(); - const storageKey = await this.#storage.getStorageKey(); - - if (!bearerToken || !storageKey) { - throw new Error('Missing BearerToken or storage key'); - } - - return { bearerToken, storageKey }; - } - - #performEnableProfileSyncing = async () => { - try { - await this.#storage.enableProfileSyncing(); - } catch (e) { - log.error('Failed to enable profile syncing', e); - throw new Error('Failed to enable profile syncing'); - } - }; - - #assertUserStorage( - storage: UserStorage | null, - ): asserts storage is UserStorage { - if (!storage) { - throw new Error('User Storage does not exist'); - } - } - - /** - * Retrieves and parses the user storage from the storage key. - * - * This method attempts to retrieve the user storage using the specified storage key, - * then parses the JSON string to an object. If the storage is not found or cannot be parsed, - * it throws an error. - * - * @returns The parsed user storage object or null - */ - async #getUserStorage(): Promise { - const userStorageString: string | null = - await this.#storage.getNotificationStorage(); - - if (!userStorageString) { - return null; - } - - try { - const userStorage: UserStorage = JSON.parse(userStorageString); - return userStorage; - } catch (error) { - log.error('Unable to parse User Storage'); - return null; - } - } - - /** - * @deprecated - This needs rework for it to be feasible. Currently this is a half-baked solution, as it fails once we add new triggers (introspection for filters is difficult). - * - * Checks for the complete presence of trigger types by group across all addresses in user storage. - * - * This method retrieves the user storage and uses `MetamaskNotificationsUtils` to verify if all expected trigger types for each group are present for every address. - * @returns A record indicating whether all expected trigger types for each group are present for every address. - * @throws {Error} If user storage does not exist. - */ - public async checkTriggersPresenceByGroup(): Promise< - Record - > { - const userStorage = await this.#getUserStorage(); - this.#assertUserStorage(userStorage); - - // Use MetamaskNotificationsUtils to check the presence of triggers - return MetamaskNotificationsUtils.checkTriggersPresenceByGroup(userStorage); - } - - /** - * Retrieves the current enabled state of MetaMask notifications. - * - * This method directly returns the boolean value of `isMetamaskNotificationsEnabled` - * from the controller's state, indicating whether MetaMask notifications are currently enabled. - * - * @returns The enabled state of MetaMask notifications. - */ - public selectIsMetamaskNotificationsEnabled(): boolean { - return this.state.isMetamaskNotificationsEnabled; - } - - /** - * Sets the state of notification creation process. - * - * This method updates the `isUpdatingMetamaskNotifications` state, which can be used to indicate - * whether the notification creation process is currently active or not. This is useful - * for UI elements that need to reflect the state of ongoing operations, such as loading - * indicators or disabled buttons during processing. - * - * @param isUpdatingMetamaskNotifications - A boolean value representing the new state of the notification creation process. - */ - #setIsUpdatingMetamaskNotifications( - isUpdatingMetamaskNotifications: boolean, - ) { - this.update((state) => { - state.isUpdatingMetamaskNotifications = isUpdatingMetamaskNotifications; - }); - } - - /** - * Updates the state to indicate whether fetching of MetaMask notifications is in progress. - * - * This method is used to set the `isFetchingMetamaskNotifications` state, which can be utilized - * to show or hide loading indicators in the UI when notifications are being fetched. - * - * @param isFetchingMetamaskNotifications - A boolean value representing the fetching state. - */ - #setIsFetchingMetamaskNotifications( - isFetchingMetamaskNotifications: boolean, - ) { - this.update((state) => { - state.isFetchingMetamaskNotifications = isFetchingMetamaskNotifications; - }); - } - - /** - * Updates the state to indicate that the checking of accounts presence is in progress. - * - * This method modifies the `isCheckingAccountsPresence` state, which can be used to manage UI elements - * that depend on the status of account presence checks, such as displaying loading indicators or disabling - * buttons while the check is ongoing. - * - * @param isCheckingAccountsPresence - A boolean value indicating whether the account presence check is currently active. - */ - #setIsCheckingAccountsPresence(isCheckingAccountsPresence: boolean) { - this.update((state) => { - state.isCheckingAccountsPresence = isCheckingAccountsPresence; - }); - } - - /** - * Updates the state to indicate that account updates are in progress. - * Removes duplicate accounts before updating the state. - * - * @param accounts - The accounts being updated. - */ - #updateUpdatingAccountsState(accounts: string[]) { - this.update((state) => { - const uniqueAccounts = new Set([ - ...state.isUpdatingMetamaskNotificationsAccount, - ...accounts, - ]); - state.isUpdatingMetamaskNotificationsAccount = Array.from(uniqueAccounts); - }); - } - - /** - * Clears the state indicating that account updates are complete. - * - * @param accounts - The accounts that have finished updating. - */ - #clearUpdatingAccountsState(accounts: string[]) { - this.update((state) => { - state.isUpdatingMetamaskNotificationsAccount = - state.isUpdatingMetamaskNotificationsAccount.filter( - (existingAccount) => !accounts.includes(existingAccount), - ); - }); - } - - public async checkAccountsPresence( - accounts: string[], - ): Promise> { - try { - this.#setIsCheckingAccountsPresence(true); - - // Retrieve user storage - const userStorage = await this.#getUserStorage(); - this.#assertUserStorage(userStorage); - - const presence = MetamaskNotificationsUtils.checkAccountsPresence( - userStorage, - accounts, - ); - return presence; - } catch (error) { - log.error('Failed to check accounts presence', error); - throw error; - } finally { - this.#setIsCheckingAccountsPresence(false); - } - } - - /** - * Sets the enabled state of feature announcements. - * - * **Action** - used in the notification settings to enable/disable feature announcements. - * - * @param featureAnnouncementsEnabled - A boolean value indicating the desired enabled state of the feature announcements. - * @async - * @throws {Error} If fails to update - */ - public async setFeatureAnnouncementsEnabled( - featureAnnouncementsEnabled: boolean, - ) { - try { - this.update((s) => { - s.isFeatureAnnouncementsEnabled = featureAnnouncementsEnabled; - }); - } catch (e) { - log.error('Unable to toggle feature announcements', e); - throw new Error('Unable to toggle feature announcements'); - } - } - - /** - * This creates/re-creates on-chain triggers defined in User Storage. - * - * **Action** - Used during Sign In / Enabling of notifications. - * - * @returns The updated or newly created user storage. - * @throws {Error} Throws an error if unauthenticated or from other operations. - */ - public async createOnChainTriggers(): Promise { - try { - this.#setIsUpdatingMetamaskNotifications(true); - - await this.#performEnableProfileSyncing(); - - const { bearerToken, storageKey } = - await this.#getValidStorageKeyAndBearerToken(); - - const { accounts } = await this.#accounts.listAccounts(); - - let userStorage = await this.#getUserStorage(); - - // If userStorage does not exist, create a new one - // All the triggers created are set as: "disabled" - if (userStorage?.[USER_STORAGE_VERSION_KEY] === undefined) { - userStorage = MetamaskNotificationsUtils.initializeUserStorage( - accounts.map((account) => ({ address: account })), - false, - ); - - // Write the userStorage - await this.#storage.setNotificationStorage(JSON.stringify(userStorage)); - } - - // Create the triggers - const triggers = - MetamaskNotificationsUtils.traverseUserStorageTriggers(userStorage); - await OnChainNotifications.createOnChainTriggers( - userStorage, - storageKey, - bearerToken, - triggers, - ); - - // Create push notifications triggers - const allUUIDS = MetamaskNotificationsUtils.getAllUUIDs(userStorage); - await this.#pushNotifications.enablePushNotifications(allUUIDS); - - // Write the new userStorage (triggers are now "enabled") - await this.#storage.setNotificationStorage(JSON.stringify(userStorage)); - - // Update the state of the controller - this.update((state) => { - state.isMetamaskNotificationsEnabled = true; - state.isFeatureAnnouncementsEnabled = true; - state.isMetamaskNotificationsFeatureSeen = true; - }); - - return userStorage; - } catch (err) { - log.error('Failed to create On Chain triggers', err); - throw new Error('Failed to create On Chain triggers'); - } finally { - this.#setIsUpdatingMetamaskNotifications(false); - } - } - - /** - * Enables all MetaMask notifications for the user. - * This is identical flow when initializing notifications for the first time. - * 1. Enable Profile Syncing - * 2. Get or Create Notification User Storage - * 3. Upsert Triggers - * 4. Update Push notifications - * - * @throws {Error} If there is an error during the process of enabling notifications. - */ - public async enableMetamaskNotifications() { - try { - this.#setIsUpdatingMetamaskNotifications(true); - await this.createOnChainTriggers(); - } catch (e) { - log.error('Unable to enable notifications', e); - throw new Error('Unable to enable notifications'); - } finally { - this.#setIsUpdatingMetamaskNotifications(false); - } - } - - /** - * Disables all MetaMask notifications for the user. - * This method ensures that the user is authenticated, retrieves all linked accounts, - * and disables on-chain triggers for each account. It also sets the global notification - * settings for MetaMask, feature announcements to false. - * - * @throws {Error} If the user is not authenticated or if there is an error during the process. - */ - public async disableMetamaskNotifications() { - try { - this.#setIsUpdatingMetamaskNotifications(true); - - // Disable Push Notifications - const userStorage = await this.#getUserStorage(); - this.#assertUserStorage(userStorage); - const UUIDs = MetamaskNotificationsUtils.getAllUUIDs(userStorage); - await this.#pushNotifications.disablePushNotifications(UUIDs); - - // Clear Notification States (toggles and list) - this.update((state) => { - state.isMetamaskNotificationsEnabled = false; - state.isFeatureAnnouncementsEnabled = false; - state.metamaskNotificationsList = []; - }); - } catch (e) { - log.error('Unable to disable notifications', e); - throw new Error('Unable to disable notifications'); - } finally { - this.#setIsUpdatingMetamaskNotifications(false); - } - } - - /** - * Deletes on-chain triggers associated with a specific account. - * This method performs several key operations: - * 1. Validates Auth & Storage - * 2. Finds and deletes all triggers associated with the account - * 3. Disables any related push notifications - * 4. Updates Storage to reflect new state. - * - * **Action** - When a user disables notifications for a given account in settings. - * - * @param accounts - The account for which on-chain triggers are to be deleted. - * @returns A promise that resolves to void or an object containing a success message. - * @throws {Error} Throws an error if unauthenticated or from other operations. - */ - public async deleteOnChainTriggersByAccount( - accounts: string[], - ): Promise { - try { - this.#updateUpdatingAccountsState(accounts); - // Get and Validate BearerToken and User Storage Key - const { bearerToken, storageKey } = - await this.#getValidStorageKeyAndBearerToken(); - - // Get & Validate User Storage - const userStorage = await this.#getUserStorage(); - this.#assertUserStorage(userStorage); - - // Get the UUIDs to delete - const UUIDs = accounts - .map((a) => - MetamaskNotificationsUtils.getUUIDsForAccount( - userStorage, - a.toLowerCase(), - ), - ) - .flat(); - - if (UUIDs.length === 0) { - return userStorage; - } - - // Delete these UUIDs (Mutates User Storage) - await OnChainNotifications.deleteOnChainTriggers( - userStorage, - storageKey, - bearerToken, - UUIDs, - ); - - // Delete these UUIDs from the push notifications - await this.#pushNotifications.disablePushNotifications(UUIDs); - - // Update User Storage - await this.#storage.setNotificationStorage(JSON.stringify(userStorage)); - return userStorage; - } catch (err) { - log.error('Failed to delete OnChain triggers', err); - throw new Error('Failed to delete OnChain triggers'); - } finally { - this.#clearUpdatingAccountsState(accounts); - } - } - - /** - * Updates/Creates on-chain triggers for a specific account. - * - * This method performs several key operations: - * 1. Validates Auth & Storage - * 2. Finds and creates any missing triggers associated with the account - * 3. Enables any related push notifications - * 4. Updates Storage to reflect new state. - * - * **Action** - When a user enables notifications for an account - * - * @param accounts - List of accounts you want to update. - * @returns A promise that resolves to the updated user storage. - * @throws {Error} Throws an error if unauthenticated or from other operations. - */ - public async updateOnChainTriggersByAccount( - accounts: string[], - ): Promise { - try { - this.#updateUpdatingAccountsState(accounts); - // Get and Validate BearerToken and User Storage Key - const { bearerToken, storageKey } = - await this.#getValidStorageKeyAndBearerToken(); - - // Get & Validate User Storage - const userStorage = await this.#getUserStorage(); - this.#assertUserStorage(userStorage); - - // Add any missing triggers - accounts.forEach((a) => - MetamaskNotificationsUtils.upsertAddressTriggers(a, userStorage), - ); - - const newTriggers = - MetamaskNotificationsUtils.traverseUserStorageTriggers(userStorage, { - mapTrigger: (t) => { - if (t.enabled === false) { - return t; - } - return undefined; - }, - }); - - // Create any missing triggers. - if (newTriggers.length > 0) { - // Write te updated userStorage (where triggers are disabled) - await this.#storage.setNotificationStorage(JSON.stringify(userStorage)); - - // Create the triggers - const triggers = MetamaskNotificationsUtils.traverseUserStorageTriggers( - userStorage, - { - mapTrigger: (t) => { - if ( - accounts.some( - (a) => a.toLowerCase() === t.address.toLowerCase(), - ) - ) { - return t; - } - return undefined; - }, - }, - ); - await OnChainNotifications.createOnChainTriggers( - userStorage, - storageKey, - bearerToken, - triggers, - ); - } - - // Update Push Notifications Triggers - const UUIDs = MetamaskNotificationsUtils.getAllUUIDs(userStorage); - await this.#pushNotifications.updatePushNotifications(UUIDs); - - // Update the userStorage (where triggers are enabled) - await this.#storage.setNotificationStorage(JSON.stringify(userStorage)); - return userStorage; - } catch (err) { - log.error('Failed to update OnChain triggers', err); - throw new Error('Failed to update OnChain triggers'); - } finally { - this.#clearUpdatingAccountsState(accounts); - } - } - - /** - * Fetches the list of metamask notifications. - * This includes OnChain notifications and Feature Announcements. - * - * **Action** - When a user views the notification list page/dropdown - * - * @throws {Error} Throws an error if unauthenticated or from other operations. - */ - public async fetchAndUpdateMetamaskNotifications(): Promise { - try { - this.#setIsFetchingMetamaskNotifications(true); - - // Raw Feature Notifications - const rawFeatureAnnouncementNotifications = this.state - .isFeatureAnnouncementsEnabled - ? await FeatureNotifications.getFeatureAnnouncementNotifications().catch( - () => [], - ) - : []; - - // Raw On Chain Notifications - const rawOnChainNotifications: OnChainRawNotification[] = []; - const userStorage = await this.#storage - .getNotificationStorage() - .then((s) => s && (JSON.parse(s) as UserStorage)) - .catch(() => null); - const bearerToken = await this.#auth.getBearerToken().catch(() => null); - if (userStorage && bearerToken) { - const notifications = - await OnChainNotifications.getOnChainNotifications( - userStorage, - bearerToken, - ).catch(() => []); - - rawOnChainNotifications.push(...notifications); - } - - const readIds = this.state.metamaskNotificationsReadList; - - // Combined Notifications - const isNotUndefined = (t?: T): t is T => Boolean(t); - const processAndFilter = (ns: NotificationUnion[]) => - ns - .map((n) => { - try { - return processNotification(n, readIds); - } catch { - // So we don't throw and show no notifications - return undefined; - } - }) - .filter(isNotUndefined); - - const featureAnnouncementNotifications = processAndFilter( - rawFeatureAnnouncementNotifications, - ); - const onChainNotifications = processAndFilter(rawOnChainNotifications); - - const metamaskNotifications: Notification[] = [ - ...featureAnnouncementNotifications, - ...onChainNotifications, - ]; - metamaskNotifications.sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); - - // Update State - this.update((state) => { - state.metamaskNotificationsList = metamaskNotifications; - }); - - this.messagingSystem.publish( - `${controllerName}:notificationsListUpdated`, - this.state.metamaskNotificationsList, - ); - - this.#setIsFetchingMetamaskNotifications(false); - return metamaskNotifications; - } catch (err) { - this.#setIsFetchingMetamaskNotifications(false); - log.error('Failed to fetch notifications', err); - throw new Error('Failed to fetch notifications'); - } - } - - /** - * Marks specified metamask notifications as read. - * - * @param notifications - An array of notifications to be marked as read. Each notification should include its type and read status. - * @returns A promise that resolves when the operation is complete. - */ - public async markMetamaskNotificationsAsRead( - notifications: MarkAsReadNotificationsParam, - ): Promise { - let onchainNotificationIds: string[] = []; - let featureAnnouncementNotificationIds: string[] = []; - - try { - // Filter unread on/off chain notifications - const onChainNotifications = notifications.filter( - (notification) => - notification.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT && - !notification.isRead, - ); - - const featureAnnouncementNotifications = notifications.filter( - (notification) => - notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT && - !notification.isRead, - ); - - // Mark On-Chain Notifications as Read - if (onChainNotifications.length > 0) { - const bearerToken = await this.#auth.getBearerToken(); - - if (bearerToken) { - onchainNotificationIds = onChainNotifications.map( - (notification) => notification.id, - ); - await OnChainNotifications.markNotificationsAsRead( - bearerToken, - onchainNotificationIds, - ).catch(() => { - onchainNotificationIds = []; - log.warn('Unable to mark onchain notifications as read'); - }); - } - } - - // Mark Off-Chain notifications as Read - if (featureAnnouncementNotifications.length > 0) { - featureAnnouncementNotificationIds = - featureAnnouncementNotifications.map( - (notification) => notification.id, - ); - } - } catch (err) { - log.warn('Something failed when marking notifications as read', err); - } - - // Update the state - this.update((state) => { - const currentReadList = state.metamaskNotificationsReadList; - const newReadIds = [...featureAnnouncementNotificationIds]; - state.metamaskNotificationsReadList = [ - ...new Set([...currentReadList, ...newReadIds]), - ]; - - state.metamaskNotificationsList = state.metamaskNotificationsList.map( - (notification: Notification) => { - if ( - newReadIds.includes(notification.id) || - onchainNotificationIds.includes(notification.id) - ) { - return { ...notification, isRead: true }; - } - return notification; - }, - ); - }); - - // Publish the event - this.messagingSystem.publish( - `${controllerName}:markNotificationsAsRead`, - this.state.metamaskNotificationsList, - ); - } - - /** - * Updates the list of MetaMask notifications by adding a new notification at the beginning of the list. - * This method ensures that the most recent notification is displayed first in the UI. - * - * @param notification - The new notification object to be added to the list. - * @returns A promise that resolves when the notification list has been successfully updated. - */ - public async updateMetamaskNotificationsList( - notification: Notification, - ): Promise { - if ( - this.state.metamaskNotificationsList.some((n) => n.id === notification.id) - ) { - return; - } - - const processedNotification = - processAndFilterSingleNotification(notification); - - if (processedNotification) { - this.update((state) => { - const existingNotificationIds = new Set( - state.metamaskNotificationsList.map((n) => n.id), - ); - // Add the new notification only if its ID is not already present in the list - if (!existingNotificationIds.has(notification.id)) { - state.metamaskNotificationsList = [ - notification, - ...state.metamaskNotificationsList, - ]; - this.messagingSystem.publish( - `${controllerName}:notificationsListUpdated`, - state.metamaskNotificationsList, - ); - } - }); - } - } -} - -const isNotUndefined = (t?: T): t is T => Boolean(t); -function processAndFilterSingleNotification(n: NotificationUnion) { - try { - const processedNotification = processNotification(n); - if (isNotUndefined(processedNotification)) { - return processedNotification; - } - } catch { - return undefined; - } - return undefined; -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts deleted file mode 100644 index 71e2b637e5bb..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { ContentfulResult } from '../services/feature-announcements'; -import { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; -import { TRIGGER_TYPES } from '../constants/notification-schema'; - -export function createMockFeatureAnnouncementAPIResult(): ContentfulResult { - return { - sys: { - type: 'Array', - }, - total: 17, - skip: 0, - limit: 1, - items: [ - { - metadata: { - tags: [], - }, - sys: { - space: { - sys: { - type: 'Link', - linkType: 'Space', - id: 'jdkgyfmyd9sw', - }, - }, - id: '1ABRmHaNCgmxROKXXLXsMu', - type: 'Entry', - createdAt: '2024-04-09T13:24:01.872Z', - updatedAt: '2024-04-09T13:24:01.872Z', - environment: { - sys: { - id: 'master', - type: 'Link', - linkType: 'Environment', - }, - }, - revision: 1, - contentType: { - sys: { - type: 'Link', - linkType: 'ContentType', - id: 'productAnnouncement', - }, - }, - locale: 'en-US', - }, - fields: { - title: 'Don’t miss out on airdrops and new NFT mints!', - id: 'dont-miss-out-on-airdrops-and-new-nft-mints', - category: 'ANNOUNCEMENT', - shortDescription: - 'Check your airdrop eligibility and see trending NFT drops. Head over to the Explore tab to get started. ', - image: { - sys: { - type: 'Link', - linkType: 'Asset', - id: '5jqq8sFeLc6XEoeWlpI3aB', - }, - }, - longDescription: { - data: {}, - content: [ - { - data: {}, - content: [ - { - data: {}, - marks: [], - value: - 'You can now verify if any of your connected addresses are eligible for airdrops and other ERC-20 claims in a secure and convenient way. We’ve also added trending NFT mints based on creators you’ve minted from before or other tokens you hold. Head over to the Explore tab to get started. \n', - nodeType: 'text', - }, - ], - nodeType: 'paragraph', - }, - ], - nodeType: 'document', - }, - link: { - sys: { - type: 'Link', - linkType: 'Entry', - id: '62xKYM2ydo4F1mS5q97K5q', - }, - }, - }, - }, - ], - includes: { - Entry: [ - { - metadata: { - tags: [], - }, - sys: { - space: { - sys: { - type: 'Link', - linkType: 'Space', - id: 'jdkgyfmyd9sw', - }, - }, - id: '62xKYM2ydo4F1mS5q97K5q', - type: 'Entry', - createdAt: '2024-04-09T13:23:03.636Z', - updatedAt: '2024-04-09T13:23:03.636Z', - environment: { - sys: { - id: 'master', - type: 'Link', - linkType: 'Environment', - }, - }, - revision: 1, - contentType: { - sys: { - type: 'Link', - linkType: 'ContentType', - id: 'link', - }, - }, - locale: 'en-US', - }, - fields: { - extensionLinkText: 'Try now', - extensionLinkRoute: 'home.html', - }, - }, - ], - Asset: [ - { - metadata: { - tags: [], - }, - sys: { - space: { - sys: { - type: 'Link', - linkType: 'Space', - id: 'jdkgyfmyd9sw', - }, - }, - id: '5jqq8sFeLc6XEoeWlpI3aB', - type: 'Asset', - createdAt: '2024-04-09T13:23:13.327Z', - updatedAt: '2024-04-09T13:23:13.327Z', - environment: { - sys: { - id: 'master', - type: 'Link', - linkType: 'Environment', - }, - }, - revision: 1, - locale: 'en-US', - }, - fields: { - title: 'PDAPP notification image Airdrops & NFT mints', - description: '', - file: { - url: '//images.ctfassets.net/jdkgyfmyd9sw/5jqq8sFeLc6XEoeWlpI3aB/73ee0f1afa9916c3a7538b0bbee09c26/PDAPP_notification_image_Airdrops___NFT_mints.png', - details: { - size: 797731, - image: { - width: 2880, - height: 1921, - }, - }, - fileName: 'PDAPP notification image_Airdrops & NFT mints.png', - contentType: 'image/png', - }, - }, - }, - ], - }, - } as unknown as ContentfulResult; -} - -export function createMockFeatureAnnouncementRaw(): FeatureAnnouncementRawNotification { - return { - type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, - createdAt: '2999-04-09T13:24:01.872Z', - data: { - id: 'dont-miss-out-on-airdrops-and-new-nft-mints', - category: 'ANNOUNCEMENT', - title: 'Don’t miss out on airdrops and new NFT mints!', - longDescription: `

You can now verify if any of your connected addresses are eligible for airdrops and other ERC-20 claims in a secure and convenient way. We’ve also added trending NFT mints based on creators you’ve minted from before or other tokens you hold. Head over to the Explore tab to get started.

`, - shortDescription: - 'Check your airdrop eligibility and see trending NFT drops. Head over to the Explore tab to get started.', - image: { - title: 'PDAPP notification image Airdrops & NFT mints', - description: '', - url: '//images.ctfassets.net/jdkgyfmyd9sw/5jqq8sFeLc6XEoeWlpI3aB/73ee0f1afa9916c3a7538b0bbee09c26/PDAPP_notification_image_Airdrops___NFT_mints.png', - }, - extensionLink: { - extensionLinkText: 'Try now', - extensionLinkRoute: 'home.html', - }, - }, - }; -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts deleted file mode 100644 index d4ce4a61114f..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; -import { NotificationTrigger } from '../utils/utils'; - -export function createMockNotificationTrigger( - override?: Partial, -): NotificationTrigger { - return { - id: uuidv4(), - address: '0xFAKE_ADDRESS', - chainId: '1', - kind: 'eth_sent', - enabled: true, - ...override, - }; -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-notification-user-storage.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-notification-user-storage.ts deleted file mode 100644 index a799a8d35b52..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-notification-user-storage.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { USER_STORAGE_VERSION_KEY } from '../constants/constants'; -import { UserStorage } from '../types/user-storage/user-storage'; -import { initializeUserStorage } from '../utils/utils'; - -export const MOCK_USER_STORAGE_ACCOUNT = - '0x0000000000000000000000000000000000000000'; -export const MOCK_USER_STORAGE_CHAIN = '1'; - -export function createMockUserStorage( - override?: Partial, -): UserStorage { - return { - [USER_STORAGE_VERSION_KEY]: '1', - [MOCK_USER_STORAGE_ACCOUNT]: { - [MOCK_USER_STORAGE_CHAIN]: { - '111-111-111-111': { - k: TRIGGER_TYPES.ERC20_RECEIVED, - e: true, - }, - '222-222-222-222': { - k: TRIGGER_TYPES.ERC20_SENT, - e: true, - }, - }, - }, - ...override, - }; -} - -export function createMockUserStorageWithTriggers( - triggers: string[] | { id: string; e: boolean; k?: TRIGGER_TYPES }[], -): UserStorage { - const userStorage: UserStorage = { - [USER_STORAGE_VERSION_KEY]: '1', - [MOCK_USER_STORAGE_ACCOUNT]: { - [MOCK_USER_STORAGE_CHAIN]: {}, - }, - }; - - // insert triggerIds - triggers.forEach((t) => { - let tId: string; - let e: boolean; - let k: TRIGGER_TYPES; - if (typeof t === 'string') { - tId = t; - e = true; - k = TRIGGER_TYPES.ERC20_RECEIVED; - } else { - tId = t.id; - e = t.e; - k = t.k ?? TRIGGER_TYPES.ERC20_RECEIVED; - } - - userStorage[MOCK_USER_STORAGE_ACCOUNT][MOCK_USER_STORAGE_CHAIN][tId] = { - k, - e, - }; - }); - - return userStorage; -} - -export function createMockFullUserStorage( - props: { triggersEnabled?: boolean; address?: string } = {}, -): UserStorage { - return initializeUserStorage( - [{ address: props.address ?? MOCK_USER_STORAGE_ACCOUNT }], - props.triggersEnabled ?? true, - ); -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts deleted file mode 100644 index f78b4a53a21f..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts +++ /dev/null @@ -1,609 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; - -export function createMockNotificationEthSent() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ETH_SENT, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C', - unread: true, - created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'eth_sent', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - from: '0x881D40237659C251811CEC9c364ef91dC08D300C', - to: '0x881D40237659C251811CEC9c364ef91dC08D300D', - amount: { - usd: '670.64', - eth: '0.005', - }, - }, - }; - - return mockNotification; -} - -export function createMockNotificationEthReceived() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ETH_RECEIVED, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C', - unread: true, - created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'eth_received', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - from: '0x881D40237659C251811CEC9c364ef91dC08D300C', - to: '0x881D40237659C251811CEC9c364ef91dC08D300D', - amount: { - usd: '670.64', - eth: '808.000000000000000000', - }, - }, - }; - - return mockNotification; -} - -export function createMockNotificationERC20Sent() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC20_SENT, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C', - unread: true, - created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'erc20_sent', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - to: '0xecc19e177d24551aa7ed6bc6fe566eca726cc8a9', - from: '0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae', - token: { - usd: '1.00', - name: 'USDC', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdc.svg', - amount: '4956250000', - symbol: 'USDC', - address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - decimals: '6', - }, - }, - }; - - return mockNotification; -} - -export function createMockNotificationERC20Received() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC20_RECEIVED, - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - trigger_id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - chain_id: 1, - block_number: 17485840, - block_timestamp: '2022-03-01T00:00:00Z', - tx_hash: '0x881D40237659C251811CEC9c364ef91dC08D300C', - unread: true, - created_at: '2022-03-01T00:00:00Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'erc20_received', - network_fee: { - gas_price: '207806259583', - native_token_price_in_usd: '0.83', - }, - to: '0xeae7380dd4cef6fbd1144f49e4d1e6964258a4f4', - from: '0x51c72848c68a965f66fa7a88855f9f7784502a7f', - token: { - usd: '0.00', - name: 'SHIBA INU', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/shib.svg', - amount: '8382798736999999457296646144', - symbol: 'SHIB', - address: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce', - decimals: '18', - }, - }, - }; - - return mockNotification; -} - -export function createMockNotificationERC721Sent() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC721_SENT, - block_number: 18576643, - block_timestamp: '1700043467', - chain_id: 1, - created_at: '2023-11-15T11:08:17.895407Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0xf47f628fe3bd2595e9ab384bfffc3859b448e451', - nft: { - // This is not a hex color - // eslint-disable-next-line @metamask/design-tokens/color-no-hex - name: 'Captainz #8680', - image: - 'https://i.seadn.io/s/raw/files/ae0fc06714ff7fb40217340d8a242c0e.gif?w=500&auto=format', - token_id: '8680', - collection: { - name: 'The Captainz', - image: - 'https://i.seadn.io/gcs/files/6df4d75778066bce740050615bc84e21.png?w=500&auto=format', - symbol: 'Captainz', - address: '0x769272677fab02575e84945f03eca517acc544cc', - }, - }, - from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', - kind: 'erc721_sent', - network_fee: { - gas_price: '24550653274', - native_token_price_in_usd: '1986.61', - }, - }, - id: 'a4193058-9814-537e-9df4-79dcac727fb6', - trigger_id: '028485be-b994-422b-a93b-03fcc01ab715', - tx_hash: - '0x0833c69fb41cf972a0f031fceca242939bc3fcf82b964b74606649abcad371bd', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationERC721Received() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC721_RECEIVED, - block_number: 18571446, - block_timestamp: '1699980623', - chain_id: 1, - created_at: '2023-11-14T17:40:52.319281Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0xba7f3daa8adfdad686574406ab9bd5d2f0a49d2e', - nft: { - // This is not a hex color - // eslint-disable-next-line @metamask/design-tokens/color-no-hex - name: 'The Plague #2722', - image: - 'https://i.seadn.io/s/raw/files/a96f90ec8ebf55a2300c66a0c46d6a16.png?w=500&auto=format', - token_id: '2722', - collection: { - name: 'The Plague NFT', - image: - 'https://i.seadn.io/gcs/files/4577987a5ca45ca5118b2e31559ee4d1.jpg?w=500&auto=format', - symbol: 'FROG', - address: '0xc379e535caff250a01caa6c3724ed1359fe5c29b', - }, - }, - from: '0x24a0bb54b7e7a8e406e9b28058a9fd6c49e6df4f', - kind: 'erc721_received', - network_fee: { - gas_price: '53701898538', - native_token_price_in_usd: '2047.01', - }, - }, - id: '00a79d24-befa-57ed-a55a-9eb8696e1654', - trigger_id: 'd24ac26a-8579-49ec-9947-d04d63592ebd', - tx_hash: - '0xe554c9e29e6eeca8ba94da4d047334ba08b8eb9ca3b801dd69cec08dfdd4ae43', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationERC1155Sent() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC1155_SENT, - block_number: 18615206, - block_timestamp: '1700510003', - chain_id: 1, - created_at: '2023-11-20T20:44:10.110706Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', - nft: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', - token_id: '1', - collection: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', - symbol: 'TRUTH', - address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', - }, - }, - from: '0x0000000000000000000000000000000000000000', - kind: 'erc1155_sent', - network_fee: { - gas_price: '33571446596', - native_token_price_in_usd: '2038.88', - }, - }, - id: 'a09ff9d1-623a-52ab-a3d4-c7c8c9a58362', - trigger_id: 'e2130f7d-78b8-4c34-999a-3f3d3bb5b03c', - tx_hash: - '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationERC1155Received() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ERC1155_RECEIVED, - block_number: 18615206, - block_timestamp: '1700510003', - chain_id: 1, - created_at: '2023-11-20T20:44:10.110706Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - to: '0x15bd77ccacf2da39b84f0c31fee2e451225bb190', - nft: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gcs/files/79a77cb37c7b2f1069f752645d29fea7.jpg?w=500&auto=format', - token_id: '1', - collection: { - name: 'IlluminatiNFT DAO', - image: - 'https://i.seadn.io/gae/LTKz3om2eCQfn3M6PkqEmY7KhLtdMCOm0QVch2318KJq7-KyToCH7NBTMo4UuJ0AZI-oaBh1HcgrAEIEWYbXY3uMcYpuGXunaXEh?w=500&auto=format', - symbol: 'TRUTH', - address: '0xe25f0fe686477f9df3c2876c4902d3b85f75f33a', - }, - }, - from: '0x0000000000000000000000000000000000000000', - kind: 'erc1155_received', - network_fee: { - gas_price: '33571446596', - native_token_price_in_usd: '2038.88', - }, - }, - id: 'b6b93c84-e8dc-54ed-9396-7ea50474843a', - trigger_id: '710c8abb-43a9-42a5-9d86-9dd258726c82', - tx_hash: - '0x03381aba290facbaf71c123e263c8dc3dd550aac00ef589cce395182eaeff76f', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationMetaMaskSwapsCompleted() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - block_number: 18377666, - block_timestamp: '1697637275', - chain_id: 1, - created_at: '2023-10-18T13:58:49.854596Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'metamask_swap_completed', - rate: '1558.27', - token_in: { - usd: '1576.73', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '9000000000000000', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - token_out: { - usd: '1.00', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/usdt.svg', - amount: '14024419', - symbol: 'USDT', - address: '0xdac17f958d2ee523a2206206994597c13d831ec7', - decimals: '6', - name: 'USDT', - }, - network_fee: { - gas_price: '15406129273', - native_token_price_in_usd: '1576.73', - }, - }, - id: '7ddfe6a1-ac52-5ffe-aa40-f04242db4b8b', - trigger_id: 'd2eaa2eb-2e6e-4fd5-8763-b70ea571b46c', - tx_hash: - '0xf69074290f3aa11bce567aabc9ca0df7a12559dfae1b80ba1a124e9dfe19ecc5', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationRocketPoolStakeCompleted() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, - block_number: 18585057, - block_timestamp: '1700145059', - chain_id: 1, - created_at: '2023-11-20T12:02:48.796824Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'rocketpool_stake_completed', - stake_in: { - usd: '2031.86', - name: 'Ethereum', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '190690478063438272', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - }, - stake_out: { - usd: '2226.49', - name: 'Rocket Pool ETH', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', - amount: '175024360778165879', - symbol: 'RETH', - address: '0xae78736Cd615f374D3085123A210448E74Fc6393', - decimals: '18', - }, - network_fee: { - gas_price: '36000000000', - native_token_price_in_usd: '2031.86', - }, - }, - id: 'c2a2f225-b2fb-5d6c-ba56-e27a5c71ffb9', - trigger_id: '5110ff97-acff-40c0-83b4-11d487b8c7b0', - tx_hash: - '0xcfc0693bf47995907b0f46ef0644cf16dd9a0de797099b2e00fd481e1b2117d3', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationRocketPoolUnStakeCompleted() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, - block_number: 18384336, - block_timestamp: '1697718011', - chain_id: 1, - created_at: '2023-10-19T13:11:10.623042Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'rocketpool_unstake_completed', - stake_in: { - usd: '1686.34', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg', - amount: '66608041413696770', - symbol: 'RETH', - address: '0xae78736Cd615f374D3085123A210448E74Fc6393', - decimals: '18', - name: 'Rocketpool Eth', - }, - stake_out: { - usd: '1553.75', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '72387843427700824', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '5656322987', - native_token_price_in_usd: '1553.75', - }, - }, - id: 'd8c246e7-a0a4-5f1d-b079-2b1707665fbc', - trigger_id: '291ec897-f569-4837-b6c0-21001b198dff', - tx_hash: - '0xc7972a7e409abfc62590ec90e633acd70b9b74e76ad02305be8bf133a0e22d5f', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationLidoStakeCompleted() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - block_number: 18487118, - block_timestamp: '1698961091', - chain_id: 1, - created_at: '2023-11-02T22:28:49.970865Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_stake_completed', - stake_in: { - usd: '1806.33', - name: 'Ethereum', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '330303634023928032', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - }, - stake_out: { - usd: '1801.30', - name: 'Liquid staked Ether 2.0', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '330303634023928032', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - }, - network_fee: { - gas_price: '26536359866', - native_token_price_in_usd: '1806.33', - }, - }, - id: '9d9b1467-b3ee-5492-8ca2-22382657b690', - trigger_id: 'ec10d66a-f78f-461f-83c9-609aada8cc50', - tx_hash: - '0x8cc0fa805f7c3b1743b14f3b91c6b824113b094f26d4ccaf6a71ad8547ce6a0f', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationLidoWithdrawalRequested() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, - block_number: 18377760, - block_timestamp: '1697638415', - chain_id: 1, - created_at: '2023-10-18T15:04:02.482526Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_withdrawal_requested', - stake_in: { - usd: '1568.54', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '97180668792218669859', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - name: 'Staked Eth', - }, - stake_out: { - usd: '1576.73', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '97180668792218669859', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '11658906980', - native_token_price_in_usd: '1576.73', - }, - }, - id: '29ddc718-78c6-5f91-936f-2bef13a605f0', - trigger_id: 'ef003925-3379-4ba7-9e2d-8218690cadc8', - tx_hash: - '0x58b5f82e084cb750ea174e02b20fbdfd2ba8d78053deac787f34fc38e5d427aa', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationLidoWithdrawalCompleted() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, - block_number: 18378208, - block_timestamp: '1697643851', - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_withdrawal_completed', - stake_in: { - usd: '1570.23', - image: - 'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg', - amount: '35081997661451346', - symbol: 'STETH', - address: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', - decimals: '18', - name: 'Staked Eth', - }, - stake_out: { - usd: '1571.74', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - amount: '35081997661451346', - symbol: 'ETH', - address: '0x0000000000000000000000000000000000000000', - decimals: '18', - name: 'Ethereum', - }, - network_fee: { - gas_price: '12699495150', - native_token_price_in_usd: '1571.74', - }, - }, - id: 'f4ef0b7f-5612-537f-9144-0b5c63ae5391', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042c', - tx_hash: - '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', - unread: true, - }; - - return mockNotification; -} - -export function createMockNotificationLidoReadyToBeWithdrawn() { - const mockNotification: OnChainRawNotification = { - type: TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, - block_number: 18378208, - block_timestamp: '1697643851', - chain_id: 1, - created_at: '2023-10-18T16:35:03.147606Z', - address: '0x881D40237659C251811CEC9c364ef91dC08D300C', - data: { - kind: 'lido_stake_ready_to_be_withdrawn', - request_id: '123456789', - staked_eth: { - address: '0x881D40237659C251811CEC9c364ef91dC08D300F', - symbol: 'ETH', - name: 'Ethereum', - amount: '2.5', - decimals: '18', - image: - 'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg', - usd: '10000.00', - }, - }, - id: 'f4ef0b7f-5612-537f-9144-0b5c63ae5391', - trigger_id: 'd73df14d-ce73-4f38-bad3-ab028154042c', - tx_hash: - '0xe6d210d2e601ef3dd1075c48e71452cf35f2daae3886911e964e3babad8ac657', - unread: true, - }; - - return mockNotification; -} - -export function createMockRawOnChainNotifications(): OnChainRawNotification[] { - return [1, 2, 3].map((id) => { - const notification = createMockNotificationEthSent(); - notification.id += `-${id}`; - return notification; - }); -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts b/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts deleted file mode 100644 index 940646c10640..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mockResponses.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { FEATURE_ANNOUNCEMENT_API } from '../services/feature-announcements'; -import { - NOTIFICATION_API_LIST_ENDPOINT, - NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, - TRIGGER_API_BATCH_ENDPOINT, -} from '../services/onchain-notifications'; -import { createMockFeatureAnnouncementAPIResult } from './mock-feature-announcements'; -import { createMockRawOnChainNotifications } from './mock-raw-notifications'; - -type MockResponse = { - url: string; - requestMethod: 'GET' | 'POST' | 'PUT' | 'DELETE'; - response: unknown; -}; - -export const CONTENTFUL_RESPONSE = createMockFeatureAnnouncementAPIResult(); - -export function getMockFeatureAnnouncementResponse() { - return { - url: FEATURE_ANNOUNCEMENT_API, - requestMethod: 'GET', - response: CONTENTFUL_RESPONSE, - } satisfies MockResponse; -} - -export function getMockBatchCreateTriggersResponse() { - return { - url: TRIGGER_API_BATCH_ENDPOINT, - requestMethod: 'POST', - response: null, - } satisfies MockResponse; -} - -export function getMockBatchDeleteTriggersResponse() { - return { - url: TRIGGER_API_BATCH_ENDPOINT, - requestMethod: 'DELETE', - response: null, - } satisfies MockResponse; -} - -export const MOCK_RAW_ON_CHAIN_NOTIFICATIONS = - createMockRawOnChainNotifications(); - -export function getMockListNotificationsResponse() { - return { - url: NOTIFICATION_API_LIST_ENDPOINT, - requestMethod: 'POST', - response: MOCK_RAW_ON_CHAIN_NOTIFICATIONS, - } satisfies MockResponse; -} - -export function getMockMarkNotificationsAsReadResponse() { - return { - url: NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, - requestMethod: 'POST', - response: null, - } satisfies MockResponse; -} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts b/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts deleted file mode 100644 index 84de50ec8555..000000000000 --- a/app/scripts/controllers/metamask-notifications/mocks/mockServices.ts +++ /dev/null @@ -1,71 +0,0 @@ -import nock from 'nock'; -import { - getMockBatchCreateTriggersResponse, - getMockBatchDeleteTriggersResponse, - getMockFeatureAnnouncementResponse, - getMockListNotificationsResponse, - getMockMarkNotificationsAsReadResponse, -} from './mockResponses'; - -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockFetchFeatureAnnouncementNotifications( - mockReply?: MockReply, -) { - const mockResponse = getMockFeatureAnnouncementResponse(); - const reply = mockReply ?? { status: 200, body: mockResponse.response }; - const mockEndpoint = nock(mockResponse.url) - .get('') - .query(true) - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockBatchCreateTriggers(mockReply?: MockReply) { - const mockResponse = getMockBatchCreateTriggersResponse(); - const reply = mockReply ?? { status: 204 }; - - const mockEndpoint = nock(mockResponse.url) - .post('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockBatchDeleteTriggers(mockReply?: MockReply) { - const mockResponse = getMockBatchDeleteTriggersResponse(); - const reply = mockReply ?? { status: 204 }; - - const mockEndpoint = nock(mockResponse.url) - .delete('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockListNotifications(mockReply?: MockReply) { - const mockResponse = getMockListNotificationsResponse(); - const reply = mockReply ?? { status: 200, body: mockResponse.response }; - - const mockEndpoint = nock(mockResponse.url) - .post('') - .query(true) - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockMarkNotificationsAsRead(mockReply?: MockReply) { - const mockResponse = getMockMarkNotificationsAsReadResponse(); - const reply = mockReply ?? { status: 200 }; - - const mockEndpoint = nock(mockResponse.url) - .post('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} diff --git a/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.test.ts b/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.test.ts deleted file mode 100644 index 8785ad47197a..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { createMockFeatureAnnouncementRaw } from '../mocks/mock-feature-announcements'; -import { - isFeatureAnnouncementRead, - processFeatureAnnouncement, -} from './process-feature-announcement'; - -describe('process-feature-announcement - isFeatureAnnouncementRead()', () => { - const MOCK_NOTIFICATION_ID = 'MOCK_NOTIFICATION_ID'; - - test('Returns true if a given notificationId is within list of read platform notifications', () => { - const notification = { - id: MOCK_NOTIFICATION_ID, - createdAt: new Date().toString(), - }; - - const result1 = isFeatureAnnouncementRead(notification, [ - 'id-1', - 'id-2', - MOCK_NOTIFICATION_ID, - ]); - expect(result1).toBe(true); - - const result2 = isFeatureAnnouncementRead(notification, ['id-1', 'id-2']); - expect(result2).toBe(false); - }); - - test('Returns isRead if notification is older than 90 days', () => { - const mockDate = new Date(); - mockDate.setDate(mockDate.getDate() - 100); - - const notification = { - id: MOCK_NOTIFICATION_ID, - createdAt: mockDate.toString(), - }; - - const result = isFeatureAnnouncementRead(notification, []); - expect(result).toBe(true); - }); -}); - -describe('process-feature-announcement - processFeatureAnnouncement()', () => { - test('Processes a Raw Feature Announcement to a shared Notification Type', () => { - const rawNotification = createMockFeatureAnnouncementRaw(); - const result = processFeatureAnnouncement(rawNotification); - - expect(result.id).toBe(rawNotification.data.id); - expect(result.type).toBe(TRIGGER_TYPES.FEATURES_ANNOUNCEMENT); - expect(result.isRead).toBe(false); - expect(result.data).toBeDefined(); - }); -}); diff --git a/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.ts b/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.ts deleted file mode 100644 index 260cd8b33a60..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; -import type { Notification } from '../types/notification/notification'; - -const ONE_DAY_MS = 1000 * 60 * 60 * 24; - -function shouldAutoExpire(oldDate: Date) { - const differenceInTime = Date.now() - oldDate.getTime(); - const differenceInDays = differenceInTime / ONE_DAY_MS; - return differenceInDays >= 90; -} - -export function isFeatureAnnouncementRead( - notification: Pick, - readPlatformNotificationsList: string[], -): boolean { - if (readPlatformNotificationsList.includes(notification.id)) { - return true; - } - return shouldAutoExpire(new Date(notification.createdAt)); -} - -export function processFeatureAnnouncement( - notification: FeatureAnnouncementRawNotification, -): Notification { - return { - type: notification.type, - id: notification.data.id, - createdAt: new Date(notification.createdAt).toISOString(), - data: notification.data, - isRead: false, - }; -} diff --git a/app/scripts/controllers/metamask-notifications/processors/process-notifications.test.ts b/app/scripts/controllers/metamask-notifications/processors/process-notifications.test.ts deleted file mode 100644 index c1ed0c03b67e..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-notifications.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { createMockFeatureAnnouncementRaw } from '../mocks/mock-feature-announcements'; -import { createMockNotificationEthSent } from '../mocks/mock-raw-notifications'; -import { processNotification } from './process-notifications'; - -describe('process-notifications - processNotification()', () => { - // More thorough tests are found in the specific process - test('Maps Feature Announcement to shared Notification Type', () => { - const result = processNotification(createMockFeatureAnnouncementRaw()); - expect(result).toBeDefined(); - }); - - // More thorough tests are found in the specific process - test('Maps On Chain Notification to shared Notification Type', () => { - const result = processNotification(createMockNotificationEthSent()); - expect(result).toBeDefined(); - }); - - test('Throws on invalid notification to process', () => { - const rawNotification = createMockNotificationEthSent(); - - // Testing Mock with invalid notification type - rawNotification.type = 'FAKE_NOTIFICATION_TYPE' as TRIGGER_TYPES.ETH_SENT; - - expect(() => processNotification(rawNotification)).toThrow(); - }); -}); diff --git a/app/scripts/controllers/metamask-notifications/processors/process-notifications.ts b/app/scripts/controllers/metamask-notifications/processors/process-notifications.ts deleted file mode 100644 index ee09cd232e83..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-notifications.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Notification } from '../types/notification/notification'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; -import type { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import type { NotificationUnion } from '../types/types'; -import { processOnChainNotification } from './process-onchain-notifications'; -import { - isFeatureAnnouncementRead, - processFeatureAnnouncement, -} from './process-feature-announcement'; - -const isOnChainNotification = ( - n: NotificationUnion, -): n is OnChainRawNotification => Object.values(TRIGGER_TYPES).includes(n.type); - -const isFeatureAnnouncement = ( - n: NotificationUnion, -): n is FeatureAnnouncementRawNotification => - n.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT; - -export function processNotification( - notification: NotificationUnion, - readNotifications: string[] = [], -): Notification { - const exhaustedAllCases = (_: never) => { - throw new Error( - `No processor found for notification kind ${notification.type}`, - ); - }; - - if (isFeatureAnnouncement(notification)) { - const n = processFeatureAnnouncement( - notification as FeatureAnnouncementRawNotification, - ); - n.isRead = isFeatureAnnouncementRead(n, readNotifications); - return n; - } - - if (isOnChainNotification(notification)) { - return processOnChainNotification(notification as OnChainRawNotification); - } - - return exhaustedAllCases(notification as never); -} diff --git a/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.test.ts b/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.test.ts deleted file mode 100644 index 8c703c0bedda..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - createMockNotificationEthSent, - createMockNotificationEthReceived, - createMockNotificationERC20Sent, - createMockNotificationERC20Received, - createMockNotificationERC721Sent, - createMockNotificationERC721Received, - createMockNotificationERC1155Sent, - createMockNotificationERC1155Received, - createMockNotificationMetaMaskSwapsCompleted, - createMockNotificationRocketPoolStakeCompleted, - createMockNotificationRocketPoolUnStakeCompleted, - createMockNotificationLidoStakeCompleted, - createMockNotificationLidoWithdrawalRequested, - createMockNotificationLidoWithdrawalCompleted, - createMockNotificationLidoReadyToBeWithdrawn, -} from '../mocks/mock-raw-notifications'; -import { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; -import { processOnChainNotification } from './process-onchain-notifications'; - -const rawNotifications = [ - createMockNotificationEthSent(), - createMockNotificationEthReceived(), - createMockNotificationERC20Sent(), - createMockNotificationERC20Received(), - createMockNotificationERC721Sent(), - createMockNotificationERC721Received(), - createMockNotificationERC1155Sent(), - createMockNotificationERC1155Received(), - createMockNotificationMetaMaskSwapsCompleted(), - createMockNotificationRocketPoolStakeCompleted(), - createMockNotificationRocketPoolUnStakeCompleted(), - createMockNotificationLidoStakeCompleted(), - createMockNotificationLidoWithdrawalRequested(), - createMockNotificationLidoWithdrawalCompleted(), - createMockNotificationLidoReadyToBeWithdrawn(), -]; - -const rawNotificationTestSuite = rawNotifications.map( - (n): [string, OnChainRawNotification] => [n.type, n], -); - -describe('process-onchain-notifications - processOnChainNotification()', () => { - // @ts-expect-error This is missing from the Mocha type definitions - test.each(rawNotificationTestSuite)( - 'Converts Raw On-Chain Notification (%s) to a shared Notification Type', - (_: string, rawNotification: OnChainRawNotification) => { - const result = processOnChainNotification(rawNotification); - expect(result.id).toBe(rawNotification.id); - expect(result.type).toBe(rawNotification.type); - }, - ); -}); diff --git a/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.ts b/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.ts deleted file mode 100644 index 793bfb1b1f6f..000000000000 --- a/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; -import type { Notification } from '../types/notification/notification'; - -export function processOnChainNotification( - notification: OnChainRawNotification, -): Notification { - return { - ...notification, - id: notification.id, - createdAt: new Date(notification.created_at).toISOString(), - isRead: !notification.unread, - }; -} diff --git a/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts b/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts deleted file mode 100644 index d8a178d065c5..000000000000 --- a/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { createMockFeatureAnnouncementAPIResult } from '../mocks/mock-feature-announcements'; -import { mockFetchFeatureAnnouncementNotifications } from '../mocks/mockServices'; -import { getFeatureAnnouncementNotifications } from './feature-announcements'; - -jest.mock('@contentful/rich-text-html-renderer', () => ({ - documentToHtmlString: jest - .fn() - .mockImplementation((richText) => `

${richText}

`), -})); - -describe('Feature Announcement Notifications', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should return an empty array if fetch fails', async () => { - const mockEndpoint = mockFetchFeatureAnnouncementNotifications({ - status: 500, - }); - - const notifications = await getFeatureAnnouncementNotifications(); - mockEndpoint.done(); - expect(notifications).toEqual([]); - }); - - it('should return an empty array if data is not available', async () => { - const mockEndpoint = mockFetchFeatureAnnouncementNotifications({ - status: 200, - body: { items: [] }, - }); - - const notifications = await getFeatureAnnouncementNotifications(); - mockEndpoint.done(); - expect(notifications).toEqual([]); - }); - - it('should fetch entries from Contentful and return formatted notifications', async () => { - const mockEndpoint = mockFetchFeatureAnnouncementNotifications({ - status: 200, - body: createMockFeatureAnnouncementAPIResult(), - }); - - const notifications = await getFeatureAnnouncementNotifications(); - expect(notifications).toHaveLength(1); - mockEndpoint.done(); - - const resultNotification = notifications[0]; - expect(resultNotification).toEqual( - expect.objectContaining({ - id: 'dont-miss-out-on-airdrops-and-new-nft-mints', - type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, - createdAt: expect.any(String), - isRead: expect.any(Boolean), - }), - ); - - expect(resultNotification.data).toBeDefined(); - }); -}); diff --git a/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts b/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts deleted file mode 100644 index 8b3c3870627e..000000000000 --- a/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { documentToHtmlString } from '@contentful/rich-text-html-renderer'; -import log from 'loglevel'; -import type { Entry, Asset } from 'contentful'; -import type { - FeatureAnnouncementRawNotification, - TypeFeatureAnnouncement, -} from '../types/feature-announcement/feature-announcement'; -import type { Notification } from '../types/notification/notification'; -import { processFeatureAnnouncement } from '../processors/process-feature-announcement'; -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { ImageFields } from '../types/feature-announcement/type-feature-announcement'; -import { TypeExtensionLinkFields } from '../types/feature-announcement/type-extension-link'; - -const spaceId = process.env.CONTENTFUL_ACCESS_SPACE_ID || ':space_id'; -const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN || ''; -export const FEATURE_ANNOUNCEMENT_API = `https://cdn.contentful.com/spaces/${spaceId}/environments/master/entries`; -export const FEATURE_ANNOUNCEMENT_URL = `${FEATURE_ANNOUNCEMENT_API}?access_token=${accessToken}&content_type=productAnnouncement&include=10&fields.clients=extension`; - -export type ContentfulResult = { - includes?: { - Entry?: Entry[]; - Asset?: Asset[]; - }; - items?: TypeFeatureAnnouncement[]; -}; - -async function fetchFromContentful( - url: string, - retries = 1, - retryDelay = 1000, -): Promise { - let lastError: Error | null = null; - - for (let i = 0; i < retries; i++) { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Fetch failed with status: ${response.status}`); - } - return await response.json(); - } catch (error) { - if (error instanceof Error) { - lastError = error; - } - if (i < retries - 1) { - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - } - } - - log.error( - `Error fetching from Contentful after ${retries} retries: ${lastError}`, - ); - return null; -} - -async function fetchFeatureAnnouncementNotifications(): Promise< - FeatureAnnouncementRawNotification[] -> { - const data = await fetchFromContentful(FEATURE_ANNOUNCEMENT_URL); - - if (!data) { - return []; - } - - const findIncludedItem = (sysId: string) => { - const item = - data?.includes?.Entry?.find((i: Entry) => i?.sys?.id === sysId) || - data?.includes?.Asset?.find((i: Asset) => i?.sys?.id === sysId); - return item ? item?.fields : null; - }; - - const contentfulNotifications = data?.items ?? []; - const rawNotifications: FeatureAnnouncementRawNotification[] = - contentfulNotifications.map((n: TypeFeatureAnnouncement) => { - const { fields } = n; - const imageFields = fields.image - ? (findIncludedItem(fields.image.sys.id) as ImageFields['fields']) - : undefined; - const extensionLinkFields = fields.extensionLink - ? (findIncludedItem( - fields.extensionLink.sys.id, - ) as TypeExtensionLinkFields['fields']) - : undefined; - - const notification: FeatureAnnouncementRawNotification = { - type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, - createdAt: new Date(n.sys.createdAt).toString(), - data: { - id: fields.id, - category: fields.category, - title: fields.title, - longDescription: documentToHtmlString(fields.longDescription), - shortDescription: fields.shortDescription, - image: { - title: imageFields?.title, - description: imageFields?.description, - url: imageFields?.file?.url ?? '', - }, - extensionLink: extensionLinkFields && { - extensionLinkText: extensionLinkFields?.extensionLinkText, - extensionLinkRoute: extensionLinkFields?.extensionLinkRoute, - }, - }, - }; - - return notification; - }); - - return rawNotifications; -} - -export async function getFeatureAnnouncementNotifications(): Promise< - Notification[] -> { - if ( - process.env.CONTENTFUL_ACCESS_SPACE_ID && - process.env.CONTENTFUL_ACCESS_TOKEN - ) { - const rawNotifications = await fetchFeatureAnnouncementNotifications(); - const notifications = rawNotifications.map((notification) => - processFeatureAnnouncement(notification), - ); - - return notifications; - } - - return []; -} diff --git a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts deleted file mode 100644 index eb205c7e52aa..000000000000 --- a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import { - mockBatchCreateTriggers, - mockBatchDeleteTriggers, - mockListNotifications, - mockMarkNotificationsAsRead, -} from '../mocks/mockServices'; -import { - MOCK_USER_STORAGE_ACCOUNT, - MOCK_USER_STORAGE_CHAIN, - createMockUserStorageWithTriggers, -} from '../mocks/mock-notification-user-storage'; -import { UserStorage } from '../types/user-storage/user-storage'; -import * as MetamaskNotificationsUtils from '../utils/utils'; -import * as OnChainNotifications from './onchain-notifications'; - -const MOCK_STORAGE_KEY = 'MOCK_USER_STORAGE_KEY'; -const MOCK_BEARER_TOKEN = 'MOCK_BEARER_TOKEN'; -const MOCK_TRIGGER_ID = 'TRIGGER_ID_1'; - -describe('On Chain Notifications - createOnChainTriggers()', () => { - test('Should create new triggers', async () => { - const mocks = arrangeMocks(); - - // The initial trigger to create should not be enabled - assertUserStorageTriggerStatus(mocks.mockUserStorage, false); - - await OnChainNotifications.createOnChainTriggers( - mocks.mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - mocks.triggers, - ); - - mocks.mockEndpoint.done(); - - // once we created triggers, we expect the trigger to be enabled - assertUserStorageTriggerStatus(mocks.mockUserStorage, true); - }); - - test('Does not call endpoint if there are no triggers to create', async () => { - const mocks = arrangeMocks(); - await OnChainNotifications.createOnChainTriggers( - mocks.mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - [], // there are no triggers we've provided that need to be created - ); - - expect(mocks.mockEndpoint.isDone()).toBe(false); - }); - - test('Should throw error if endpoint fails', async () => { - const mockUserStorage = createMockUserStorageWithTriggers([ - { id: MOCK_TRIGGER_ID, k: TRIGGER_TYPES.ETH_SENT, e: false }, - ]); - const triggers = - MetamaskNotificationsUtils.traverseUserStorageTriggers(mockUserStorage); - const mockBadEndpoint = mockBatchCreateTriggers({ - status: 500, - body: { error: 'mock api failure' }, - }); - - // The initial trigger to create should not be enabled - assertUserStorageTriggerStatus(mockUserStorage, false); - - await expect( - OnChainNotifications.createOnChainTriggers( - mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - triggers, - ), - ).rejects.toThrow(); - - mockBadEndpoint.done(); - - // since failed, expect triggers to not be enabled - assertUserStorageTriggerStatus(mockUserStorage, false); - }); - - function assertUserStorageTriggerStatus( - userStorage: UserStorage, - enabled: boolean, - ) { - expect( - userStorage[MOCK_USER_STORAGE_ACCOUNT][MOCK_USER_STORAGE_CHAIN][ - MOCK_TRIGGER_ID - ].e, - ).toBe(enabled); - } - - function arrangeMocks() { - const mockUserStorage = createMockUserStorageWithTriggers([ - { id: MOCK_TRIGGER_ID, k: TRIGGER_TYPES.ETH_SENT, e: false }, - ]); - const triggers = - MetamaskNotificationsUtils.traverseUserStorageTriggers(mockUserStorage); - const mockEndpoint = mockBatchCreateTriggers(); - - return { - mockUserStorage, - triggers, - mockEndpoint, - }; - } -}); - -describe('On Chain Notifications - deleteOnChainTriggers()', () => { - test('Should delete a trigger from API and in user storage', async () => { - const { mockUserStorage, triggerId1, triggerId2 } = arrangeUserStorage(); - const mockEndpoint = mockBatchDeleteTriggers(); - - // Assert that triggers exists - [triggerId1, triggerId2].forEach((t) => { - expect(getTriggerFromUserStorage(mockUserStorage, t)).toBeDefined(); - }); - - await OnChainNotifications.deleteOnChainTriggers( - mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - [triggerId2], - ); - - mockEndpoint.done(); - - // Assert trigger deletion - expect( - getTriggerFromUserStorage(mockUserStorage, triggerId1), - ).toBeDefined(); - expect( - getTriggerFromUserStorage(mockUserStorage, triggerId2), - ).toBeUndefined(); - }); - - test('Should delete all triggers and account in user storage', async () => { - const { mockUserStorage, triggerId1, triggerId2 } = arrangeUserStorage(); - const mockEndpoint = mockBatchDeleteTriggers(); - - await OnChainNotifications.deleteOnChainTriggers( - mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - [triggerId1, triggerId2], // delete all triggers for an account - ); - - mockEndpoint.done(); - - // assert that the underlying user is also deleted since all underlying triggers are deleted - expect(mockUserStorage[MOCK_USER_STORAGE_ACCOUNT]).toBeUndefined(); - }); - - test('Should throw error if endpoint fails to delete', async () => { - const { mockUserStorage, triggerId1, triggerId2 } = arrangeUserStorage(); - const mockBadEndpoint = mockBatchDeleteTriggers({ - status: 500, - body: { error: 'mock api failure' }, - }); - - await expect( - OnChainNotifications.deleteOnChainTriggers( - mockUserStorage, - MOCK_STORAGE_KEY, - MOCK_BEARER_TOKEN, - [triggerId1, triggerId2], - ), - ).rejects.toThrow(); - - mockBadEndpoint.done(); - - // Assert that triggers were not deleted from user storage - [triggerId1, triggerId2].forEach((t) => { - expect(getTriggerFromUserStorage(mockUserStorage, t)).toBeDefined(); - }); - }); - - function getTriggerFromUserStorage( - userStorage: UserStorage, - triggerId: string, - ) { - return userStorage[MOCK_USER_STORAGE_ACCOUNT][MOCK_USER_STORAGE_CHAIN][ - triggerId - ]; - } - - function arrangeUserStorage() { - const triggerId1 = 'TRIGGER_ID_1'; - const triggerId2 = 'TRIGGER_ID_2'; - const mockUserStorage = createMockUserStorageWithTriggers([ - triggerId1, - triggerId2, - ]); - - return { - mockUserStorage, - triggerId1, - triggerId2, - }; - } -}); - -describe('On Chain Notifications - getOnChainNotifications()', () => { - test('Should return a list of notifications', async () => { - const mockEndpoint = mockListNotifications(); - const mockUserStorage = createMockUserStorageWithTriggers([ - 'trigger_1', - 'trigger_2', - ]); - - const result = await OnChainNotifications.getOnChainNotifications( - mockUserStorage, - MOCK_BEARER_TOKEN, - ); - - mockEndpoint.done(); - expect(result.length > 0).toBe(true); - }); - - test('Should return an empty list if not triggers found in user storage', async () => { - const mockEndpoint = mockListNotifications(); - const mockUserStorage = createMockUserStorageWithTriggers([]); // no triggers - - const result = await OnChainNotifications.getOnChainNotifications( - mockUserStorage, - MOCK_BEARER_TOKEN, - ); - - expect(mockEndpoint.isDone()).toBe(false); - expect(result.length === 0).toBe(true); - }); - - test('Should return an empty list of notifications if endpoint fails to fetch triggers', async () => { - const mockEndpoint = mockListNotifications({ - status: 500, - body: { error: 'mock api failure' }, - }); - const mockUserStorage = createMockUserStorageWithTriggers([ - 'trigger_1', - 'trigger_2', - ]); - - const result = await OnChainNotifications.getOnChainNotifications( - mockUserStorage, - MOCK_BEARER_TOKEN, - ); - - mockEndpoint.done(); - expect(result.length === 0).toBe(true); - }); -}); - -describe('On Chain Notifications - markNotificationsAsRead()', () => { - test('Should successfully call endpoint to mark notifications as read', async () => { - const mockEndpoint = mockMarkNotificationsAsRead(); - await OnChainNotifications.markNotificationsAsRead(MOCK_BEARER_TOKEN, [ - 'notification_1', - 'notification_2', - ]); - - mockEndpoint.done(); - }); - - test('Should throw error if fails to call endpoint to mark notifications as read', async () => { - const mockBadEndpoint = mockMarkNotificationsAsRead({ - status: 500, - body: { error: 'mock api failure' }, - }); - await expect( - OnChainNotifications.markNotificationsAsRead(MOCK_BEARER_TOKEN, [ - 'notification_1', - 'notification_2', - ]), - ).rejects.toThrow(); - - mockBadEndpoint.done(); - }); -}); diff --git a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.ts b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.ts deleted file mode 100644 index a43e678ac798..000000000000 --- a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.ts +++ /dev/null @@ -1,288 +0,0 @@ -import log from 'loglevel'; -import type { UserStorage } from '../types/user-storage/user-storage'; -import type { OnChainRawNotification } from '../types/on-chain-notification/on-chain-notification'; -import { - traverseUserStorageTriggers, - toggleUserStorageTriggerStatus, - makeApiCall, -} from '../utils/utils'; -import { TRIGGER_TYPES } from '../constants/notification-schema'; -import type { components } from '../types/on-chain-notification/schema'; -import { createSHA256Hash } from '../../user-storage/encryption'; - -export type NotificationTrigger = { - id: string; - chainId: string; - kind: string; - address: string; -}; - -export const TRIGGER_API = process.env.TRIGGERS_SERVICE_URL; -export const NOTIFICATION_API = process.env.NOTIFICATIONS_SERVICE_URL; -export const TRIGGER_API_BATCH_ENDPOINT = `${TRIGGER_API}/api/v1/triggers/batch`; -export const NOTIFICATION_API_LIST_ENDPOINT = `${NOTIFICATION_API}/api/v1/notifications`; -export const NOTIFICATION_API_LIST_ENDPOINT_PAGE_QUERY = (page: number) => - `${NOTIFICATION_API_LIST_ENDPOINT}?page=${page}&per_page=100`; -export const NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT = `${NOTIFICATION_API}/api/v1/notifications/mark-as-read`; - -/** - * Creates on-chain triggers based on the provided notification triggers. - * This method generates a unique token for each trigger using the trigger ID and storage key, - * proving ownership of the trigger being updated. It then makes an API call to create these triggers. - * Upon successful creation, it updates the userStorage to reflect the new trigger status. - * - * @param userStorage - The user's storage object where triggers and their statuses are stored. - * @param storageKey - A key used along with the trigger ID to generate a unique token for each trigger. - * @param bearerToken - The JSON Web Token used for authentication in the API call. - * @param triggers - An array of notification triggers to be created. Each trigger includes an ID, chain ID, kind, and address. - * @returns A promise that resolves to void. Throws an error if the API call fails or if there's an issue creating the triggers. - */ -export async function createOnChainTriggers( - userStorage: UserStorage, - storageKey: string, - bearerToken: string, - triggers: NotificationTrigger[], -): Promise { - type RequestPayloadTrigger = { - id: string; - // this is the trigger token, generated by using the uuid + storage key. It proves you own the trigger you are updating - token: string; - config: { - kind: string; - chain_id: number; - address: string; - }; - }; - const triggersToCreate: RequestPayloadTrigger[] = triggers.map((t) => ({ - id: t.id, - token: createSHA256Hash(t.id + storageKey), - config: { - kind: t.kind, - chain_id: Number(t.chainId), - address: t.address, - }, - })); - - if (triggersToCreate.length === 0) { - return; - } - - const response = await makeApiCall( - bearerToken, - TRIGGER_API_BATCH_ENDPOINT, - 'POST', - triggersToCreate, - ); - - if (!response.ok) { - const errorData = await response.json().catch(() => undefined); - log.error('Error creating triggers:', errorData); - throw new Error('OnChain Notifications - unable to create triggers'); - } - - // If the trigger creation was fine - // then update the userStorage - for (const trigger of triggersToCreate) { - toggleUserStorageTriggerStatus( - userStorage, - trigger.config.address, - String(trigger.config.chain_id), - trigger.id, - true, - ); - } -} - -/** - * Deletes on-chain triggers based on the provided UUIDs. - * This method generates a unique token for each trigger using the UUID and storage key, - * proving ownership of the trigger being deleted. It then makes an API call to delete these triggers. - * Upon successful deletion, it updates the userStorage to remove the deleted trigger statuses. - * - * @param userStorage - The user's storage object where triggers and their statuses are stored. - * @param storageKey - A key used along with the UUID to generate a unique token for each trigger. - * @param bearerToken - The JSON Web Token used for authentication in the API call. - * @param uuids - An array of UUIDs representing the triggers to be deleted. - * @returns A promise that resolves to the updated UserStorage object. Throws an error if the API call fails or if there's an issue deleting the triggers. - */ -export async function deleteOnChainTriggers( - userStorage: UserStorage, - storageKey: string, - bearerToken: string, - uuids: string[], -): Promise { - const triggersToDelete = uuids.map((uuid) => ({ - id: uuid, - token: createSHA256Hash(uuid + storageKey), - })); - - try { - const response = await makeApiCall( - bearerToken, - TRIGGER_API_BATCH_ENDPOINT, - 'DELETE', - triggersToDelete, - ); - - if (!response.ok) { - throw new Error( - `Failed to delete on-chain notifications for uuids ${uuids.join(', ')}`, - ); - } - - // Update the state of the deleted trigger to false - for (const uuid of uuids) { - for (const address in userStorage) { - if (Object.hasOwn(userStorage, address)) { - for (const chainId in userStorage[address]) { - if (userStorage?.[address]?.[chainId]?.[uuid]) { - delete userStorage[address][chainId][uuid]; - } - } - } - } - } - - // Follow-up cleanup, if an address had no triggers whatsoever, then we can delete the address - const isEmpty = (obj = {}) => Object.keys(obj).length === 0; - for (const address in userStorage) { - if (Object.hasOwn(userStorage, address)) { - for (const chainId in userStorage[address]) { - // Chain isEmpty Check - if (isEmpty(userStorage?.[address]?.[chainId])) { - delete userStorage[address][chainId]; - } - } - - // Address isEmpty Check - if (isEmpty(userStorage?.[address])) { - delete userStorage[address]; - } - } - } - } catch (err) { - log.error( - `Error deleting on-chain notifications for uuids ${uuids.join(', ')}:`, - err, - ); - throw err; - } - - return userStorage; -} - -/** - * Fetches on-chain notifications for the given user storage and BearerToken. - * This method iterates through the userStorage to find enabled triggers and fetches notifications for those triggers. - * It makes paginated API calls to the notifications service, transforming and aggregating the notifications into a single array. - * The process stops either when all pages have been fetched or when a page has less than 100 notifications, indicating the end of the data. - * - * @param userStorage - The user's storage object containing trigger information. - * @param bearerToken - The JSON Web Token used for authentication in the API call. - * @returns A promise that resolves to an array of OnChainRawNotification objects. If no triggers are enabled or an error occurs, it may return an empty array. - */ -export async function getOnChainNotifications( - userStorage: UserStorage, - bearerToken: string, -): Promise { - const triggerIds = traverseUserStorageTriggers(userStorage, { - mapTrigger: (t) => { - if (!t.enabled) { - return undefined; - } - return t.id; - }, - }); - - if (triggerIds.length === 0) { - return []; - } - - const onChainNotifications: OnChainRawNotification[] = []; - const PAGE_LIMIT = 2; - for (let page = 1; page <= PAGE_LIMIT; page++) { - try { - const response = await makeApiCall( - bearerToken, - NOTIFICATION_API_LIST_ENDPOINT_PAGE_QUERY(page), - 'POST', - { trigger_ids: triggerIds }, - ); - - const notifications = (await response.json()) as OnChainRawNotification[]; - - // Transform and sort notifications - const transformedNotifications = notifications - .map( - ( - n: components['schemas']['Notification'], - ): OnChainRawNotification | undefined => { - if (!n.data?.kind) { - return undefined; - } - - return { - ...n, - type: n.data.kind as TRIGGER_TYPES, - } as OnChainRawNotification; - }, - ) - .filter((n): n is OnChainRawNotification => Boolean(n)); - - onChainNotifications.push(...transformedNotifications); - - // if less than 100 notifications on page, then means we reached end - if (notifications.length < 100) { - page = PAGE_LIMIT + 1; - break; - } - } catch (err) { - log.error( - `Error fetching on-chain notifications for trigger IDs ${triggerIds.join( - ', ', - )}:`, - err, - ); - // do nothing - } - } - - return onChainNotifications; -} - -/** - * Marks the specified notifications as read. - * This method sends a POST request to the notifications service to mark the provided notification IDs as read. - * If the operation is successful, it completes without error. If the operation fails, it throws an error with details. - * - * @param bearerToken - The JSON Web Token used for authentication in the API call. - * @param notificationIds - An array of notification IDs to be marked as read. - * @returns A promise that resolves to void. The promise will reject if there's an error during the API call or if the response status is not 200. - */ -export async function markNotificationsAsRead( - bearerToken: string, - notificationIds: string[], -): Promise { - if (notificationIds.length === 0) { - return; - } - - try { - const response = await makeApiCall( - bearerToken, - NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, - 'POST', - { ids: notificationIds }, - ); - - if (response.status !== 200) { - const errorData = await response.json().catch(() => undefined); - throw new Error( - `Error marking notifications as read: ${errorData?.message}`, - ); - } - } catch (err) { - log.error('Error marking notifications as read:', err); - throw err; - } -} diff --git a/app/scripts/controllers/metamask-notifications/types/feature-announcement/feature-announcement.ts b/app/scripts/controllers/metamask-notifications/types/feature-announcement/feature-announcement.ts deleted file mode 100644 index 6155c9dcd156..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/feature-announcement/feature-announcement.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { TRIGGER_TYPES } from '../../constants/notification-schema'; -import type { TypeFeatureAnnouncement } from './type-feature-announcement'; - -export type { TypeFeatureAnnouncement }; -export type { TypeFeatureAnnouncementFields } from './type-feature-announcement'; - -export type FeatureAnnouncementRawNotificationData = Omit< - TypeFeatureAnnouncement['fields'], - 'image' | 'longDescription' | 'extensionLink' -> & { - longDescription: string; - image: { - title?: string; - description?: string; - url: string; - }; - extensionLink?: { - extensionLinkText: string; - extensionLinkRoute: string; - }; -}; - -export type FeatureAnnouncementRawNotification = { - type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT; - createdAt: string; - data: FeatureAnnouncementRawNotificationData; -}; diff --git a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-extension-link.ts b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-extension-link.ts deleted file mode 100644 index bfc3d40267ae..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-extension-link.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Entry } from 'contentful'; - -export type TypeExtensionLinkFields = { - fields: { - extensionLinkText: string; - extensionLinkRoute: string; - }; - contentTypeId: 'extensionLink'; -}; - -export type TypeExtensionLink = Entry< - TypeExtensionLinkFields, - 'WITHOUT_UNRESOLVABLE_LINKS' ->; diff --git a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-feature-announcement.ts b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-feature-announcement.ts deleted file mode 100644 index 8cff6cbd7d9d..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-feature-announcement.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Entry, EntryFieldTypes } from 'contentful'; -import type { TypeExtensionLinkFields } from './type-extension-link'; - -export type ImageFields = { - fields: { - title?: string; - description?: string; - file?: { - url: string; - fileName: string; - contentType: string; - details: { - size: number; - image?: { - width: number; - height: number; - }; - }; - }; - }; - contentTypeId: 'Image'; -}; - -export type TypeFeatureAnnouncementFields = { - fields: { - title: EntryFieldTypes.Text; - id: EntryFieldTypes.Symbol; - category: EntryFieldTypes.Text; // E.g. Announcement, etc. - shortDescription: EntryFieldTypes.Text; - image: EntryFieldTypes.EntryLink; - longDescription: EntryFieldTypes.RichText; - extensionLink?: EntryFieldTypes.EntryLink; - clients?: EntryFieldTypes.Text<'extension' | 'mobile' | 'portfolio'>; - }; - contentTypeId: 'productAnnouncement'; -}; - -export type TypeFeatureAnnouncement = Entry< - TypeFeatureAnnouncementFields, - 'WITHOUT_UNRESOLVABLE_LINKS' ->; diff --git a/app/scripts/controllers/metamask-notifications/types/notification/notification.ts b/app/scripts/controllers/metamask-notifications/types/notification/notification.ts deleted file mode 100644 index 3c556ab30191..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/notification/notification.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Notification } from '../types'; - -export type { Notification } from '../types'; - -// NFT -export type NFT = { - token_id: string; - image: string; - collection?: { - name: string; - image: string; - }; -}; - -export type MarkAsReadNotificationsParam = Pick< - Notification, - 'id' | 'type' | 'isRead' ->[]; diff --git a/app/scripts/controllers/metamask-notifications/types/on-chain-notification/on-chain-notification.ts b/app/scripts/controllers/metamask-notifications/types/on-chain-notification/on-chain-notification.ts deleted file mode 100644 index 022702933bea..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/on-chain-notification/on-chain-notification.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { TRIGGER_TYPES } from '../../constants/notification-schema'; -import type { Compute } from '../type-utils'; -import type { components } from './schema'; - -export type Data_MetamaskSwapCompleted = - components['schemas']['Data_MetamaskSwapCompleted']; -export type Data_LidoStakeReadyToBeWithdrawn = - components['schemas']['Data_LidoStakeReadyToBeWithdrawn']; -export type Data_LidoStakeCompleted = - components['schemas']['Data_LidoStakeCompleted']; -export type Data_LidoWithdrawalRequested = - components['schemas']['Data_LidoWithdrawalRequested']; -export type Data_LidoWithdrawalCompleted = - components['schemas']['Data_LidoWithdrawalCompleted']; -export type Data_RocketPoolStakeCompleted = - components['schemas']['Data_RocketPoolStakeCompleted']; -export type Data_RocketPoolUnstakeCompleted = - components['schemas']['Data_RocketPoolUnstakeCompleted']; -export type Data_ETHSent = components['schemas']['Data_ETHSent']; -export type Data_ETHReceived = components['schemas']['Data_ETHReceived']; -export type Data_ERC20Sent = components['schemas']['Data_ERC20Sent']; -export type Data_ERC20Received = components['schemas']['Data_ERC20Received']; -export type Data_ERC721Sent = components['schemas']['Data_ERC721Sent']; -export type Data_ERC721Received = components['schemas']['Data_ERC721Received']; - -type Notification = components['schemas']['Notification']; -type NotificationDataKinds = NonNullable['kind']; -type ConvertToEnum = { - [K in TRIGGER_TYPES]: Kind extends `${K}` ? K : never; -}[TRIGGER_TYPES]; - -/** - * Type-Computation. - * 1. Adds a `type` field to the notification, it converts the schema type into the ENUM we use. - * 2. It ensures that the `data` field is the correct Notification data for this `type` - * - The `Compute` utility merges the intersections (`&`) for a prettier type. - */ -export type OnChainRawNotification = { - [K in NotificationDataKinds]: Compute< - Omit & { - type: ConvertToEnum; - data: Extract; - } - >; -}[NotificationDataKinds]; - -export type OnChainRawNotificationsWithNetworkFields = Extract< - OnChainRawNotification, - { data: { network_fee: unknown } } ->; diff --git a/app/scripts/controllers/metamask-notifications/types/on-chain-notification/schema.d.ts b/app/scripts/controllers/metamask-notifications/types/on-chain-notification/schema.d.ts deleted file mode 100644 index 05c725e3690d..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/on-chain-notification/schema.d.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - * Script: `npx openapi-typescript -o ./schema.d.ts` - */ - -export type paths = { - '/api/v1/notifications': { - /** List all notifications ordered by most recent */ - post: { - parameters: { - query?: { - /** @description Page number for pagination */ - page?: number; - /** @description Number of notifications per page for pagination */ - per_page?: number; - }; - }; - requestBody?: { - content: { - 'application/json': { - trigger_ids: string[]; - chain_ids?: number[]; - kinds?: string[]; - unread?: boolean; - }; - }; - }; - responses: { - /** @description Successfully fetched a list of notifications */ - 200: { - content: { - 'application/json': components['schemas']['Notification'][]; - }; - }; - }; - }; - }; - '/api/v1/notifications/mark-as-read': { - /** Mark notifications as read */ - post: { - requestBody: { - content: { - 'application/json': { - ids?: string[]; - }; - }; - }; - responses: { - /** @description Successfully marked notifications as read */ - 200: { - content: never; - }; - }; - }; - }; -}; - -export type webhooks = Record; - -export type components = { - schemas: { - Notification: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - trigger_id: string; - /** @example 1 */ - chain_id: number; - /** @example 17485840 */ - block_number: number; - block_timestamp: string; - /** - * Format: address - * - * @example 0x881D40237659C251811CEC9c364ef91dC08D300C - */ - tx_hash: string; - /** @example false */ - unread: boolean; - /** Format: date-time */ - created_at: string; - /** Format: address */ - address: string; - data?: - | components['schemas']['Data_MetamaskSwapCompleted'] - | components['schemas']['Data_LidoStakeReadyToBeWithdrawn'] - | components['schemas']['Data_LidoStakeCompleted'] - | components['schemas']['Data_LidoWithdrawalRequested'] - | components['schemas']['Data_LidoWithdrawalCompleted'] - | components['schemas']['Data_RocketPoolStakeCompleted'] - | components['schemas']['Data_RocketPoolUnstakeCompleted'] - | components['schemas']['Data_ETHSent'] - | components['schemas']['Data_ETHReceived'] - | components['schemas']['Data_ERC20Sent'] - | components['schemas']['Data_ERC20Received'] - | components['schemas']['Data_ERC721Sent'] - | components['schemas']['Data_ERC721Received'] - | components['schemas']['Data_ERC1155Sent'] - | components['schemas']['Data_ERC1155Received']; - }; - Data_MetamaskSwapCompleted: { - /** @enum {string} */ - kind: 'metamask_swap_completed'; - network_fee: components['schemas']['NetworkFee']; - /** Format: decimal */ - rate: string; - token_in: components['schemas']['Token']; - token_out: components['schemas']['Token']; - }; - Data_LidoStakeCompleted: { - /** @enum {string} */ - kind: 'lido_stake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_LidoWithdrawalRequested: { - /** @enum {string} */ - kind: 'lido_withdrawal_requested'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_LidoStakeReadyToBeWithdrawn: { - /** @enum {string} */ - kind: 'lido_stake_ready_to_be_withdrawn'; - /** Format: decimal */ - request_id: string; - staked_eth: components['schemas']['Stake']; - }; - Data_LidoWithdrawalCompleted: { - /** @enum {string} */ - kind: 'lido_withdrawal_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_RocketPoolStakeCompleted: { - /** @enum {string} */ - kind: 'rocketpool_stake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_RocketPoolUnstakeCompleted: { - /** @enum {string} */ - kind: 'rocketpool_unstake_completed'; - network_fee: components['schemas']['NetworkFee']; - stake_in: components['schemas']['Stake']; - stake_out: components['schemas']['Stake']; - }; - Data_ETHSent: { - /** @enum {string} */ - kind: 'eth_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - amount: { - /** Format: decimal */ - usd: string; - /** Format: decimal */ - eth: string; - }; - }; - Data_ETHReceived: { - /** @enum {string} */ - kind: 'eth_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - amount: { - /** Format: decimal */ - usd: string; - /** Format: decimal */ - eth: string; - }; - }; - Data_ERC20Sent: { - /** @enum {string} */ - kind: 'erc20_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - token: components['schemas']['Token']; - }; - Data_ERC20Received: { - /** @enum {string} */ - kind: 'erc20_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - token: components['schemas']['Token']; - }; - Data_ERC721Sent: { - /** @enum {string} */ - kind: 'erc721_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft: components['schemas']['NFT']; - }; - Data_ERC721Received: { - /** @enum {string} */ - kind: 'erc721_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft: components['schemas']['NFT']; - }; - Data_ERC1155Sent: { - /** @enum {string} */ - kind: 'erc1155_sent'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft?: components['schemas']['NFT']; - }; - Data_ERC1155Received: { - /** @enum {string} */ - kind: 'erc1155_received'; - network_fee: components['schemas']['NetworkFee']; - /** Format: address */ - from: string; - /** Format: address */ - to: string; - nft?: components['schemas']['NFT']; - }; - NetworkFee: { - /** Format: decimal */ - gas_price: string; - /** Format: decimal */ - native_token_price_in_usd: string; - }; - Token: { - /** Format: address */ - address: string; - symbol: string; - name: string; - /** Format: decimal */ - amount: string; - /** Format: int32 */ - decimals: string; - /** Format: uri */ - image: string; - /** Format: decimal */ - usd: string; - }; - NFT: { - name: string; - token_id: string; - /** Format: uri */ - image: string; - collection: { - /** Format: address */ - address: string; - name: string; - symbol: string; - /** Format: uri */ - image: string; - }; - }; - Stake: { - /** Format: address */ - address: string; - symbol: string; - name: string; - /** Format: decimal */ - amount: string; - /** Format: int32 */ - decimals: string; - /** Format: uri */ - image: string; - /** Format: decimal */ - usd: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -}; - -export type $defs = Record; - -export type external = Record; - -export type operations = Record; diff --git a/app/scripts/controllers/metamask-notifications/types/type-utils.ts b/app/scripts/controllers/metamask-notifications/types/type-utils.ts deleted file mode 100644 index f05a763f5d2c..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/type-utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Computes and combines intersection types for a more "prettier" type (more human readable) - */ -export type Compute = T extends T ? { [K in keyof T]: T[K] } : never; diff --git a/app/scripts/controllers/metamask-notifications/types/types.ts b/app/scripts/controllers/metamask-notifications/types/types.ts deleted file mode 100644 index 1d0d7026a430..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { FeatureAnnouncementRawNotification } from './feature-announcement/feature-announcement'; -import type { Compute } from './type-utils'; -import type { OnChainRawNotification } from './on-chain-notification/on-chain-notification'; - -export type NotificationUnion = - | FeatureAnnouncementRawNotification - | OnChainRawNotification; - -/** - * The shape of a "generic" notification. - * Other than the fields listed below, tt will also contain: - * - `type` field (declared in the Raw shapes) - * - `data` field (declared in the Raw shapes) - */ -export type Notification = Compute< - NotificationUnion & { - id: string; - createdAt: string; - isRead: boolean; - } ->; diff --git a/app/scripts/controllers/metamask-notifications/types/user-storage/user-storage.ts b/app/scripts/controllers/metamask-notifications/types/user-storage/user-storage.ts deleted file mode 100644 index 086d4cc7e4d4..000000000000 --- a/app/scripts/controllers/metamask-notifications/types/user-storage/user-storage.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { - SUPPORTED_CHAINS, - TRIGGER_TYPES, -} from '../../constants/notification-schema'; -import type { - USER_STORAGE_VERSION_KEY, - USER_STORAGE_VERSION, -} from '../../constants/constants'; - -export type UserStorage = { - /** - * The Version 'v' of the User Storage. - * NOTE - will allow us to support upgrade/downgrades in the future - */ - [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION; - [address: string]: { - [chain in (typeof SUPPORTED_CHAINS)[number]]: { - [uuid: string]: { - /** Trigger Kind 'k' */ - k: TRIGGER_TYPES; - /** - * Trigger Enabled 'e' - * This is mostly an 'acknowledgement' to determine if a trigger has been made - * For example if we fail to create a trigger, we can set to false & retry (on re-log in, or elsewhere) - * - * Most of the time this is 'true', as triggers when deleted are also removed from User Storage - */ - e: boolean; - }; - }; - }; -}; diff --git a/app/scripts/controllers/metamask-notifications/utils/utils.test.ts b/app/scripts/controllers/metamask-notifications/utils/utils.test.ts deleted file mode 100644 index b533f6c4559b..000000000000 --- a/app/scripts/controllers/metamask-notifications/utils/utils.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { USER_STORAGE_VERSION_KEY } from '../constants/constants'; -import { - NOTIFICATION_CHAINS, - TRIGGER_TYPES, -} from '../constants/notification-schema'; -import { - MOCK_USER_STORAGE_ACCOUNT, - MOCK_USER_STORAGE_CHAIN, - createMockFullUserStorage, - createMockUserStorageWithTriggers, -} from '../mocks/mock-notification-user-storage'; -import { UserStorage } from '../types/user-storage/user-storage'; -import * as MetamaskNotificationsUtils from './utils'; - -describe('metamask-notifications/utils - initializeUserStorage()', () => { - test('Creates a new user storage object based on the accounts provided', () => { - const mockAddress = 'MOCK_ADDRESS'; - const userStorage = MetamaskNotificationsUtils.initializeUserStorage( - [{ address: mockAddress }], - true, - ); - - // Addresses in User Storage are lowercase to prevent multiple entries of same address - const userStorageAddress = mockAddress.toLowerCase(); - expect(userStorage[userStorageAddress]).toBeDefined(); - }); - - test('Returns User Storage with no addresses if none provided', () => { - function assertEmptyStorage(storage: UserStorage) { - expect(Object.keys(storage).length === 1).toBe(true); - expect(USER_STORAGE_VERSION_KEY in storage).toBe(true); - } - - const userStorageTest1 = MetamaskNotificationsUtils.initializeUserStorage( - [], - true, - ); - assertEmptyStorage(userStorageTest1); - - const userStorageTest2 = MetamaskNotificationsUtils.initializeUserStorage( - [{ address: undefined }], - true, - ); - assertEmptyStorage(userStorageTest2); - }); -}); - -describe('metamask-notifications/utils - traverseUserStorageTriggers()', () => { - test('Traverses User Storage to return triggers', () => { - const storage = createMockFullUserStorage(); - const triggersObjArray = - MetamaskNotificationsUtils.traverseUserStorageTriggers(storage); - expect(triggersObjArray.length > 0).toBe(true); - expect(typeof triggersObjArray[0] === 'object').toBe(true); - }); - - test('Traverses and maps User Storage using mapper', () => { - const storage = createMockFullUserStorage(); - - // as the type suggests, the mapper returns a string, so expect this to be a string - const triggersStrArray = - MetamaskNotificationsUtils.traverseUserStorageTriggers(storage, { - mapTrigger: (t) => t.id, - }); - expect(triggersStrArray.length > 0).toBe(true); - expect(typeof triggersStrArray[0] === 'string').toBe(true); - - // if the mapper returns a falsy value, it is filtered out - const emptyTriggersArray = - MetamaskNotificationsUtils.traverseUserStorageTriggers(storage, { - mapTrigger: (_t): string | undefined => undefined, - }); - expect(emptyTriggersArray.length === 0).toBe(true); - }); -}); - -describe('metamask-notifications/utils - checkAccountsPresence()', () => { - test('Returns record of addresses that are in storage', () => { - const storage = createMockFullUserStorage(); - const result = MetamaskNotificationsUtils.checkAccountsPresence(storage, [ - MOCK_USER_STORAGE_ACCOUNT, - ]); - expect(result).toEqual({ - [MOCK_USER_STORAGE_ACCOUNT.toLowerCase()]: true, - }); - }); - - test('Returns record of addresses in storage and not fully in storage', () => { - const storage = createMockFullUserStorage(); - const MOCK_MISSING_ADDRESS = '0x2'; - const result = MetamaskNotificationsUtils.checkAccountsPresence(storage, [ - MOCK_USER_STORAGE_ACCOUNT, - MOCK_MISSING_ADDRESS, - ]); - expect(result).toEqual({ - [MOCK_USER_STORAGE_ACCOUNT.toLowerCase()]: true, - [MOCK_MISSING_ADDRESS.toLowerCase()]: false, - }); - }); - - test('Returns record where accounts are not fully present, due to missing chains', () => { - const storage = createMockFullUserStorage(); - delete storage[MOCK_USER_STORAGE_ACCOUNT][NOTIFICATION_CHAINS.ETHEREUM]; - - const result = MetamaskNotificationsUtils.checkAccountsPresence(storage, [ - MOCK_USER_STORAGE_ACCOUNT, - ]); - expect(result).toEqual({ - [MOCK_USER_STORAGE_ACCOUNT.toLowerCase()]: false, // false due to missing chains - }); - }); - - test('Returns record where accounts are not fully present, due to missing triggers', () => { - const storage = createMockFullUserStorage(); - const MOCK_TRIGGER_TO_DELETE = Object.keys( - storage[MOCK_USER_STORAGE_ACCOUNT][NOTIFICATION_CHAINS.ETHEREUM], - )[0]; - delete storage[MOCK_USER_STORAGE_ACCOUNT][NOTIFICATION_CHAINS.ETHEREUM][ - MOCK_TRIGGER_TO_DELETE - ]; - - const result = MetamaskNotificationsUtils.checkAccountsPresence(storage, [ - MOCK_USER_STORAGE_ACCOUNT, - ]); - expect(result).toEqual({ - [MOCK_USER_STORAGE_ACCOUNT.toLowerCase()]: false, // false due to missing triggers - }); - }); -}); - -describe('metamask-notifications/utils - inferEnabledKinds()', () => { - test('Returns all kinds from a User Storage Obj', () => { - const partialStorage = createMockUserStorageWithTriggers([ - { id: '1', e: true, k: TRIGGER_TYPES.ERC1155_RECEIVED }, - { id: '2', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, - { id: '3', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, // should remove duplicates - ]); - - const result = MetamaskNotificationsUtils.inferEnabledKinds(partialStorage); - expect(result.length).toBe(2); - expect(result.includes(TRIGGER_TYPES.ERC1155_RECEIVED)).toBe(true); - expect(result.includes(TRIGGER_TYPES.ERC1155_SENT)).toBe(true); - }); -}); - -describe('metamask-notifications/utils - getUUIDsForAccount()', () => { - test('Returns all trigger IDs in user storage from a given address', () => { - const partialStorage = createMockUserStorageWithTriggers(['t1', 't2']); - - const result = MetamaskNotificationsUtils.getUUIDsForAccount( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - ); - expect(result.length).toBe(2); - expect(result.includes('t1')).toBe(true); - expect(result.includes('t2')).toBe(true); - }); - test('Returns an empty array if the address does not exist or has any triggers', () => { - const partialStorage = createMockUserStorageWithTriggers(['t1', 't2']); - const result = MetamaskNotificationsUtils.getUUIDsForAccount( - partialStorage, - 'ACCOUNT_THAT_DOES_NOT_EXIST_IN_STORAGE', - ); - expect(result.length).toBe(0); - }); -}); - -describe('metamask-notifications/utils - getAllUUIDs()', () => { - test('Returns all triggerIds in User Storage', () => { - const partialStorage = createMockUserStorageWithTriggers(['t1', 't2']); - const result1 = MetamaskNotificationsUtils.getAllUUIDs(partialStorage); - expect(result1.length).toBe(2); - expect(result1.includes('t1')).toBe(true); - expect(result1.includes('t2')).toBe(true); - - const fullStorage = createMockFullUserStorage(); - const result2 = MetamaskNotificationsUtils.getAllUUIDs(fullStorage); - expect(result2.length).toBeGreaterThan(2); // we expect there to be more than 2 triggers. We have multiple chains to there should be quite a few UUIDs. - }); -}); - -describe('metamask-notifications/utils - getUUIDsForKinds()', () => { - test('Returns all triggerIds that match the kind', () => { - const partialStorage = createMockUserStorageWithTriggers([ - { id: 't1', e: true, k: TRIGGER_TYPES.ERC1155_RECEIVED }, - { id: 't2', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, - ]); - const result = MetamaskNotificationsUtils.getUUIDsForKinds(partialStorage, [ - TRIGGER_TYPES.ERC1155_RECEIVED, - ]); - expect(result).toEqual(['t1']); - }); - - test('Returns empty list if no triggers are found matching the kinds', () => { - const partialStorage = createMockUserStorageWithTriggers([ - { id: 't1', e: true, k: TRIGGER_TYPES.ERC1155_RECEIVED }, - { id: 't2', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, - ]); - const result = MetamaskNotificationsUtils.getUUIDsForKinds(partialStorage, [ - TRIGGER_TYPES.ETH_SENT, // A kind we have not created a trigger for - ]); - expect(result.length).toBe(0); - }); -}); - -describe('metamask-notifications/utils - getUUIDsForAccountByKinds()', () => { - const createPartialStorage = () => - createMockUserStorageWithTriggers([ - { id: 't1', e: true, k: TRIGGER_TYPES.ERC1155_RECEIVED }, - { id: 't2', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, - ]); - - test('Returns triggers with correct account and matching kinds', () => { - const partialStorage = createPartialStorage(); - const result = MetamaskNotificationsUtils.getUUIDsForAccountByKinds( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - [TRIGGER_TYPES.ERC1155_RECEIVED], - ); - expect(result.length).toBe(1); - }); - - test('Returns empty when using incorrect account', () => { - const partialStorage = createPartialStorage(); - const result = MetamaskNotificationsUtils.getUUIDsForAccountByKinds( - partialStorage, - 'ACCOUNT_THAT_DOES_NOT_EXIST_IN_STORAGE', - [TRIGGER_TYPES.ERC1155_RECEIVED], - ); - expect(result.length).toBe(0); - }); - - test('Returns empty when using incorrect kind', () => { - const partialStorage = createPartialStorage(); - const result = MetamaskNotificationsUtils.getUUIDsForAccountByKinds( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - [TRIGGER_TYPES.ETH_SENT], // this trigger was not created in partial storage - ); - expect(result.length).toBe(0); - }); -}); - -describe('metamask-notifications/utils - upsertAddressTriggers()', () => { - test('Updates and adds new triggers for a new address', () => { - const MOCK_NEW_ADDRESS = 'MOCK_NEW_ADDRESS'.toLowerCase(); // addresses stored in user storage are lower-case - const storage = createMockFullUserStorage(); - - // Before - expect(storage[MOCK_NEW_ADDRESS]).toBeUndefined(); - - MetamaskNotificationsUtils.upsertAddressTriggers(MOCK_NEW_ADDRESS, storage); - - // After - expect(storage[MOCK_NEW_ADDRESS]).toBeDefined(); - const newTriggers = MetamaskNotificationsUtils.getUUIDsForAccount( - storage, - MOCK_NEW_ADDRESS, - ); - expect(newTriggers.length > 0).toBe(true); - }); -}); - -describe('metamask-notifications/utils - upsertTriggerTypeTriggers()', () => { - test('Updates and adds a new trigger to an address', () => { - const partialStorage = createMockUserStorageWithTriggers([ - { id: 't1', e: true, k: TRIGGER_TYPES.ERC1155_RECEIVED }, - { id: 't2', e: true, k: TRIGGER_TYPES.ERC1155_SENT }, - ]); - - // Before - expect( - MetamaskNotificationsUtils.getUUIDsForAccount( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - ).length, - ).toBe(2); - - MetamaskNotificationsUtils.upsertTriggerTypeTriggers( - TRIGGER_TYPES.ETH_SENT, - partialStorage, - ); - - // After - expect( - MetamaskNotificationsUtils.getUUIDsForAccount( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - ).length, - ).toBe(3); - }); -}); - -describe('metamask-notifications/utils - toggleUserStorageTriggerStatus()', () => { - test('Updates Triggers from disabled to enabled', () => { - // Triggers are initially set to false false. - const partialStorage = createMockUserStorageWithTriggers([ - { id: 't1', k: TRIGGER_TYPES.ERC1155_RECEIVED, e: false }, - { id: 't2', k: TRIGGER_TYPES.ERC1155_SENT, e: false }, - ]); - - MetamaskNotificationsUtils.toggleUserStorageTriggerStatus( - partialStorage, - MOCK_USER_STORAGE_ACCOUNT, - MOCK_USER_STORAGE_CHAIN, - 't1', - true, - ); - - expect( - partialStorage[MOCK_USER_STORAGE_ACCOUNT][MOCK_USER_STORAGE_CHAIN].t1.e, - ).toBe(true); - }); -}); diff --git a/app/scripts/controllers/metamask-notifications/utils/utils.ts b/app/scripts/controllers/metamask-notifications/utils/utils.ts deleted file mode 100644 index 547a38b217d5..000000000000 --- a/app/scripts/controllers/metamask-notifications/utils/utils.ts +++ /dev/null @@ -1,619 +0,0 @@ -import log from 'loglevel'; -import { v4 as uuidv4 } from 'uuid'; -import type { UserStorage } from '../types/user-storage/user-storage'; -import { - USER_STORAGE_VERSION_KEY, - USER_STORAGE_VERSION, -} from '../constants/constants'; -import { - TRIGGER_TYPES, - TRIGGER_TYPES_GROUPS, - TRIGGERS, -} from '../constants/notification-schema'; - -export type NotificationTrigger = { - id: string; - chainId: string; - kind: string; - address: string; - enabled: boolean; -}; - -type MapTriggerFn = ( - trigger: NotificationTrigger, -) => Result | undefined; - -type TraverseTriggerOpts = { - address?: string; - mapTrigger?: MapTriggerFn; -}; - -/** - * Extracts and returns the ID from a notification trigger. - * This utility function is primarily used as a mapping function in `traverseUserStorageTriggers` - * to convert a full trigger object into its ID string. - * - * @param trigger - The notification trigger from which the ID is extracted. - * @returns The ID of the provided notification trigger. - */ -const triggerToId = (trigger: NotificationTrigger): string => trigger.id; - -/** - * A utility function that returns the input trigger without any transformation. - * This function is used as the default mapping function in `traverseUserStorageTriggers` - * when no custom mapping function is provided. - * - * @param trigger - The notification trigger to be returned as is. - * @returns The same notification trigger that was passed in. - */ -const triggerIdentity = (trigger: NotificationTrigger): NotificationTrigger => - trigger; - -/** - * Maps a given trigger type to its corresponding trigger group. - * - * This method categorizes each trigger type into one of the predefined groups: - * RECEIVED, SENT, or DEFI. These groups help in organizing triggers based on their nature. - * For instance, triggers related to receiving assets are categorized under RECEIVED, - * triggers for sending assets under SENT, and triggers related to decentralized finance (DeFi) - * operations under DEFI. This categorization aids in managing and responding to different types - * of notifications more effectively. - * - * @param type - The trigger type to be categorized. - * @returns The group to which the trigger type belongs. - */ -const groupTriggerTypes = (type: TRIGGER_TYPES): TRIGGER_TYPES_GROUPS => { - switch (type) { - case TRIGGER_TYPES.ERC20_RECEIVED: - case TRIGGER_TYPES.ETH_RECEIVED: - case TRIGGER_TYPES.ERC721_RECEIVED: - case TRIGGER_TYPES.ERC1155_RECEIVED: - return TRIGGER_TYPES_GROUPS.RECEIVED; - case TRIGGER_TYPES.ERC20_SENT: - case TRIGGER_TYPES.ETH_SENT: - case TRIGGER_TYPES.ERC721_SENT: - case TRIGGER_TYPES.ERC1155_SENT: - return TRIGGER_TYPES_GROUPS.SENT; - case TRIGGER_TYPES.METAMASK_SWAP_COMPLETED: - case TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED: - case TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED: - case TRIGGER_TYPES.LIDO_STAKE_COMPLETED: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED: - default: - return TRIGGER_TYPES_GROUPS.DEFI; - } -}; - -/** - * Create a completely new user storage object with the given accounts and state. - * This method initializes the user storage with a version key and iterates over each account to populate it with triggers. - * Each trigger is associated with supported chains, and for each chain, a unique identifier (UUID) is generated. - * The trigger object contains a kind (`k`) indicating the type of trigger and an enabled state (`e`). - * The kind and enabled state are stored with abbreviated keys to reduce the JSON size. - * - * This is used primarily for creating a new user storage (e.g. when first signing in/enabling notification profile syncing), - * caution is needed in case you need to remove triggers that you don't want (due to notification setting filters) - * - * @param accounts - An array of account objects, each optionally containing an address. - * @param state - A boolean indicating the initial enabled state for all triggers in the user storage. - * @returns A `UserStorage` object populated with triggers for each account and chain. - */ -export function initializeUserStorage( - accounts: { address?: string }[], - state: boolean, -): UserStorage { - const userStorage: UserStorage = { - [USER_STORAGE_VERSION_KEY]: USER_STORAGE_VERSION, - }; - - accounts.forEach((account) => { - const address = account.address?.toLowerCase(); - if (!address) { - return; - } - if (!userStorage[address]) { - userStorage[address] = {}; - } - - Object.entries(TRIGGERS).forEach( - ([trigger, { supported_chains: supportedChains }]) => { - supportedChains.forEach((chain) => { - if (!userStorage[address]?.[chain]) { - userStorage[address][chain] = {}; - } - - userStorage[address][chain][uuidv4()] = { - k: trigger as TRIGGER_TYPES, // use 'k' instead of 'kind' to reduce the json weight - e: state, // use 'e' instead of 'enabled' to reduce the json weight - }; - }); - }, - ); - }); - - return userStorage; -} - -/** - * Iterates over user storage to find and optionally transform notification triggers. - * This method allows for flexible retrieval and transformation of triggers based on provided options. - * - * @param userStorage - The user storage object containing notification triggers. - * @param options - Optional parameters to filter and map triggers: - * - `address`: If provided, only triggers for this address are considered. - * - `mapTrigger`: A function to transform each trigger. If not provided, triggers are returned as is. - * @returns An array of triggers, potentially transformed by the `mapTrigger` function. - */ -export function traverseUserStorageTriggers< - ResultTriggers = NotificationTrigger, ->( - userStorage: UserStorage, - options?: TraverseTriggerOpts, -): ResultTriggers[] { - const triggers: ResultTriggers[] = []; - const mapTrigger = - options?.mapTrigger ?? (triggerIdentity as MapTriggerFn); - - for (const address in userStorage) { - if (address === (USER_STORAGE_VERSION_KEY as unknown as string)) { - continue; - } - if (options?.address && address !== options.address) { - continue; - } - - for (const chainId in userStorage[address]) { - if (Object.hasOwn(userStorage[address], chainId)) { - for (const uuid in userStorage[address][chainId]) { - if (uuid) { - const mappedTrigger = mapTrigger({ - id: uuid, - kind: userStorage[address]?.[chainId]?.[uuid]?.k, - chainId, - address, - enabled: userStorage[address]?.[chainId]?.[uuid]?.e ?? false, - }); - if (mappedTrigger) { - triggers.push(mappedTrigger); - } - } - } - } - } - } - - return triggers; -} - -/** - * @deprecated - This needs rework for it to be feasible. Currently this is a half-baked solution, as it fails once we add new triggers (introspection for filters is difficult). - * - * Checks for the complete presence of trigger types by group across all addresses in the user storage. - * This method ensures that each address has at least one trigger of each type expected for every group. - * It leverages `traverseUserStorageTriggers` to iterate over triggers and check their presence. - * @param userStorage - The user storage object containing notification triggers. - * @returns A record indicating whether all expected trigger types for each group are present for every address. - */ -export function checkTriggersPresenceByGroup( - userStorage: UserStorage, -): Record { - // Initialize a record to track the complete presence of triggers for each group - const completeGroupPresence: Record = { - [TRIGGER_TYPES_GROUPS.RECEIVED]: true, - [TRIGGER_TYPES_GROUPS.SENT]: true, - [TRIGGER_TYPES_GROUPS.DEFI]: true, - }; - - // Map to track the required trigger types for each group - const requiredTriggersByGroup: Record< - TRIGGER_TYPES_GROUPS, - Set - > = { - [TRIGGER_TYPES_GROUPS.RECEIVED]: new Set([ - TRIGGER_TYPES.ERC20_RECEIVED, - TRIGGER_TYPES.ETH_RECEIVED, - TRIGGER_TYPES.ERC721_RECEIVED, - TRIGGER_TYPES.ERC1155_RECEIVED, - ]), - [TRIGGER_TYPES_GROUPS.SENT]: new Set([ - TRIGGER_TYPES.ERC20_SENT, - TRIGGER_TYPES.ETH_SENT, - TRIGGER_TYPES.ERC721_SENT, - TRIGGER_TYPES.ERC1155_SENT, - ]), - [TRIGGER_TYPES_GROUPS.DEFI]: new Set([ - TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, - TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, - TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, - TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, - TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, - ]), - }; - - // Object to keep track of encountered triggers for each group by address - const encounteredTriggers: Record< - string, - Record> - > = {}; - - // Use traverseUserStorageTriggers to iterate over all triggers - traverseUserStorageTriggers(userStorage, { - mapTrigger: (trigger) => { - const group = groupTriggerTypes(trigger.kind as TRIGGER_TYPES); - if (!encounteredTriggers[trigger.address]) { - encounteredTriggers[trigger.address] = { - [TRIGGER_TYPES_GROUPS.RECEIVED]: new Set(), - [TRIGGER_TYPES_GROUPS.SENT]: new Set(), - [TRIGGER_TYPES_GROUPS.DEFI]: new Set(), - }; - } - encounteredTriggers[trigger.address][group].add( - trigger.kind as TRIGGER_TYPES, - ); - return undefined; // We don't need to transform the trigger, just record its presence - }, - }); - - // Check if all required triggers for each group are present for every address - Object.keys(encounteredTriggers).forEach((address) => { - Object.entries(requiredTriggersByGroup).forEach( - ([group, requiredTriggers]) => { - const hasAllTriggers = Array.from(requiredTriggers).every( - (triggerType) => - encounteredTriggers[address][group as TRIGGER_TYPES_GROUPS].has( - triggerType, - ), - ); - if (!hasAllTriggers) { - completeGroupPresence[group as TRIGGER_TYPES_GROUPS] = false; - } - }, - ); - }); - - return completeGroupPresence; -} - -/** - * Verifies the presence of specified accounts and their chains in the user storage. - * This method checks if each provided account exists in the user storage and if all its supported chains are present. - * - * @param userStorage - The user storage object containing notification triggers. - * @param accounts - An array of account addresses to check for presence. - * @returns A record where each key is an account address and each value is a boolean indicating whether the account and all its supported chains are present in the user storage. - */ -export function checkAccountsPresence( - userStorage: UserStorage, - accounts: string[], -): Record { - const presenceRecord: Record = {}; - - // Initialize presence record for all accounts as false - accounts.forEach((account) => { - presenceRecord[account.toLowerCase()] = isAccountEnabled( - account, - userStorage, - ); - }); - - return presenceRecord; -} - -function isAccountEnabled( - accountAddress: string, - userStorage: UserStorage, -): boolean { - const accountObject = userStorage[accountAddress?.toLowerCase()]; - - // If the account address is not present in the userStorage, return true - if (!accountObject) { - return false; - } - - // Check if all available chains are present - for (const [triggerKind, triggerConfig] of Object.entries(TRIGGERS)) { - for (const chain of triggerConfig.supported_chains) { - if (!accountObject[chain]) { - return false; - } - - const triggerExists = Object.values(accountObject[chain]).some( - (obj) => obj.k === triggerKind, - ); - if (!triggerExists) { - return false; - } - - // Check if any trigger is disabled - for (const uuid in accountObject[chain]) { - if (!accountObject[chain][uuid].e) { - return false; - } - } - } - } - - return true; -} - -/** - * Infers and returns an array of enabled notification trigger kinds from the user storage. - * This method counts the occurrences of each kind of trigger and returns the kinds that are present. - * - * @param userStorage - The user storage object containing notification triggers. - * @returns An array of trigger kinds (`TRIGGER_TYPES`) that are enabled in the user storage. - */ -export function inferEnabledKinds(userStorage: UserStorage): TRIGGER_TYPES[] { - const allSupportedKinds = new Set(); - - traverseUserStorageTriggers(userStorage, { - mapTrigger: (t) => { - allSupportedKinds.add(t.kind as TRIGGER_TYPES); - }, - }); - - return Array.from(allSupportedKinds); -} - -/** - * Retrieves all UUIDs associated with a specific account address from the user storage. - * This function utilizes `traverseUserStorageTriggers` with a mapping function to extract - * just the UUIDs of the notification triggers for the given address. - * - * @param userStorage - The user storage object containing notification triggers. - * @param address - The specific account address to retrieve UUIDs for. - * @returns An array of UUID strings associated with the given account address. - */ -export function getUUIDsForAccount( - userStorage: UserStorage, - address: string, -): string[] { - return traverseUserStorageTriggers(userStorage, { - address, - mapTrigger: triggerToId, - }); -} - -/** - * Retrieves all UUIDs from the user storage, regardless of the account address or chain ID. - * This method leverages `traverseUserStorageTriggers` with a specific mapping function (`triggerToId`) - * to extract only the UUIDs from all notification triggers present in the user storage. - * - * @param userStorage - The user storage object containing notification triggers. - * @returns An array of UUID strings from all notification triggers in the user storage. - */ -export function getAllUUIDs(userStorage: UserStorage): string[] { - return traverseUserStorageTriggers(userStorage, { - mapTrigger: triggerToId, - }); -} - -/** - * Retrieves UUIDs for notification triggers that match any of the specified kinds. - * This method filters triggers based on their kind and returns an array of UUIDs for those that match the allowed kinds. - * It utilizes `traverseUserStorageTriggers` with a custom mapping function that checks if a trigger's kind is in the allowed list. - * - * @param userStorage - The user storage object containing notification triggers. - * @param allowedKinds - An array of kinds (as strings) to filter the triggers by. - * @returns An array of UUID strings for triggers that match the allowed kinds. - */ -export function getUUIDsForKinds( - userStorage: UserStorage, - allowedKinds: string[], -): string[] { - const kindsSet = new Set(allowedKinds); - - return traverseUserStorageTriggers(userStorage, { - mapTrigger: (t) => (kindsSet.has(t.kind) ? t.id : undefined), - }); -} - -/** - * Retrieves notification triggers for a specific account address that match any of the specified kinds. - * This method filters triggers both by the account address and their kind, returning triggers that match the allowed kinds for the specified address. - * It leverages `traverseUserStorageTriggers` with a custom mapping function to filter and return only the relevant triggers. - * - * @param userStorage - The user storage object containing notification triggers. - * @param address - The specific account address for which to retrieve triggers. - * @param allowedKinds - An array of trigger kinds (`TRIGGER_TYPES`) to filter the triggers by. - * @returns An array of `NotificationTrigger` objects that match the allowed kinds for the specified account address. - */ -export function getUUIDsForAccountByKinds( - userStorage: UserStorage, - address: string, - allowedKinds: TRIGGER_TYPES[], -): NotificationTrigger[] { - const allowedKindsSet = new Set(allowedKinds); - return traverseUserStorageTriggers(userStorage, { - address, - mapTrigger: (trigger) => { - if (allowedKindsSet.has(trigger.kind as TRIGGER_TYPES)) { - return trigger; - } - return undefined; - }, - }); -} - -/** - * Upserts (updates or inserts) notification triggers for a given account across all supported chains. - * This method ensures that each supported trigger type exists for each chain associated with the account. - * If a trigger type does not exist for a chain, it creates a new trigger with a unique UUID. - * - * @param _account - The account address for which to upsert triggers. The address is normalized to lowercase. - * @param userStorage - The user storage object to be updated with new or existing triggers. - * @returns The updated user storage object with upserted triggers for the specified account. - */ -export function upsertAddressTriggers( - _account: string, - userStorage: UserStorage, -): UserStorage { - // Ensure the account exists in userStorage - const account = _account.toLowerCase(); - userStorage[account] = userStorage[account] || {}; - - // Iterate over each trigger and its supported chains - for (const [trigger, { supported_chains: supportedChains }] of Object.entries( - TRIGGERS, - )) { - for (const chain of supportedChains) { - // Ensure the chain exists for the account - userStorage[account][chain] = userStorage[account][chain] || {}; - - // Check if the trigger exists for the chain - const existingTrigger = Object.values(userStorage[account][chain]).find( - (obj) => obj.k === trigger, - ); - - if (!existingTrigger) { - // If the trigger doesn't exist, create a new one with a new UUID - const uuid = uuidv4(); - userStorage[account][chain][uuid] = { - k: trigger as TRIGGER_TYPES, - e: false, - }; - } - } - } - - return userStorage; -} - -/** - * Upserts (updates or inserts) notification triggers of a specific type across all accounts and chains in user storage. - * This method ensures that a trigger of the specified type exists for each account and chain. If a trigger of the specified type - * does not exist for an account and chain, it creates a new trigger with a unique UUID. - * - * @param triggerType - The type of trigger to upsert across all accounts and chains. - * @param userStorage - The user storage object to be updated with new or existing triggers of the specified type. - * @returns The updated user storage object with upserted triggers of the specified type for all accounts and chains. - */ -export function upsertTriggerTypeTriggers( - triggerType: TRIGGER_TYPES, - userStorage: UserStorage, -): UserStorage { - // Iterate over each account in userStorage - Object.entries(userStorage).forEach(([account, chains]) => { - if (account === (USER_STORAGE_VERSION_KEY as unknown as string)) { - return; - } - - // Iterate over each chain for the account - Object.entries(chains).forEach(([chain, triggers]) => { - // Check if the trigger type exists for the chain - const existingTrigger = Object.values(triggers).find( - (obj) => obj.k === triggerType, - ); - - if (!existingTrigger) { - // If the trigger type doesn't exist, create a new one with a new UUID - const uuid = uuidv4(); - userStorage[account][chain][uuid] = { - k: triggerType, - e: false, - }; - } - }); - }); - - return userStorage; -} - -/** - * Toggles the enabled status of a user storage trigger. - * - * @param userStorage - The user storage object. - * @param address - The user's address. - * @param chainId - The chain ID. - * @param uuid - The unique identifier for the trigger. - * @param enabled - The new enabled status. - * @returns The updated user storage object. - */ -export function toggleUserStorageTriggerStatus( - userStorage: UserStorage, - address: string, - chainId: string, - uuid: string, - enabled: boolean, -): UserStorage { - if (userStorage?.[address]?.[chainId]?.[uuid]) { - userStorage[address][chainId][uuid].e = enabled; - } - - return userStorage; -} - -/** - * Attempts to fetch a resource from the network, retrying the request up to a specified number of times - * in case of failure, with a delay between attempts. - * - * @param url - The resource URL. - * @param options - The options for the fetch request. - * @param retries - Maximum number of retry attempts. Defaults to 3. - * @param retryDelay - Delay between retry attempts in milliseconds. Defaults to 1000. - * @returns A Promise resolving to the Response object. - * @throws Will throw an error if the request fails after the specified number of retries. - */ -async function fetchWithRetry( - url: string, - options: RequestInit, - retries = 3, - retryDelay = 1000, -): Promise { - for (let attempt = 1; attempt <= retries; attempt++) { - try { - const response = await fetch(url, options); - if (!response.ok) { - throw new Error(`Fetch failed with status: ${response.status}`); - } - return response; - } catch (error) { - log.error(`Attempt ${attempt} failed for fetch:`, error); - if (attempt < retries) { - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } else { - throw new Error( - `Fetching failed after ${retries} retries. Last error: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ); - } - } - } - - throw new Error('Unexpected error in fetchWithRetry'); -} - -/** - * Performs an API call with automatic retries on failure. - * - * @param bearerToken - The JSON Web Token for authorization. - * @param endpoint - The URL of the API endpoint to call. - * @param method - The HTTP method ('POST' or 'DELETE'). - * @param body - The body of the request. It should be an object that can be serialized to JSON. - * @param retries - The number of retry attempts in case of failure (default is 3). - * @param retryDelay - The delay between retries in milliseconds (default is 1000). - * @returns A Promise that resolves to the response of the fetch request. - */ -export async function makeApiCall( - bearerToken: string, - endpoint: string, - method: 'POST' | 'DELETE', - body: T, - retries = 1, - retryDelay = 1000, -): Promise { - const options: RequestInit = { - method, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${bearerToken}`, - }, - body: JSON.stringify(body), - }; - - return fetchWithRetry(endpoint, options, retries, retryDelay); -} diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 4206de67a7fd..40a3f2795f01 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -7,8 +7,9 @@ import { METAMETRICS_BACKGROUND_PAGE_OBJECT, MetaMetricsUserTrait, } from '../../../shared/constants/metametrics'; -import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network'; +import { CHAIN_IDS } from '../../../shared/constants/network'; import * as Utils from '../lib/util'; +import { mockNetworkState } from '../../../test/stub/networks'; import MetaMetricsController from './metametrics'; const segment = createSegmentMock(2, 10000); @@ -1015,17 +1016,11 @@ describe('MetaMetricsController', function () { }, }, allTokens: MOCK_ALL_TOKENS, - networkConfigurations: { - 'network-configuration-id-1': { - chainId: CHAIN_IDS.MAINNET, - ticker: CURRENCY_SYMBOLS.ETH, - }, - 'network-configuration-id-2': { - chainId: CHAIN_IDS.GOERLI, - ticker: CURRENCY_SYMBOLS.TEST_ETH, - }, - 'network-configuration-id-3': { chainId: '0xaf' }, - }, + ...mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.GOERLI }, + { chainId: '0xaf' }, + ), internalAccounts: { accounts: { mock1: {}, @@ -1109,16 +1104,17 @@ describe('MetaMetricsController', function () { it('should return only changed traits object on subsequent calls', function () { const metaMetricsController = getMetaMetricsController(); + const networkState = mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.GOERLI }, + ); metaMetricsController._buildUserTraitsObject({ addressBook: { [CHAIN_IDS.MAINNET]: [{ address: '0x' }], [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - networkConfigurations: { - 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, - 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, - }, + ...networkState, ledgerTransportType: 'web-hid', openSeaEnabled: true, internalAccounts: { @@ -1144,10 +1140,7 @@ describe('MetaMetricsController', function () { '0xabcde': [{ '0x12345': { address: '0xtestAddress' } }], }, }, - networkConfigurations: { - 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, - 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, - }, + ...networkState, ledgerTransportType: 'web-hid', openSeaEnabled: false, internalAccounts: { @@ -1175,16 +1168,17 @@ describe('MetaMetricsController', function () { it('should return null if no traits changed', function () { const metaMetricsController = getMetaMetricsController(); + const networkState = mockNetworkState( + { chainId: CHAIN_IDS.MAINNET }, + { chainId: CHAIN_IDS.GOERLI }, + ); metaMetricsController._buildUserTraitsObject({ addressBook: { [CHAIN_IDS.MAINNET]: [{ address: '0x' }], [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - networkConfigurations: { - 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, - 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, - }, + ...networkState, ledgerTransportType: 'web-hid', openSeaEnabled: true, internalAccounts: { @@ -1206,10 +1200,7 @@ describe('MetaMetricsController', function () { [CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }], }, allTokens: {}, - networkConfigurations: { - 'network-configuration-id-1': { chainId: CHAIN_IDS.MAINNET }, - 'network-configuration-id-2': { chainId: CHAIN_IDS.GOERLI }, - }, + ...networkState, ledgerTransportType: 'web-hid', openSeaEnabled: true, internalAccounts: { diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 102cd63d52f3..3a9e6cddba6a 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -23,6 +23,7 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; +import { mockNetworkState } from '../../../test/stub/networks'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ handleMmiPortfolio: jest.fn(), @@ -98,13 +99,7 @@ describe('MMIController', function () { 'NetworkController:infuraIsUnblocked', ], }), - state: { - providerConfig: { - type: NETWORK_TYPES.SEPOLIA, - chainId: CHAIN_IDS.SEPOLIA, - ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], - }, - }, + state: mockNetworkState({chainId: CHAIN_IDS.SEPOLIA}), infuraProjectId: 'mock-infura-project-id', }); @@ -456,26 +451,6 @@ describe('MMIController', function () { }); }); - describe('getCustodianAccountsByAddress', () => { - it('should return custodian accounts by address', async () => { - CUSTODIAN_TYPES['MOCK-CUSTODIAN-TYPE'] = { - keyringClass: { type: 'mock-keyring-class' }, - }; - mmiController.addKeyringIfNotExists = jest.fn().mockResolvedValue({ - getCustodianAccounts: jest.fn().mockResolvedValue(['account1']), - }); - - const result = await mmiController.getCustodianAccountsByAddress( - 'token', - 'envName', - 'address', - 'mock-custodian-type', - ); - - expect(result).toEqual(['account1']); - }); - }); - describe('getCustodianTransactionDeepLink', () => { it('should return a transaction deep link', async () => { mmiController.custodyController.getCustodyTypeByAddress = jest diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 755cab0f8fbf..1ba853a1b846 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -38,6 +38,7 @@ import { ConnectionRequest, } from '../../../shared/constants/mmi-controller'; import AccountTracker from '../lib/account-tracker'; +import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; import { PreferencesController } from './preferences'; @@ -570,33 +571,6 @@ export default class MMIController extends EventEmitter { return accounts; } - async getCustodianAccountsByAddress( - token: string, - envName: string, - address: string, - custodianType: string, - ) { - let keyring; - - if (custodianType) { - const custodian = CUSTODIAN_TYPES[custodianType.toUpperCase()]; - if (!custodian) { - throw new Error('No such custodian'); - } - - keyring = await this.addKeyringIfNotExists(custodian.keyringClass.type); - } else { - throw new Error('No custodian specified'); - } - - const accounts = await keyring.getCustodianAccounts( - token, - envName, - address, - ); - return accounts; - } - async getCustodianTransactionDeepLink(address: string, txId: string) { const custodyType = this.custodyController.getCustodyTypeByAddress( toChecksumHexAddress(address), @@ -817,23 +791,25 @@ export default class MMIController extends EventEmitter { const isCustodial = Boolean(accountDetails); const updatedMsgParams = { ...msgParams, deferSetAsSigned: isCustodial }; - if (req.method.includes('eth_signTypedData')) { + if ( + req.method === 'eth_signTypedData' || + req.method === 'eth_signTypedData_v3' || + req.method === 'eth_signTypedData_v4' + ) { return await this.signatureController.newUnsignedTypedMessage( updatedMsgParams as PersonalMessageParams, req as OriginalRequest, version, { parseJsonData: false }, ); - } else if (req.method.includes('personal_sign')) { + } else if (req.method === 'personal_sign') { return await this.signatureController.newUnsignedPersonalMessage( updatedMsgParams as PersonalMessageParams, req as OriginalRequest, ); } - return await this.signatureController.newUnsignedMessage( - updatedMsgParams as PersonalMessageParams, - req as OriginalRequest, - ); + + throw new Error('Unexpected method'); } async handleSigningEvents( @@ -878,11 +854,11 @@ export default class MMIController extends EventEmitter { ); } const selectedChainId = parseInt( - this.networkController.state.providerConfig.chainId, + getCurrentChainId({ metamask: this.networkController.state }), 16, ); if (selectedChainId !== chainId && chainId === 1) { - await this.networkController.setProviderType('mainnet'); + await this.networkController.setActiveNetwork('mainnet'); } else if (selectedChainId !== chainId) { const { networkConfigurations } = this.networkController.state; diff --git a/app/scripts/controllers/permissions/caveat-mutators.js b/app/scripts/controllers/permissions/caveat-mutators.js index d712a681e1e3..551d1f7b37b2 100644 --- a/app/scripts/controllers/permissions/caveat-mutators.js +++ b/app/scripts/controllers/permissions/caveat-mutators.js @@ -1,6 +1,6 @@ import { CaveatMutatorOperation } from '@metamask/permission-controller'; -import { toChecksumAddress } from 'ethereumjs-util'; import { CaveatTypes } from '../../../../shared/constants/permissions'; +import { normalizeSafeAddress } from '../../lib/multichain/address'; /** * Factories that construct caveat mutator functions that are passed to @@ -27,9 +27,9 @@ export const CaveatMutatorFactories = { * account permissions. */ function removeAccount(targetAccount, existingAccounts) { - const checkSumTargetAccount = toChecksumAddress(targetAccount); + const checkSumTargetAccount = normalizeSafeAddress(targetAccount); const newAccounts = existingAccounts.filter( - (address) => toChecksumAddress(address) !== checkSumTargetAccount, + (address) => normalizeSafeAddress(address) !== checkSumTargetAccount, ); if (newAccounts.length === existingAccounts.length) { diff --git a/app/scripts/controllers/permissions/caveat-mutators.test.js b/app/scripts/controllers/permissions/caveat-mutators.test.js index 43a4a55effe8..a87115dc744b 100644 --- a/app/scripts/controllers/permissions/caveat-mutators.test.js +++ b/app/scripts/controllers/permissions/caveat-mutators.test.js @@ -2,6 +2,10 @@ import { CaveatMutatorOperation } from '@metamask/permission-controller'; import { CaveatTypes } from '../../../../shared/constants/permissions'; import { CaveatMutatorFactories } from './caveat-mutators'; +const address1 = '0xbf16f7f5db8ae6af2512399bace3101debbde7fc'; +const address2 = '0xb6d5abeca51bfc3d53d00afed06b17eeea32ecdf'; +const nonEvmAddress = 'bc1qdkwac3em6mvlur4fatn2g4q050f4kkqadrsmnp'; + describe('caveat mutators', () => { describe('restrictReturnedAccounts', () => { const { removeAccount } = @@ -9,33 +13,55 @@ describe('caveat mutators', () => { describe('removeAccount', () => { it('returns the no-op operation if the target account is not permitted', () => { - expect(removeAccount('0x2', ['0x1'])).toStrictEqual({ + expect(removeAccount(address2, [address1])).toStrictEqual({ operation: CaveatMutatorOperation.noop, }); }); it('returns the update operation and a new value if the target account is permitted', () => { - expect(removeAccount('0x2', ['0x1', '0x2'])).toStrictEqual({ + expect(removeAccount(address2, [address1, address2])).toStrictEqual({ operation: CaveatMutatorOperation.updateValue, - value: ['0x1'], + value: [address1], }); }); it('returns the revoke permission operation the target account is the only permitted account', () => { - expect(removeAccount('0x1', ['0x1'])).toStrictEqual({ + expect(removeAccount(address1, [address1])).toStrictEqual({ operation: CaveatMutatorOperation.revokePermission, }); }); it('returns the revoke permission operation even if the target account is a checksummed address', () => { - expect( - removeAccount('0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAee5', [ - '0x95222290dd7278aa3ddd389cc1e1d165cc4baee5', - ]), - ).toStrictEqual({ + const address3 = '0x95222290dd7278aa3ddd389cc1e1d165cc4baee5'; + const checksummedAddress3 = + '0x95222290dd7278AA3DDd389cc1E1d165Cc4BaeE5'; + expect(removeAccount(checksummedAddress3, [address3])).toStrictEqual({ operation: CaveatMutatorOperation.revokePermission, }); }); + + describe('Multichain behaviour', () => { + it('returns the no-op operation if the target account is not permitted', () => { + expect(removeAccount(address2, [nonEvmAddress])).toStrictEqual({ + operation: CaveatMutatorOperation.noop, + }); + }); + + it('can revoke permission for non-EVM addresses', () => { + expect(removeAccount(nonEvmAddress, [nonEvmAddress])).toStrictEqual({ + operation: CaveatMutatorOperation.revokePermission, + }); + }); + + it('returns the update operation and a new value if the target non-EVM account is permitted', () => { + expect( + removeAccount(nonEvmAddress, [address1, nonEvmAddress]), + ).toStrictEqual({ + operation: CaveatMutatorOperation.updateValue, + value: [address1], + }); + }); + }); }); }); }); diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index eab746b42608..2d25ab16b1e4 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -25,7 +25,7 @@ import { */ export const PermissionNames = Object.freeze({ ...RestrictedMethods, - permittedChains: 'permittedChains', + permittedChains: 'endowment:permitted-chains', }); /** @@ -78,6 +78,11 @@ export const getCaveatSpecifications = ({ type: CaveatTypes.restrictNetworkSwitching, validator: (caveat, _origin, _target) => validateCaveatNetworks(caveat.value, findNetworkClientIdByChainId), + merger: (leftValue, rightValue) => { + const newValue = Array.from(new Set([...leftValue, ...rightValue])); + const diff = newValue.filter((value) => !leftValue.includes(value)); + return [newValue, diff]; + }, }, ...snapsCaveatsSpecifications, @@ -312,7 +317,6 @@ function validateCaveatNetworks( export const unrestrictedEthSigningMethods = Object.freeze([ 'eth_sendRawTransaction', 'eth_sendTransaction', - 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -366,7 +370,6 @@ export const unrestrictedMethods = Object.freeze([ 'eth_requestAccounts', 'eth_sendRawTransaction', 'eth_sendTransaction', - 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -405,6 +408,7 @@ export const unrestrictedMethods = Object.freeze([ 'snap_createInterface', 'snap_updateInterface', 'snap_getInterfaceState', + 'snap_resolveInterface', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) 'metamaskinstitutional_authenticate', 'metamaskinstitutional_reauthenticate', diff --git a/app/scripts/controllers/permissions/specifications.test.js b/app/scripts/controllers/permissions/specifications.test.js index 29f9f4f1b8ce..b27ec07a45b1 100644 --- a/app/scripts/controllers/permissions/specifications.test.js +++ b/app/scripts/controllers/permissions/specifications.test.js @@ -9,6 +9,7 @@ import { CaveatFactories, getCaveatSpecifications, getPermissionSpecifications, + PermissionNames, unrestrictedMethods, } from './specifications'; @@ -239,9 +240,9 @@ describe('PermissionController specifications', () => { expect( permissionSpecifications[RestrictedMethods.eth_accounts].targetName, ).toStrictEqual(RestrictedMethods.eth_accounts); - expect(permissionSpecifications.permittedChains.targetName).toStrictEqual( - 'permittedChains', - ); + expect( + permissionSpecifications[PermissionNames.permittedChains].targetName, + ).toStrictEqual('endowment:permitted-chains'); }); describe('eth_accounts', () => { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 5509d804b6fd..677761cd2ef5 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -47,11 +47,7 @@ export default class PreferencesController { useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, - disabledRpcMethodPreferences: { - eth_sign: false, - }, useMultiAccountBalanceChecker: true, - hasDismissedOpenSeaToBlockaidBanner: false, useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used // set to false will be using the static list from contract-metadata @@ -62,6 +58,9 @@ export default class PreferencesController { useRequestQueue: true, openSeaEnabled: true, // todo set this to true securityAlertsEnabled: true, + watchEthereumAccountEnabled: false, + bitcoinSupportEnabled: false, + bitcoinTestnetSupportEnabled: false, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) addSnapAccountEnabled: false, ///: END:ONLY_INCLUDE_IF @@ -92,9 +91,10 @@ export default class PreferencesController { hideZeroBalanceTokens: false, petnamesEnabled: true, redesignedConfirmationsEnabled: true, + redesignedTransactionsEnabled: true, featureNotificationsEnabled: false, - showTokenAutodetectModal: null, - showNftAutodetectModal: null, // null because we want to show the modal only the first time + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, }, // ENS decentralized website resolution ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -191,14 +191,6 @@ export default class PreferencesController { this.store.updateState({ useMultiAccountBalanceChecker: val }); } - /** - * Setter for the `dismissOpenSeaToBlockaidBanner` property - * - */ - dismissOpenSeaToBlockaidBanner() { - this.store.updateState({ hasDismissedOpenSeaToBlockaidBanner: true }); - } - /** * Setter for the `useSafeChainsListValidation` property * @@ -299,6 +291,42 @@ export default class PreferencesController { } ///: END:ONLY_INCLUDE_IF + /** + * Setter for the `watchEthereumAccountEnabled` property. + * + * @param {boolean} watchEthereumAccountEnabled - Whether or not the user wants to + * enable the "Watch Ethereum account (Beta)" button. + */ + setWatchEthereumAccountEnabled(watchEthereumAccountEnabled) { + this.store.updateState({ + watchEthereumAccountEnabled, + }); + } + + /** + * Setter for the `bitcoinSupportEnabled` property. + * + * @param {boolean} bitcoinSupportEnabled - Whether or not the user wants to + * enable the "Add a new Bitcoin account (Beta)" button. + */ + setBitcoinSupportEnabled(bitcoinSupportEnabled) { + this.store.updateState({ + bitcoinSupportEnabled, + }); + } + + /** + * Setter for the `bitcoinTestnetSupportEnabled` property. + * + * @param {boolean} bitcoinTestnetSupportEnabled - Whether or not the user wants to + * enable the "Add a new Bitcoin account (Testnet)" button. + */ + setBitcoinTestnetSupportEnabled(bitcoinTestnetSupportEnabled) { + this.store.updateState({ + bitcoinTestnetSupportEnabled, + }); + } + /** * Setter for the `useExternalNameSources` property * @@ -556,25 +584,6 @@ export default class PreferencesController { }); } - /** - * A setter for the user preference to enable/disable rpc methods - * - * @param {string} methodName - The RPC method name to change the setting of - * @param {bool} isEnabled - true to enable the rpc method - */ - async setDisabledRpcMethodPreference(methodName, isEnabled) { - const currentRpcMethodPreferences = - this.store.getState().disabledRpcMethodPreferences; - const updatedRpcMethodPreferences = { - ...currentRpcMethodPreferences, - [methodName]: isEnabled, - }; - - this.store.updateState({ - disabledRpcMethodPreferences: updatedRpcMethodPreferences, - }); - } - /** * A setter for the incomingTransactions in preference to be updated * @@ -591,10 +600,6 @@ export default class PreferencesController { this.store.updateState({ enableMV3TimestampSave: value }); } - getRpcMethodPreferences() { - return this.store.getState().disabledRpcMethodPreferences; - } - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) setSnapsAddSnapAccountModalDismissed(value) { this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index fc344ada1264..60784db8984f 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -5,23 +5,26 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { TokenListController } from '@metamask/assets-controllers'; import { AccountsController } from '@metamask/accounts-controller'; import { CHAIN_IDS } from '../../../shared/constants/network'; +import { mockNetworkState } from '../../../test/stub/networks'; import PreferencesController from './preferences'; -const NETWORK_CONFIGURATION_DATA = { - 'test-networkConfigurationId-1': { +const NETWORK_CONFIGURATION_DATA = mockNetworkState( + { + id: 'test-networkConfigurationId-1', rpcUrl: 'https://testrpc.com', chainId: CHAIN_IDS.GOERLI, nickname: '0X5', rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' }, }, - 'test-networkConfigurationId-2': { + { + id: 'test-networkConfigurationId-2', rpcUrl: 'http://localhost:8545', chainId: '0x539', ticker: 'ETH', nickname: 'Localhost 8545', rpcPrefs: {}, }, -}; +).networkConfigurations; describe('preferences controller', () => { let controllerMessenger; @@ -308,21 +311,13 @@ describe('preferences controller', () => { }); }); - describe('dismissOpenSeaToBlockaidBanner', () => { - it('hasDismissedOpenSeaToBlockaidBanner should default to false', () => { + describe('isRedesignedConfirmationsFeatureEnabled', () => { + it('isRedesignedConfirmationsFeatureEnabled should default to false', () => { expect( - preferencesController.store.getState() - .hasDismissedOpenSeaToBlockaidBanner, + preferencesController.store.getState().preferences + .isRedesignedConfirmationsDeveloperEnabled, ).toStrictEqual(false); }); - - it('should set the hasDismissedOpenSeaToBlockaidBanner property in state', () => { - preferencesController.dismissOpenSeaToBlockaidBanner(); - expect( - preferencesController.store.getState() - .hasDismissedOpenSeaToBlockaidBanner, - ).toStrictEqual(true); - }); }); describe('setUseSafeChainsListValidation', function () { @@ -577,4 +572,24 @@ describe('preferences controller', () => { ).toStrictEqual(false); }); }); + + describe('setBitcoinSupportEnabled', () => { + it('has the default value as false', () => { + expect( + preferencesController.store.getState().bitcoinSupportEnabled, + ).toStrictEqual(false); + }); + + it('sets the bitcoinSupportEnabled property in state to true and then false', () => { + preferencesController.setBitcoinSupportEnabled(true); + expect( + preferencesController.store.getState().bitcoinSupportEnabled, + ).toStrictEqual(true); + + preferencesController.setBitcoinSupportEnabled(false); + expect( + preferencesController.store.getState().bitcoinSupportEnabled, + ).toStrictEqual(false); + }); + }); }); diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-image.ts b/app/scripts/controllers/push-notifications/get-notification-image.ts similarity index 100% rename from app/scripts/controllers/push-platform-notifications/utils/get-notification-image.ts rename to app/scripts/controllers/push-notifications/get-notification-image.ts diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts b/app/scripts/controllers/push-notifications/get-notification-message.test.ts similarity index 96% rename from app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts rename to app/scripts/controllers/push-notifications/get-notification-message.test.ts index fdbe7e345104..9d8a6a1e2fdf 100644 --- a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts +++ b/app/scripts/controllers/push-notifications/get-notification-message.test.ts @@ -1,4 +1,7 @@ -import { +import { NotificationServicesController } from '@metamask/notification-services-controller'; +import { createNotificationMessage } from './get-notification-message'; + +const { createMockNotificationERC1155Received, createMockNotificationERC1155Sent, createMockNotificationERC20Received, @@ -14,9 +17,9 @@ import { createMockNotificationMetaMaskSwapsCompleted, createMockNotificationRocketPoolStakeCompleted, createMockNotificationRocketPoolUnStakeCompleted, -} from '../../metamask-notifications/mocks/mock-raw-notifications'; -import { processNotification } from '../../metamask-notifications/processors/process-notifications'; -import { createNotificationMessage } from './get-notification-message'; +} = NotificationServicesController.Mocks; + +const { processNotification } = NotificationServicesController.Processors; describe('notification-message tests', () => { test('displays erc20 sent notification', () => { diff --git a/app/scripts/controllers/push-notifications/get-notification-message.ts b/app/scripts/controllers/push-notifications/get-notification-message.ts new file mode 100644 index 000000000000..dd47b766ce1a --- /dev/null +++ b/app/scripts/controllers/push-notifications/get-notification-message.ts @@ -0,0 +1,71 @@ +import type { NotificationServicesController } from '@metamask/notification-services-controller'; +import { NotificationsServicesPushController } from '@metamask/notification-services-controller'; +import { t as translate } from '../../translate'; + +const t = (...args: Parameters) => translate(...args) ?? ''; + +const translations: NotificationsServicesPushController.Utils.TranslationKeys = + { + pushPlatformNotificationsFundsSentTitle: () => + t('pushPlatformNotificationsFundsSentTitle'), + pushPlatformNotificationsFundsSentDescriptionDefault: () => + t('pushPlatformNotificationsFundsSentDescriptionDefault'), + pushPlatformNotificationsFundsSentDescription: (amount, symbol) => + t('pushPlatformNotificationsFundsSentDescription', amount, symbol), + pushPlatformNotificationsFundsReceivedTitle: () => + t('pushPlatformNotificationsFundsReceivedTitle'), + pushPlatformNotificationsFundsReceivedDescriptionDefault: () => + t('pushPlatformNotificationsFundsReceivedDescriptionDefault'), + pushPlatformNotificationsFundsReceivedDescription: (amount, symbol) => + t('pushPlatformNotificationsFundsReceivedDescription', amount, symbol), + pushPlatformNotificationsSwapCompletedTitle: () => + t('pushPlatformNotificationsSwapCompletedTitle'), + pushPlatformNotificationsSwapCompletedDescription: () => + t('pushPlatformNotificationsSwapCompletedDescription'), + pushPlatformNotificationsNftSentTitle: () => + t('pushPlatformNotificationsNftSentTitle'), + pushPlatformNotificationsNftSentDescription: () => + t('pushPlatformNotificationsNftSentDescription'), + pushPlatformNotificationsNftReceivedTitle: () => + t('pushPlatformNotificationsNftReceivedTitle'), + pushPlatformNotificationsNftReceivedDescription: () => + t('pushPlatformNotificationsNftReceivedDescription'), + pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle: () => + t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), + pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription: () => + t('pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription'), + pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle: () => + t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), + pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription: () => + t( + 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', + ), + pushPlatformNotificationsStakingLidoStakeCompletedTitle: () => + t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), + pushPlatformNotificationsStakingLidoStakeCompletedDescription: () => + t('pushPlatformNotificationsStakingLidoStakeCompletedDescription'), + pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle: () => + t('pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle'), + pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription: + () => + t( + 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', + ), + pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), + pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription'), + pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), + pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription: () => + t('pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription'), + }; + +export function createNotificationMessage( + n: NotificationServicesController.Types.INotification, +) { + return NotificationsServicesPushController.Utils.createOnChainPushNotificationMessage( + n, + translations, + ); +} diff --git a/app/scripts/controllers/push-notifications/index.ts b/app/scripts/controllers/push-notifications/index.ts new file mode 100644 index 000000000000..c71ffdd250db --- /dev/null +++ b/app/scripts/controllers/push-notifications/index.ts @@ -0,0 +1,54 @@ +// We are defining that this file uses a webworker global scope. +// eslint-disable-next-line spaced-comment +/// + +import { NotificationServicesController } from '@metamask/notification-services-controller'; +import ExtensionPlatform from '../../platforms/extension'; +import { getNotificationImage } from './get-notification-image'; +import { createNotificationMessage } from './get-notification-message'; + +type INotification = NotificationServicesController.Types.INotification; + +const sw = self as unknown as ServiceWorkerGlobalScope; +const extensionPlatform = new ExtensionPlatform(); + +export async function onPushNotificationReceived( + notification: INotification, +): Promise { + const notificationMessage = createNotificationMessage(notification); + if (!notificationMessage) { + return; + } + + const registration = sw?.registration; + if (!registration) { + return; + } + + const iconUrl = await getNotificationImage(); + + await registration.showNotification(notificationMessage.title, { + body: notificationMessage.description, + icon: iconUrl, + tag: notification?.id, + data: notification, + }); +} + +export async function onPushNotificationClicked( + event: NotificationEvent, + notification?: INotification, +) { + // Close notification + event.notification.close(); + + // Get Data + const data: INotification = notification ?? event?.notification?.data; + + // Navigate + const destination = `${extensionPlatform.getExtensionURL( + null, + null, + )}#notifications/${data.id}`; + event.waitUntil(sw.clients.openWindow(destination)); +} diff --git a/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts b/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts deleted file mode 100644 index 3ff965b3444f..000000000000 --- a/app/scripts/controllers/push-platform-notifications/mocks/mockResponse.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { REGISTRATION_TOKENS_ENDPOINT } from '../services/endpoints'; -import { LinksResult } from '../services/services'; - -type MockResponse = { - url: string | RegExp; - requestMethod: 'GET' | 'POST' | 'PUT'; - response: unknown; -}; - -export const MOCK_REG_TOKEN = 'REG_TOKEN'; -export const MOCK_LINKS_RESPONSE: LinksResult = { - trigger_ids: ['1', '2', '3'], - registration_tokens: [ - { token: 'reg_token_1', platform: 'portfolio' }, - { token: 'reg_token_2', platform: 'extension' }, - ], -}; - -export function getMockRetrievePushNotificationLinksResponse() { - return { - url: REGISTRATION_TOKENS_ENDPOINT, - requestMethod: 'GET', - response: MOCK_LINKS_RESPONSE, - } satisfies MockResponse; -} - -export function getMockUpdatePushNotificationLinksResponse() { - return { - url: REGISTRATION_TOKENS_ENDPOINT, - requestMethod: 'POST', - response: null, - } satisfies MockResponse; -} - -export const MOCK_FCM_RESPONSE = { - name: '', - token: 'fcm-token', - web: { - endpoint: '', - p256dh: '', - auth: '', - applicationPubKey: '', - }, -}; - -export function getMockCreateFCMRegistrationTokenResponse() { - return { - url: /^https:\/\/fcmregistrations\.googleapis\.com\/v1\/projects\/.*$/u, - requestMethod: 'POST', - response: MOCK_FCM_RESPONSE, - } satisfies MockResponse; -} - -export function getMockDeleteFCMRegistrationTokenResponse() { - return { - url: /^https:\/\/fcmregistrations\.googleapis\.com\/v1\/projects\/.*$/u, - requestMethod: 'POST', - response: {}, - } satisfies MockResponse; -} diff --git a/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts b/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts deleted file mode 100644 index e0c3cb98d0c2..000000000000 --- a/app/scripts/controllers/push-platform-notifications/mocks/mockServices.ts +++ /dev/null @@ -1,36 +0,0 @@ -import nock from 'nock'; -import { - getMockRetrievePushNotificationLinksResponse, - getMockUpdatePushNotificationLinksResponse, -} from './mockResponse'; - -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockEndpointGetPushNotificationLinks(mockReply?: MockReply) { - const mockResponse = getMockRetrievePushNotificationLinksResponse(); - const reply = mockReply ?? { - status: 200, - body: mockResponse.response, - }; - - const mockEndpoint = nock(mockResponse.url) - .get('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockEndpointUpdatePushNotificationLinks(mockReply?: MockReply) { - const mockResponse = getMockUpdatePushNotificationLinksResponse(); - const reply = mockReply ?? { - status: 200, - body: mockResponse.response, - }; - - const mockEndpoint = nock(mockResponse.url).post('').reply(reply.status); - - return mockEndpoint; -} diff --git a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts deleted file mode 100644 index 0b596fdb4992..000000000000 --- a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { ControllerMessenger } from '@metamask/base-controller'; -import type { AuthenticationControllerGetBearerToken } from '../authentication/authentication-controller'; -import { PushPlatformNotificationsController } from './push-platform-notifications'; - -import * as services from './services/services'; -import type { - PushPlatformNotificationsControllerMessenger, - PushPlatformNotificationsControllerState, -} from './push-platform-notifications'; - -const MOCK_JWT = 'mockJwt'; -const MOCK_FCM_TOKEN = 'mockFcmToken'; -const MOCK_TRIGGERS = ['uuid1', 'uuid2']; - -describe('PushPlatformNotificationsController', () => { - if (!process.env.ENABLE_MV3) { - it('No MV2 tests, this functionality is not enabled', () => { - expect(true).toBe(true); - }); - } - - if (process.env.ENABLE_MV3) { - describe('enablePushNotifications', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should update the state with the fcmToken', async () => { - await withController(async ({ controller, messenger }) => { - mockAuthBearerTokenCall(messenger); - jest - .spyOn(services, 'activatePushNotifications') - .mockResolvedValue(MOCK_FCM_TOKEN); - - const unsubscribeMock = jest.fn(); - jest - .spyOn(services, 'listenToPushNotifications') - .mockResolvedValue(unsubscribeMock); - - await controller.enablePushNotifications(MOCK_TRIGGERS); - expect(controller.state.fcmToken).toBe(MOCK_FCM_TOKEN); - - expect(services.listenToPushNotifications).toHaveBeenCalled(); - }); - }); - - it('should fail if a jwt token is not provided', async () => { - await withController(async ({ messenger, controller }) => { - mockAuthBearerTokenCall(messenger).mockResolvedValue( - null as unknown as string, - ); - await expect( - controller.enablePushNotifications([]), - ).rejects.toThrow(); - }); - }); - }); - - describe('disablePushNotifications', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should update the state removing the fcmToken', async () => { - await withController(async ({ messenger, controller }) => { - mockAuthBearerTokenCall(messenger); - await controller.disablePushNotifications(MOCK_TRIGGERS); - expect(controller.state.fcmToken).toBe(''); - }); - }); - - it('should fail if a jwt token is not provided', async () => { - await withController(async ({ messenger, controller }) => { - mockAuthBearerTokenCall(messenger).mockResolvedValue( - null as unknown as string, - ); - await expect( - controller.disablePushNotifications([]), - ).rejects.toThrow(); - }); - }); - }); - - describe('updateTriggerPushNotifications', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should call updateTriggerPushNotifications with the correct parameters', async () => { - await withController(async ({ messenger, controller }) => { - mockAuthBearerTokenCall(messenger); - const spy = jest - .spyOn(services, 'updateTriggerPushNotifications') - .mockResolvedValue({ - isTriggersLinkedToPushNotifications: true, - }); - - await controller.updateTriggerPushNotifications(MOCK_TRIGGERS); - - expect(spy).toHaveBeenCalledWith( - controller.state.fcmToken, - MOCK_JWT, - MOCK_TRIGGERS, - ); - }); - }); - }); - } -}); - -// Test helper functions - -type WithControllerCallback = ({ - controller, - initialState, - messenger, -}: { - controller: PushPlatformNotificationsController; - initialState: PushPlatformNotificationsControllerState; - messenger: PushPlatformNotificationsControllerMessenger; -}) => Promise | ReturnValue; - -function buildMessenger() { - return new ControllerMessenger< - AuthenticationControllerGetBearerToken, - never - >(); -} - -function buildPushPlatformNotificationsControllerMessenger( - messenger = buildMessenger(), -) { - return messenger.getRestricted({ - name: 'PushPlatformNotificationsController', - allowedActions: ['AuthenticationController:getBearerToken'], - allowedEvents: [], - }) as PushPlatformNotificationsControllerMessenger; -} - -async function withController( - fn: WithControllerCallback, -): Promise { - const messenger = buildPushPlatformNotificationsControllerMessenger(); - const controller = new PushPlatformNotificationsController({ - messenger, - state: { fcmToken: '' }, - }); - - return await fn({ - controller, - initialState: controller.state, - messenger, - }); -} - -function mockAuthBearerTokenCall( - messenger: PushPlatformNotificationsControllerMessenger, -) { - type Fn = AuthenticationControllerGetBearerToken['handler']; - const mockAuthGetBearerToken = jest - .fn, Parameters>() - .mockResolvedValue(MOCK_JWT); - - jest.spyOn(messenger, 'call').mockImplementation((...args) => { - const [actionType] = args; - if (actionType === 'AuthenticationController:getBearerToken') { - return mockAuthGetBearerToken(); - } - - throw new Error('MOCK - unsupported messenger call mock'); - }); - - return mockAuthGetBearerToken; -} diff --git a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts deleted file mode 100644 index fa28505bb55d..000000000000 --- a/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { - BaseController, - RestrictedControllerMessenger, - ControllerGetStateAction, -} from '@metamask/base-controller'; -import log from 'loglevel'; - -import type { AuthenticationControllerGetBearerToken } from '../authentication/authentication-controller'; -import type { Notification } from '../metamask-notifications/types/notification/notification'; -import { - activatePushNotifications, - deactivatePushNotifications, - listenToPushNotifications, - updateTriggerPushNotifications, -} from './services/services'; - -const controllerName = 'PushPlatformNotificationsController'; - -export type PushPlatformNotificationsControllerState = { - fcmToken: string; -}; - -export declare type PushPlatformNotificationsControllerEnablePushNotifications = - { - type: `${typeof controllerName}:enablePushNotifications`; - handler: PushPlatformNotificationsController['enablePushNotifications']; - }; - -export declare type PushPlatformNotificationsControllerDisablePushNotifications = - { - type: `${typeof controllerName}:disablePushNotifications`; - handler: PushPlatformNotificationsController['disablePushNotifications']; - }; -export declare type PushPlatformNotificationsControllerUpdateTriggerPushNotifications = - { - type: `${typeof controllerName}:updateTriggerPushNotifications`; - handler: PushPlatformNotificationsController['updateTriggerPushNotifications']; - }; - -export type PushPlatformNotificationsControllerMessengerActions = - | PushPlatformNotificationsControllerEnablePushNotifications - | PushPlatformNotificationsControllerDisablePushNotifications - | PushPlatformNotificationsControllerUpdateTriggerPushNotifications - | ControllerGetStateAction<'state', PushPlatformNotificationsControllerState>; - -type AllowedActions = AuthenticationControllerGetBearerToken; - -export type PushPlatformNotificationsControllerOnNewNotificationEvent = { - type: `${typeof controllerName}:onNewNotifications`; - payload: [Notification]; -}; - -export type PushPlatformNotificationsControllerPushNotificationClicked = { - type: `${typeof controllerName}:pushNotificationClicked`; - payload: [Notification]; -}; - -type AllowedEvents = - | PushPlatformNotificationsControllerOnNewNotificationEvent - | PushPlatformNotificationsControllerPushNotificationClicked; - -export type PushPlatformNotificationsControllerMessenger = - RestrictedControllerMessenger< - typeof controllerName, - PushPlatformNotificationsControllerMessengerActions | AllowedActions, - AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] - >; - -const metadata = { - fcmToken: { - persist: true, - anonymous: true, - }, -}; - -/** - * Manages push notifications for the application, including enabling, disabling, and updating triggers for push notifications. - * This controller integrates with Firebase Cloud Messaging (FCM) to handle the registration and management of push notifications. - * It is responsible for registering and unregistering the service worker that listens for push notifications, - * managing the FCM token, and communicating with the server to register or unregister the device for push notifications. - * Additionally, it provides functionality to update the server with new UUIDs that should trigger push notifications. - * - * @augments {BaseController} - */ -export class PushPlatformNotificationsController extends BaseController< - typeof controllerName, - PushPlatformNotificationsControllerState, - PushPlatformNotificationsControllerMessenger -> { - #pushListenerUnsubscribe: (() => void) | undefined = undefined; - - constructor({ - messenger, - state, - }: { - messenger: PushPlatformNotificationsControllerMessenger; - state: PushPlatformNotificationsControllerState; - }) { - super({ - messenger, - metadata, - name: controllerName, - state: { - fcmToken: state?.fcmToken || '', - }, - }); - - this.#registerMessageHandlers(); - } - - #registerMessageHandlers(): void { - this.messagingSystem.registerActionHandler( - 'PushPlatformNotificationsController:enablePushNotifications', - this.enablePushNotifications.bind(this), - ); - this.messagingSystem.registerActionHandler( - 'PushPlatformNotificationsController:disablePushNotifications', - this.disablePushNotifications.bind(this), - ); - this.messagingSystem.registerActionHandler( - 'PushPlatformNotificationsController:updateTriggerPushNotifications', - this.updateTriggerPushNotifications.bind(this), - ); - } - - async #getAndAssertBearerToken() { - const bearerToken = await this.messagingSystem.call( - 'AuthenticationController:getBearerToken', - ); - if (!bearerToken) { - log.error( - 'Failed to enable push notifications: BearerToken token is missing.', - ); - throw new Error('BearerToken token is missing'); - } - - return bearerToken; - } - - /** - * Enables push notifications for the application. - * - * This method sets up the necessary infrastructure for handling push notifications by: - * 1. Registering the service worker to listen for messages. - * 2. Fetching the Firebase Cloud Messaging (FCM) token from Firebase. - * 3. Sending the FCM token to the server responsible for sending notifications, to register the device. - * - * @param UUIDs - An array of UUIDs to enable push notifications for. - */ - public async enablePushNotifications(UUIDs: string[]) { - // TEMP: disabling push notifications if browser does not support MV3. - // Will need work to support firefox on MV2 - if (!process.env.ENABLE_MV3) { - return; - } - - const bearerToken = await this.#getAndAssertBearerToken(); - - try { - // Activate Push Notifications - const regToken = await activatePushNotifications(bearerToken, UUIDs); - - if (!regToken) { - return; - } - - // Listen to push notifications - this.#pushListenerUnsubscribe ??= await listenToPushNotifications( - (n) => - this.messagingSystem.publish( - 'PushPlatformNotificationsController:onNewNotifications', - n, - ), - (n) => - this.messagingSystem.publish( - 'PushPlatformNotificationsController:pushNotificationClicked', - n, - ), - ); - - // Update state - this.update((state) => { - state.fcmToken = regToken; - }); - } catch (error) { - log.error('Failed to enable push notifications:', error); - throw new Error('Failed to enable push notifications'); - } - } - - /** - * Disables push notifications for the application. - * This method handles the process of disabling push notifications by: - * 1. Unregistering the service worker to stop listening for messages. - * 2. Sending a request to the server to unregister the device using the FCM token. - * 3. Removing the FCM token from the state to complete the process. - * - * @param UUIDs - An array of UUIDs for which push notifications should be disabled. - */ - public async disablePushNotifications(UUIDs: string[]) { - // TEMP: disabling push notifications if browser does not support MV3. - // Will need work to support firefox on MV2 - if (!process.env.ENABLE_MV3) { - return; - } - - const bearerToken = await this.#getAndAssertBearerToken(); - let isPushNotificationsDisabled: boolean; - - try { - // Send a request to the server to unregister the token/device - isPushNotificationsDisabled = await deactivatePushNotifications( - this.state.fcmToken, - bearerToken, - UUIDs, - ); - } catch (error) { - const errorMessage = `Failed to disable push notifications: ${error}`; - log.error(errorMessage); - throw new Error(errorMessage); - } - - // Remove the FCM token from the state - if (!isPushNotificationsDisabled) { - return; - } - - // Unsubscribe from push notifications - this.#pushListenerUnsubscribe?.(); - - // Update State - if (isPushNotificationsDisabled) { - this.update((state) => { - state.fcmToken = ''; - }); - } - } - - /** - * Updates the triggers for push notifications. - * This method is responsible for updating the server with the new set of UUIDs that should trigger push notifications. - * It uses the current FCM token and a BearerToken for authentication. - * - * @param UUIDs - An array of UUIDs that should trigger push notifications. - */ - public async updateTriggerPushNotifications(UUIDs: string[]) { - // TEMP: disabling push notifications if browser does not support MV3. - // Will need work to support firefox on MV2 - if (!process.env.ENABLE_MV3) { - return; - } - - const bearerToken = await this.#getAndAssertBearerToken(); - - try { - const { fcmToken } = await updateTriggerPushNotifications( - this.state.fcmToken, - bearerToken, - UUIDs, - ); - - // update the state with the new FCM token - if (fcmToken) { - this.update((state) => { - state.fcmToken = fcmToken; - }); - } - } catch (error) { - const errorMessage = `Failed to update triggers for push notifications: ${error}`; - log.error(errorMessage); - throw new Error(errorMessage); - } - } -} diff --git a/app/scripts/controllers/push-platform-notifications/services/endpoints.ts b/app/scripts/controllers/push-platform-notifications/services/endpoints.ts deleted file mode 100644 index ff6520eb03a5..000000000000 --- a/app/scripts/controllers/push-platform-notifications/services/endpoints.ts +++ /dev/null @@ -1,2 +0,0 @@ -const url = process.env.PUSH_NOTIFICATIONS_SERVICE_URL; -export const REGISTRATION_TOKENS_ENDPOINT = `${url}/v1/link`; diff --git a/app/scripts/controllers/push-platform-notifications/services/services.test.ts b/app/scripts/controllers/push-platform-notifications/services/services.test.ts deleted file mode 100644 index 413c2d28caf8..000000000000 --- a/app/scripts/controllers/push-platform-notifications/services/services.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as FirebaseApp from 'firebase/app'; -import * as FirebaseMessaging from 'firebase/messaging'; -import * as FirebaseMessagingSW from 'firebase/messaging/sw'; -import { - mockEndpointGetPushNotificationLinks, - mockEndpointUpdatePushNotificationLinks, -} from '../mocks/mockServices'; -import * as services from './services'; - -jest.mock('firebase/app'); -jest.mock('firebase/messaging'); -jest.mock('firebase/messaging/sw'); - -const MOCK_REG_TOKEN = 'REG_TOKEN'; -const MOCK_NEW_REG_TOKEN = 'NEW_REG_TOKEN'; -const MOCK_TRIGGERS = ['1', '2', '3']; -const MOCK_JWT = 'MOCK_JWT'; - -describe('PushPlatformNotificationsServices', () => { - describe('getPushNotificationLinks', () => { - it('Should return reg token links', async () => { - const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks(); - const res = await services.getPushNotificationLinks(MOCK_JWT); - - expect(mockGetLinksEndpoint.isDone()).toBe(true); - expect(res).toBeDefined(); - expect(res?.trigger_ids).toBeDefined(); - expect(res?.registration_tokens).toBeDefined(); - }); - - it('Should return null if api call fails', async () => { - const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks({ - status: 500, - }); - const res = await services.getPushNotificationLinks(MOCK_JWT); - - expect(mockGetLinksEndpoint.isDone()).toBe(true); - expect(res).toBeNull(); - }); - }); - - describe('updateLinksAPI', () => { - it('Should return true if links are updated', async () => { - const mockUpdateLinksEndpoint = mockEndpointUpdatePushNotificationLinks(); - - const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ - { token: MOCK_NEW_REG_TOKEN, platform: 'extension' }, - ]); - - expect(mockUpdateLinksEndpoint.isDone()).toBe(true); - expect(res).toBe(true); - }); - - it('Should return false if links are not updated', async () => { - mockEndpointUpdatePushNotificationLinks({ status: 500 }); - - const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ - { token: MOCK_NEW_REG_TOKEN, platform: 'extension' }, - ]); - - expect(res).toBe(false); - }); - }); - - describe('activatePushNotifications()', () => { - it('should append registration token when enabling push', async () => { - arrangeEndpoints(); - arrangeFCMMocks(); - - const res = await services.activatePushNotifications( - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toBe(MOCK_NEW_REG_TOKEN); - }); - - it('should fail if unable to get existing notification links', async () => { - mockEndpointGetPushNotificationLinks({ status: 500 }); - - const res = await services.activatePushNotifications( - MOCK_JWT, - MOCK_TRIGGERS, - ); - expect(res).toBeNull(); - }); - - it('should fail if unable to create new reg token', async () => { - arrangeEndpoints(); - const fcmMocks = arrangeFCMMocks(); - fcmMocks.getToken.mockRejectedValue(new Error('MOCK ERROR')); - const res = await services.activatePushNotifications( - MOCK_JWT, - MOCK_TRIGGERS, - ); - expect(res).toBeNull(); - }); - - it('should silently fail and return if failed to activate push notifications', async () => { - mockEndpointGetPushNotificationLinks(); - mockEndpointUpdatePushNotificationLinks({ status: 500 }); - arrangeFCMMocks(); - const res = await services.activatePushNotifications( - MOCK_JWT, - MOCK_TRIGGERS, - ); - - // We return the registration token, but we haven't updating the links. - // This can be redone at a later invocation (e.g. when the extension is re-initialized or notification setting changes) - expect(res).toBe(MOCK_NEW_REG_TOKEN); - }); - }); - - describe('deactivatePushNotifications()', () => { - it('should fail if unable to get existing notification links', async () => { - mockEndpointGetPushNotificationLinks({ status: 500 }); - - const res = await services.deactivatePushNotifications( - MOCK_REG_TOKEN, - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toBe(false); - }); - - it('should fail if unable to update links', async () => { - mockEndpointGetPushNotificationLinks(); - mockEndpointUpdatePushNotificationLinks({ status: 500 }); - - const res = await services.deactivatePushNotifications( - MOCK_REG_TOKEN, - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toBe(false); - }); - - it('should fail if unable to delete reg token', async () => { - arrangeEndpoints(); - const fcmMocks = arrangeFCMMocks(); - fcmMocks.deleteToken.mockRejectedValue(new Error('MOCK FAIL')); - - const res = await services.deactivatePushNotifications( - MOCK_REG_TOKEN, - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toBe(false); - }); - }); - - describe('updateTriggerPushNotifications()', () => { - it('should update triggers for push notifications', async () => { - arrangeEndpoints(); - - const res = await services.updateTriggerPushNotifications( - MOCK_REG_TOKEN, - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toEqual({ - isTriggersLinkedToPushNotifications: true, - fcmToken: expect.any(String), - }); - }); - - it('should fail if unable to update triggers', async () => { - mockEndpointGetPushNotificationLinks(); - mockEndpointUpdatePushNotificationLinks({ status: 500 }); - - const res = await services.updateTriggerPushNotifications( - MOCK_REG_TOKEN, - MOCK_JWT, - MOCK_TRIGGERS, - ); - - expect(res).toEqual({ - isTriggersLinkedToPushNotifications: false, - fcmToken: expect.any(String), - }); - }); - }); - - function arrangeEndpoints() { - const mockGetLinksEndpoint = mockEndpointGetPushNotificationLinks(); - const mockUpdateLinksEndpoint = mockEndpointUpdatePushNotificationLinks(); - - return { - mockGetLinksEndpoint, - mockUpdateLinksEndpoint, - }; - } - - function arrangeFCMMocks() { - const mockFirebaseApp: FirebaseApp.FirebaseApp = { - name: '', - automaticDataCollectionEnabled: false, - options: {}, - }; - const mockFirebaseMessaging: FirebaseMessagingSW.Messaging = { - app: mockFirebaseApp, - }; - - jest.spyOn(FirebaseApp, 'getApp').mockReturnValue(mockFirebaseApp); - jest.spyOn(FirebaseApp, 'initializeApp').mockReturnValue(mockFirebaseApp); - - const getMessaging = jest - .spyOn(FirebaseMessagingSW, 'getMessaging') - .mockReturnValue(mockFirebaseMessaging); - const onBackgroundMessage = jest - .spyOn(FirebaseMessagingSW, 'onBackgroundMessage') - .mockReturnValue(() => jest.fn()); - - const getToken = jest - .spyOn(FirebaseMessaging, 'getToken') - .mockResolvedValue(MOCK_NEW_REG_TOKEN); - const deleteToken = jest - .spyOn(FirebaseMessaging, 'deleteToken') - .mockResolvedValue(true); - - return { - getMessaging, - onBackgroundMessage, - getToken, - deleteToken, - }; - } -}); diff --git a/app/scripts/controllers/push-platform-notifications/services/services.ts b/app/scripts/controllers/push-platform-notifications/services/services.ts deleted file mode 100644 index 392cd22ad051..000000000000 --- a/app/scripts/controllers/push-platform-notifications/services/services.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { getToken, deleteToken } from 'firebase/messaging'; -import type { FirebaseApp } from 'firebase/app'; -import { getApp, initializeApp } from 'firebase/app'; -import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw'; -import type { Messaging, MessagePayload } from 'firebase/messaging/sw'; -import log from 'loglevel'; -import { - onNotificationClick, - onPushNotification, -} from '../utils/get-notification-message'; -import { - Notification, - NotificationUnion, -} from '../../metamask-notifications/types/types'; -import { processNotification } from '../../metamask-notifications/processors/process-notifications'; -import { REGISTRATION_TOKENS_ENDPOINT } from './endpoints'; - -const sw = self as unknown as ServiceWorkerGlobalScope; - -export type RegToken = { - token: string; - platform: 'extension' | 'mobile' | 'portfolio'; -}; - -export type LinksResult = { - trigger_ids: string[]; - registration_tokens: RegToken[]; -}; - -/** - * Attempts to retrieve an existing Firebase app instance. If no instance exists, it initializes a new app with the provided configuration. - * - * @returns The Firebase app instance. - */ -async function createFirebaseApp(): Promise { - try { - return getApp(); - } catch { - const firebaseConfig = { - apiKey: process.env.FIREBASE_API_KEY, - authDomain: process.env.FIREBASE_AUTH_DOMAIN, - storageBucket: process.env.FIREBASE_STORAGE_BUCKET, - projectId: process.env.FIREBASE_PROJECT_ID, - messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.FIREBASE_APP_ID, - measurementId: process.env.FIREBASE_MEASUREMENT_ID, - }; - return initializeApp(firebaseConfig); - } -} - -/** - * Retrieves the Firebase Messaging service instance. - * - * This function first ensures a Firebase app instance is created or retrieved by calling `createFirebaseApp`. - * It then initializes and returns the Firebase Messaging service associated with the Firebase app. - * - * @returns A promise that resolves with the Firebase Messaging service instance. - */ -async function getFirebaseMessaging(): Promise { - const app = await createFirebaseApp(); - return getMessaging(app); -} - -/** - * Creates a registration token for Firebase Cloud Messaging. - * - * @returns A promise that resolves with the registration token or null if an error occurs. - */ -async function createRegToken(): Promise { - try { - const messaging = await getFirebaseMessaging(); - const token = await getToken(messaging, { - serviceWorkerRegistration: sw.registration, - vapidKey: process.env.VAPID_KEY, - }); - return token; - } catch { - return null; - } -} - -/** - * Deletes the Firebase Cloud Messaging registration token. - * - * @returns A promise that resolves with true if the token was successfully deleted, false otherwise. - */ -async function deleteRegToken(): Promise { - try { - const messaging = await getFirebaseMessaging(); - await deleteToken(messaging); - return true; - } catch (error) { - return false; - } -} - -/** - * Fetches push notification links from a remote endpoint using a BearerToken for authorization. - * - * @param bearerToken - The JSON Web Token used for authorization. - * @returns A promise that resolves with the links result or null if an error occurs. - */ -export async function getPushNotificationLinks( - bearerToken: string, -): Promise { - try { - const response = await fetch(REGISTRATION_TOKENS_ENDPOINT, { - headers: { Authorization: `Bearer ${bearerToken}` }, - }); - if (!response.ok) { - log.error('Failed to fetch the push notification links'); - throw new Error('Failed to fetch the push notification links'); - } - return response.json() as Promise; - } catch (error) { - log.error('Failed to fetch the push notification links', error); - return null; - } -} - -/** - * Updates the push notification links on a remote API. - * - * @param bearerToken - The JSON Web Token used for authorization. - * @param triggers - An array of trigger identifiers. - * @param regTokens - An array of registration tokens. - * @returns A promise that resolves with true if the update was successful, false otherwise. - */ -export async function updateLinksAPI( - bearerToken: string, - triggers: string[], - regTokens: RegToken[], -): Promise { - try { - const body: LinksResult = { - trigger_ids: triggers, - registration_tokens: regTokens, - }; - const response = await fetch(REGISTRATION_TOKENS_ENDPOINT, { - method: 'POST', - headers: { - Authorization: `Bearer ${bearerToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - return response.status === 200; - } catch { - return false; - } -} - -/** - * Enables push notifications by registering the device and linking triggers. - * - * @param bearerToken - The JSON Web Token used for authorization. - * @param triggers - An array of trigger identifiers. - * @returns A promise that resolves with an object containing the success status and the BearerToken token. - */ -export async function activatePushNotifications( - bearerToken: string, - triggers: string[], -): Promise { - const notificationLinks = await getPushNotificationLinks(bearerToken); - - if (!notificationLinks) { - return null; - } - - const regToken = await createRegToken().catch(() => null); - if (!regToken) { - return null; - } - - const newRegTokens = new Set(notificationLinks.registration_tokens); - newRegTokens.add({ token: regToken, platform: 'extension' }); - - await updateLinksAPI(bearerToken, triggers, Array.from(newRegTokens)); - return regToken; -} - -export async function listenToPushNotifications( - onNewNotification: (notification: Notification) => void, - onNotificationClicked: (notification: Notification) => void, -): Promise<() => void> { - /* - Push notifications require 2 listeners that need tracking (when creating and for tearing down): - 1. handling receiving a push notification (and the content we want to display) - 2. handling when a user clicks on a push notification - */ - - // Firebase - const messaging = await getFirebaseMessaging(); - const unsubscribePushNotifications = onBackgroundMessage( - messaging, - async (payload: MessagePayload): Promise => { - try { - const data = payload?.data?.data - ? JSON.parse(payload?.data?.data) - : undefined; - - // if the payload does not contain data, do nothing - if (!data) { - return; - } - - const notificationData = { - ...data, - type: data?.type ?? data?.data?.kind, - } as NotificationUnion; - - const notification = processNotification(notificationData); - onNewNotification(notification); - - await onPushNotification(notification); - } catch (error) { - // Do Nothing, cannot parse a bad notification - log.error('Unable to send push notification:', { - notification: payload?.data?.data, - error, - }); - throw new Error('Unable to send push notification'); - } - }, - ); - - // Notification Click Listener - const notificationClickHandler = (event: NotificationEvent) => { - onNotificationClick(event, onNotificationClicked); - }; - sw.addEventListener('notificationclick', notificationClickHandler); - const unsubscribeNotificationClicks = () => - sw.removeEventListener('notificationclick', notificationClickHandler); - - const unsubscribe = () => { - unsubscribePushNotifications(); - unsubscribeNotificationClicks(); - }; - - return unsubscribe; -} - -/** - * Handle Clicking Notifications. - */ - -/** - * Disables push notifications by removing the registration token and unlinking triggers. - * - * @param regToken - The registration token to be removed. - * @param bearerToken - The JSON Web Token used for authorization. - * @param triggers - An array of trigger identifiers to be unlinked. - * @returns A promise that resolves with true if notifications were successfully disabled, false otherwise. - */ -export async function deactivatePushNotifications( - regToken: string, - bearerToken: string, - triggers: string[], -): Promise { - // if we don't have a reg token, then we can early return - if (!regToken) { - return true; - } - - const notificationLinks = await getPushNotificationLinks(bearerToken); - if (!notificationLinks) { - return false; - } - - const filteredRegTokens = notificationLinks.registration_tokens.filter( - (r) => r.token !== regToken, - ); - - const isTokenRemovedFromAPI = await updateLinksAPI( - bearerToken, - triggers, - filteredRegTokens, - ); - if (!isTokenRemovedFromAPI) { - return false; - } - - const isTokenRemovedFromFCM = await deleteRegToken(); - if (!isTokenRemovedFromFCM) { - return false; - } - - return true; -} - -/** - * Updates the triggers linked to push notifications for a given registration token. - * If the provided registration token does not exist or is not in the current set of registration tokens, - * a new registration token is created and used for the update. - * - * @param regToken - The registration token to update triggers for. If null or not found, a new token will be created. - * @param bearerToken - The JSON Web Token used for authorization. - * @param triggers - An array of new trigger identifiers to link. - * @returns A promise that resolves with an object containing: - * - isTriggersLinkedToPushNotifications: boolean indicating if the triggers were successfully updated. - * - fcmToken: the new or existing Firebase Cloud Messaging token used for the update, if applicable. - */ -export async function updateTriggerPushNotifications( - regToken: string, - bearerToken: string, - triggers: string[], -): Promise<{ - isTriggersLinkedToPushNotifications: boolean; - fcmToken?: string | null; -}> { - const notificationLinks = await getPushNotificationLinks(bearerToken); - if (!notificationLinks) { - return { isTriggersLinkedToPushNotifications: false }; - } - // Create new registration token if doesn't exist - const hasRegToken = Boolean( - regToken && - notificationLinks.registration_tokens.some((r) => r.token === regToken), - ); - - let newRegToken: string | null = null; - if (!hasRegToken) { - await deleteRegToken(); - newRegToken = await createRegToken(); - if (!newRegToken) { - throw new Error('Failed to create a new registration token'); - } - notificationLinks.registration_tokens.push({ - token: newRegToken, - platform: 'extension', - }); - } - - const isTriggersLinkedToPushNotifications = await updateLinksAPI( - bearerToken, - triggers, - notificationLinks.registration_tokens, - ); - - return { - isTriggersLinkedToPushNotifications, - fcmToken: newRegToken ?? null, - }; -} diff --git a/app/scripts/controllers/push-platform-notifications/types/firebase.ts b/app/scripts/controllers/push-platform-notifications/types/firebase.ts deleted file mode 100644 index 91dc2c2e4d6f..000000000000 --- a/app/scripts/controllers/push-platform-notifications/types/firebase.ts +++ /dev/null @@ -1,46 +0,0 @@ -export declare type Messaging = { - app: FirebaseApp; -}; - -export declare type FirebaseApp = { - readonly name: string; - readonly options: FirebaseOptions; - automaticDataCollectionEnabled: boolean; -}; - -export declare type FirebaseOptions = { - apiKey?: string; - authDomain?: string; - databaseURL?: string; - projectId?: string; - storageBucket?: string; - messagingSenderId?: string; - appId?: string; - measurementId?: string; -}; - -export type NotificationPayload = { - title?: string; - body?: string; - image?: string; - icon?: string; -}; - -export type FcmOptions = { - link?: string; - analyticsLabel?: string; -}; - -export type MessagePayload = { - notification?: NotificationPayload; - data?: { [key: string]: string }; - fcmOptions?: FcmOptions; - from: string; - collapseKey: string; - messageId: string; -}; - -export type GetTokenOptions = { - vapidKey?: string; - serviceWorkerRegistration?: ServiceWorkerRegistration; -}; diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts deleted file mode 100644 index 2ffbe89fdd9e..000000000000 --- a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - formatAmount, - getAmount, - getLeadingZeroCount, -} from './get-notification-data'; - -describe('getNotificationData - formatAmount() tests', () => { - test('Should format large numbers', () => { - expect(formatAmount(1000)).toBe('1K'); - expect(formatAmount(1500)).toBe('1.5K'); - expect(formatAmount(1000000)).toBe('1M'); - expect(formatAmount(1000000000)).toBe('1B'); - expect(formatAmount(1000000000000)).toBe('1T'); - expect(formatAmount(1234567)).toBe('1.23M'); - }); - - test('Should format smaller numbers (<1000) with custom decimal place', () => { - const formatOptions = { decimalPlaces: 18 }; - expect(formatAmount(100.0012, formatOptions)).toBe('100.0012'); - expect(formatAmount(100.001200001, formatOptions)).toBe('100.001200001'); - expect(formatAmount(1e-18, formatOptions)).toBe('0.000000000000000001'); - expect(formatAmount(1e-19, formatOptions)).toBe('0'); // number is smaller than decimals given, hence 0 - }); - - test('Should format small numbers (<1000) up to 4 decimals otherwise uses ellipses', () => { - const formatOptions = { shouldEllipse: true }; - expect(formatAmount(100.1, formatOptions)).toBe('100.1'); - expect(formatAmount(100.01, formatOptions)).toBe('100.01'); - expect(formatAmount(100.001, formatOptions)).toBe('100.001'); - expect(formatAmount(100.0001, formatOptions)).toBe('100.0001'); - expect(formatAmount(100.00001, formatOptions)).toBe('100.0000...'); // since number is has >4 decimals, it will be truncated - expect(formatAmount(0.00001, formatOptions)).toBe('0.0000...'); // since number is has >4 decimals, it will be truncated - }); - - test('Should format small numbers (<1000) to custom decimal places and ellipse', () => { - const formatOptions = { decimalPlaces: 2, shouldEllipse: true }; - expect(formatAmount(100.1, formatOptions)).toBe('100.1'); - expect(formatAmount(100.01, formatOptions)).toBe('100.01'); - expect(formatAmount(100.001, formatOptions)).toBe('100.00...'); - expect(formatAmount(100.0001, formatOptions)).toBe('100.00...'); - expect(formatAmount(100.00001, formatOptions)).toBe('100.00...'); // since number is has >2 decimals, it will be truncated - expect(formatAmount(0.00001, formatOptions)).toBe('0.00...'); // since number is has >2 decimals, it will be truncated - }); -}); - -describe('getNotificationData - getAmount() tests', () => { - test('Should get formatted amount for larger numbers', () => { - expect(getAmount('1', '2')).toBe('0.01'); - expect(getAmount('10', '2')).toBe('0.1'); - expect(getAmount('100', '2')).toBe('1'); - expect(getAmount('1000', '2')).toBe('10'); - expect(getAmount('10000', '2')).toBe('100'); - expect(getAmount('100000', '2')).toBe('1K'); - expect(getAmount('1000000', '2')).toBe('10K'); - }); - test('Should get formatted amount for small/decimal numbers', () => { - const formatOptions = { shouldEllipse: true }; - expect(getAmount('100000', '5', formatOptions)).toBe('1'); - expect(getAmount('100001', '5', formatOptions)).toBe('1.0000...'); - expect(getAmount('10000', '5', formatOptions)).toBe('0.1'); - expect(getAmount('1000', '5', formatOptions)).toBe('0.01'); - expect(getAmount('100', '5', formatOptions)).toBe('0.001'); - expect(getAmount('10', '5', formatOptions)).toBe('0.0001'); - expect(getAmount('1', '5', formatOptions)).toBe('0.0000...'); - }); -}); - -describe('getNotificationData - getLeadingZeroCount() tests', () => { - test('Should handle all test cases', () => { - expect(getLeadingZeroCount(0)).toBe(0); - expect(getLeadingZeroCount(-1)).toBe(0); - expect(getLeadingZeroCount(1e-1)).toBe(0); - - expect(getLeadingZeroCount('1.01')).toBe(1); - expect(getLeadingZeroCount('3e-2')).toBe(1); - expect(getLeadingZeroCount('100.001e1')).toBe(1); - - expect(getLeadingZeroCount('0.00120043')).toBe(2); - }); -}); diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts deleted file mode 100644 index f95149c54c19..000000000000 --- a/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { BigNumber } from 'bignumber.js'; -import { calcTokenAmount } from '../../../../../shared/lib/transactions-controller-utils'; - -type FormatOptions = { - decimalPlaces?: number; - shouldEllipse?: boolean; -}; -const defaultFormatOptions = { - decimalPlaces: 4, -}; - -/** - * Calculates the number of leading zeros in the fractional part of a number. - * - * This function converts a number or a string representation of a number into - * its decimal form and then counts the number of leading zeros present in the - * fractional part of the number. This is useful for determining the precision - * of very small numbers. - * - * @param num - The number to analyze, which can be in the form - * of a number or a string. - * @returns The count of leading zeros in the fractional part of the number. - */ -export const getLeadingZeroCount = (num: number | string) => { - const numToString = new BigNumber(num, 10).toString(10); - const fractionalPart = numToString.split('.')[1] ?? ''; - return fractionalPart.match(/^0*/u)?.[0]?.length || 0; -}; - -/** - * This formats a number using Intl - * It abbreviates large numbers (using K, M, B, T) - * And abbreviates small numbers in 2 ways: - * - Will format to the given number of decimal places - * - Will format up to 4 decimal places - * - Will ellipse the number if longer than given decimal places - * - * @param numericAmount - The number to format - * @param opts - The options to use when formatting - * @returns The formatted number - */ -export const formatAmount = (numericAmount: number, opts?: FormatOptions) => { - // create options with defaults - const options = { ...defaultFormatOptions, ...opts }; - - const leadingZeros = getLeadingZeroCount(numericAmount); - const isDecimal = numericAmount.toString().includes('.') || leadingZeros > 0; - const isLargeNumber = numericAmount > 999; - - const handleShouldEllipse = (decimalPlaces: number) => - Boolean(options?.shouldEllipse) && leadingZeros >= decimalPlaces; - - if (isLargeNumber) { - return Intl.NumberFormat('en-US', { - notation: 'compact', - compactDisplay: 'short', - maximumFractionDigits: 2, - }).format(numericAmount); - } - - if (isDecimal) { - const ellipse = handleShouldEllipse(options.decimalPlaces); - const formattedValue = Intl.NumberFormat('en-US', { - minimumFractionDigits: ellipse ? options.decimalPlaces : undefined, - maximumFractionDigits: options.decimalPlaces, - }).format(numericAmount); - - return ellipse ? `${formattedValue}...` : formattedValue; - } - - // Default to showing the raw amount - return numericAmount.toString(); -}; - -export const getAmount = ( - amount: string, - decimals: string, - options?: FormatOptions, -) => { - if (!amount || !decimals) { - return ''; - } - - const numericAmount = calcTokenAmount( - amount, - parseFloat(decimals), - ).toNumber(); - - return formatAmount(numericAmount, options); -}; diff --git a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts deleted file mode 100644 index 514c5b40fadc..000000000000 --- a/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts +++ /dev/null @@ -1,264 +0,0 @@ -// We are defining that this file uses a webworker global scope. -// eslint-disable-next-line spaced-comment -/// - -import { CHAIN_SYMBOLS } from '../../metamask-notifications/constants/notification-schema'; -import type { TRIGGER_TYPES } from '../../metamask-notifications/constants/notification-schema'; -import type { OnChainRawNotification } from '../../metamask-notifications/types/on-chain-notification/on-chain-notification'; -import { t } from '../../../translate'; -import type { Notification } from '../../metamask-notifications/types/types'; -import ExtensionPlatform from '../../../platforms/extension'; -import { getAmount, formatAmount } from './get-notification-data'; -import { getNotificationImage } from './get-notification-image'; - -type PushNotificationMessage = { - title: string; - description: string; -}; - -type NotificationMessage = { - title: string | null; - defaultDescription: string | null; - getDescription?: (n: N) => string | null; -}; - -type NotificationMessageDict = { - [K in TRIGGER_TYPES]?: NotificationMessage< - Extract - >; -}; - -const sw = self as unknown as ServiceWorkerGlobalScope; -const extensionPlatform = new ExtensionPlatform(); - -function getChainSymbol(chainId: number) { - return CHAIN_SYMBOLS[chainId] ?? null; -} - -export async function onPushNotification( - notification: Notification, -): Promise { - const notificationMessage = createNotificationMessage(notification); - if (!notificationMessage) { - return; - } - - const registration = sw?.registration; - if (!registration) { - return; - } - - const iconUrl = await getNotificationImage(); - - await registration.showNotification(notificationMessage.title, { - body: notificationMessage.description, - icon: iconUrl, - tag: notification?.id, - data: notification, - }); -} - -export async function onNotificationClick( - event: NotificationEvent, - emitEvent?: (n: Notification) => void, -) { - // Close notification - event.notification.close(); - - // Get Data - const data: Notification = event?.notification?.data; - emitEvent?.(data); - - // Navigate - const destination = `${extensionPlatform.getExtensionURL( - null, - null, - )}#notifications/${data.id}`; - event.waitUntil(sw.clients.openWindow(destination)); -} - -export function isOnChainNotification(n: unknown): n is OnChainRawNotification { - const assumed = n as OnChainRawNotification; - - // We don't have a validation/parsing library to check all possible types of an on chain notification - // It is safe enough just to check "some" fields, and catch any errors down the line if the shape is bad. - const isValidEnoughToBeOnChainNotification = [ - assumed?.id, - assumed?.data, - assumed?.trigger_id, - ].every((field) => field !== undefined); - return isValidEnoughToBeOnChainNotification; -} - -const notificationMessageDict: NotificationMessageDict = { - erc20_sent: { - title: t('pushPlatformNotificationsFundsSentTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsSentDescriptionDefault', - ), - getDescription: (n) => { - const symbol = n?.data?.token?.symbol; - const tokenAmount = n?.data?.token?.amount; - const tokenDecimals = n?.data?.token?.decimals; - if (!symbol || !tokenAmount || !tokenDecimals) { - return null; - } - - const amount = getAmount(tokenAmount, tokenDecimals, { - shouldEllipse: true, - }); - return t('pushPlatformNotificationsFundsSentDescription', amount, symbol); - }, - }, - eth_sent: { - title: t('pushPlatformNotificationsFundsSentTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsSentDescriptionDefault', - ), - getDescription: (n) => { - const symbol = getChainSymbol(n?.chain_id); - const tokenAmount = n?.data?.amount?.eth; - if (!symbol || !tokenAmount) { - return null; - } - - const amount = formatAmount(parseFloat(tokenAmount), { - shouldEllipse: true, - }); - return t('pushPlatformNotificationsFundsSentDescription', amount, symbol); - }, - }, - erc20_received: { - title: t('pushPlatformNotificationsFundsReceivedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsReceivedDescriptionDefault', - ), - getDescription: (n) => { - const symbol = n?.data?.token?.symbol; - const tokenAmount = n?.data?.token?.amount; - const tokenDecimals = n?.data?.token?.decimals; - if (!symbol || !tokenAmount || !tokenDecimals) { - return null; - } - - const amount = getAmount(tokenAmount, tokenDecimals, { - shouldEllipse: true, - }); - return t( - 'pushPlatformNotificationsFundsReceivedDescription', - amount, - symbol, - ); - }, - }, - eth_received: { - title: t('pushPlatformNotificationsFundsReceivedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsFundsReceivedDescriptionDefault', - ), - getDescription: (n) => { - const symbol = getChainSymbol(n?.chain_id); - const tokenAmount = n?.data?.amount?.eth; - if (!symbol || !tokenAmount) { - return null; - } - - const amount = formatAmount(parseFloat(tokenAmount), { - shouldEllipse: true, - }); - return t( - 'pushPlatformNotificationsFundsReceivedDescription', - amount, - symbol, - ); - }, - }, - metamask_swap_completed: { - title: t('pushPlatformNotificationsSwapCompletedTitle'), - defaultDescription: t('pushPlatformNotificationsSwapCompletedDescription'), - }, - erc721_sent: { - title: t('pushPlatformNotificationsNftSentTitle'), - defaultDescription: t('pushPlatformNotificationsNftSentDescription'), - }, - erc1155_sent: { - title: t('pushPlatformNotificationsNftSentTitle'), - defaultDescription: t('pushPlatformNotificationsNftSentDescription'), - }, - erc721_received: { - title: t('pushPlatformNotificationsNftReceivedTitle'), - defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), - }, - erc1155_received: { - title: t('pushPlatformNotificationsNftReceivedTitle'), - defaultDescription: t('pushPlatformNotificationsNftReceivedDescription'), - }, - rocketpool_stake_completed: { - title: t('pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription', - ), - }, - rocketpool_unstake_completed: { - title: t('pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription', - ), - }, - lido_stake_completed: { - title: t('pushPlatformNotificationsStakingLidoStakeCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoStakeCompletedDescription', - ), - }, - lido_stake_ready_to_be_withdrawn: { - title: t( - 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle', - ), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription', - ), - }, - lido_withdrawal_requested: { - title: t('pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription', - ), - }, - lido_withdrawal_completed: { - title: t('pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle'), - defaultDescription: t( - 'pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription', - ), - }, -}; - -export function createNotificationMessage( - n: Notification, -): PushNotificationMessage | null { - if (!n?.type) { - return null; - } - const notificationMessage = notificationMessageDict[n.type] as - | NotificationMessage - | undefined; - - if (!notificationMessage) { - return null; - } - - let description: string | null = null; - try { - description = - notificationMessage?.getDescription?.(n) ?? - notificationMessage.defaultDescription ?? - null; - } catch (e) { - description = notificationMessage.defaultDescription ?? null; - } - - return { - title: notificationMessage.title ?? '', // Ensure title is always a string - description: description ?? '', // Fallback to empty string if null - }; -} diff --git a/app/scripts/controllers/swaps.types.ts b/app/scripts/controllers/swaps.types.ts deleted file mode 100644 index 0164e253678e..000000000000 --- a/app/scripts/controllers/swaps.types.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'; -import type { ChainId } from '@metamask/controller-utils'; -import { GasFeeState } from '@metamask/gas-fee-controller'; -import { ProviderConfig } from '@metamask/network-controller'; -import type { ObservableStore } from '@metamask/obs-store'; -import { TransactionParams } from '@metamask/transaction-controller'; -import type { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../shared/constants/metametrics'; -import { fetchTradesInfo as defaultFetchTradesInfo } from '../../../shared/lib/swaps-utils'; - -export type SwapsControllerStore = ObservableStore<{ - swapsState: SwapsControllerState; -}>; - -export type SwapsControllerState = { - quotes: Record; - quotesPollingLimitEnabled: boolean; - fetchParams: - | (FetchTradesInfoParams & { - metaData: FetchTradesInfoParamsMetadata; - }) - | null; - tokens: string[] | null; - tradeTxId: string | null; - approveTxId: string | null; - quotesLastFetched: number | null; - customMaxGas: string; - customGasPrice: string | null; - customMaxFeePerGas: string | null; - customMaxPriorityFeePerGas: string | null; - swapsUserFeeLevel: string; - selectedAggId: string | null; - customApproveTxData: string; - errorKey: string; - topAggId: string | null; - routeState: string; - swapsFeatureIsLive: boolean; - saveFetchedQuotes: boolean; - swapsQuoteRefreshTime: number; - swapsQuotePrefetchingRefreshTime: number; - swapsStxBatchStatusRefreshTime: number; - swapsStxStatusDeadline?: number; - swapsStxGetTransactionsRefreshTime: number; - swapsStxMaxFeeMultiplier: number; - swapsFeatureFlags: Record; -}; - -export type SwapsControllerOptions = { - getBufferedGasLimit: ( - params: { - txParams: { - value: string; - data: string; - to: string; - from: string; - }; - }, - factor: number, - ) => Promise<{ gasLimit: string; simulationFails: boolean }>; - provider: ExternalProvider | JsonRpcFetchFunc; - getProviderConfig: () => ProviderConfig; - getTokenRatesState: () => { - marketData: Record< - string, - { - [tokenAddress: string]: { - price: number; - }; - } - >; - }; - fetchTradesInfo: typeof defaultFetchTradesInfo; - getCurrentChainId: () => ChainId; - getLayer1GasFee: (params: { - transactionParams: TransactionParams; - chainId: ChainId; - }) => Promise; - getEIP1559GasFeeEstimates: () => Promise; - trackMetaMetricsEvent: (event: { - event: MetaMetricsEventName; - category: MetaMetricsEventCategory; - properties: Record; - }) => void; -}; - -export type FetchTradesInfoParams = { - slippage: number; - sourceToken: string; - sourceDecimals: number; - destinationToken: string; - value: string; - fromAddress: string; - exchangeList: string; - balanceError: boolean; -}; - -export type FetchTradesInfoParamsMetadata = { - chainId: ChainId; - sourceTokenInfo: { - address: string; - symbol: string; - decimals: number; - iconUrl?: string; - }; - destinationTokenInfo: { - address: string; - symbol: string; - decimals: number; - iconUrl?: string; - }; -}; - -export type QuoteRequest = { - chainId: number; - destinationToken: string; - slippage: number; - sourceAmount: string; - sourceToken: string; - walletAddress: string; -}; - -export type AggType = 'DEX' | 'RFQ' | 'CONTRACT' | 'CNT' | 'AGG'; - -export type PriceSlippage = { - bucket: 'low' | 'medium' | 'high'; - calculationError: string; - destinationAmountInETH: number | null; - destinationAmountInNativeCurrency: number | null; - ratio: number | null; - sourceAmountInETH: number | null; - sourceAmountInNativeCurrency: number | null; - sourceAmountInUSD: number | null; - destinationAmountInUSD: number | null; -}; - -export type Trade = { - data: string; - from: string; - to: string; - value: string; - gas?: string; -}; - -export type QuoteSavings = { - performance: string; - fee: string; - metaMaskFee: string; - medianMetaMaskFee: string; - total: string; -}; -export type Quote = { - aggregator: string; - aggType: AggType; - approvalNeeded?: Trade | null; - averageGas: number; - destinationAmount: string | null; - destinationToken: string; - destinationTokenInfo: { - address: string; - symbol: string; - decimals: number; - iconUrl?: string; - }; - destinationTokenRate: number | null; - error: null | string; - estimatedRefund: string; - ethFee: string; - ethValueOfTokens: string; - fee: number; - fetchTime: number; - gasEstimate: string; - gasEstimateWithRefund: string; - gasMultiplier: number; - hasRoute: boolean; - isBestQuote: boolean; - maxGas: number; - metaMaskFeeInEth: string; - multiLayerL1TradeFeeTotal?: string; - overallValueOfQuote: string; - priceSlippage: PriceSlippage; - quoteRefreshSeconds: number; - savings?: QuoteSavings; - sourceAmount: string; - sourceToken: string; - sourceTokenRate: number; - trade: null | Trade; -}; diff --git a/app/scripts/controllers/swaps.ts b/app/scripts/controllers/swaps/index.ts similarity index 63% rename from app/scripts/controllers/swaps.ts rename to app/scripts/controllers/swaps/index.ts index daa6c6954ff1..c2a947686ad3 100644 --- a/app/scripts/controllers/swaps.ts +++ b/app/scripts/controllers/swaps/index.ts @@ -4,76 +4,85 @@ import { JsonRpcFetchFunc, Web3Provider, } from '@ethersproject/providers'; +import { BaseController, StateMetadata } from '@metamask/base-controller'; import type { ChainId } from '@metamask/controller-utils'; import { GasFeeState } from '@metamask/gas-fee-controller'; -import { ProviderConfig } from '@metamask/network-controller'; -import { ObservableStore } from '@metamask/obs-store'; import { TransactionParams } from '@metamask/transaction-controller'; import { captureException } from '@sentry/browser'; import { BigNumber } from 'bignumber.js'; import abi from 'human-standard-token-abi'; import { cloneDeep, mapValues } from 'lodash'; -import { EtherDenomination } from '../../../shared/constants/common'; -import { GasEstimateTypes } from '../../../shared/constants/gas'; +import { EtherDenomination } from '../../../../shared/constants/common'; +import { GasEstimateTypes } from '../../../../shared/constants/gas'; import { MetaMetricsEventCategory, MetaMetricsEventErrorType, MetaMetricsEventName, -} from '../../../shared/constants/metametrics'; -import { CHAIN_IDS } from '../../../shared/constants/network'; +} from '../../../../shared/constants/metametrics'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import { FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, FALLBACK_SMART_TRANSACTIONS_DEADLINE, -} from '../../../shared/constants/smartTransactions'; +} from '../../../../shared/constants/smartTransactions'; import { DEFAULT_ERC20_APPROVE_GAS, QUOTES_EXPIRED_ERROR, QUOTES_NOT_AVAILABLE_ERROR, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, SWAPS_FETCH_ORDER_CONFLICT, -} from '../../../shared/constants/swaps'; -import { SECOND } from '../../../shared/constants/time'; -import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +} from '../../../../shared/constants/swaps'; +import { SECOND } from '../../../../shared/constants/time'; +import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { fetchTradesInfo as defaultFetchTradesInfo, getBaseApi, -} from '../../../shared/lib/swaps-utils'; +} from '../../../../shared/lib/swaps-utils'; import { calcGasTotal, calcTokenAmount, -} from '../../../shared/lib/transactions-controller-utils'; +} from '../../../../shared/lib/transactions-controller-utils'; import { decGWEIToHexWEI, sumHexes, -} from '../../../shared/modules/conversion.utils'; -import { Numeric } from '../../../shared/modules/Numeric'; -import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; -import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'; +} from '../../../../shared/modules/conversion.utils'; +import { Numeric } from '../../../../shared/modules/Numeric'; +import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; +import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils'; import { + controllerName, FALLBACK_QUOTE_REFRESH_TIME, MAX_GAS_LIMIT, POLL_COUNT_LIMIT, - swapsControllerInitialState, + getDefaultSwapsControllerState, } from './swaps.constants'; +import { + calculateGasEstimateWithRefund, + getMedianEthValueQuote, +} from './swaps.utils'; import type { FetchTradesInfoParams, FetchTradesInfoParamsMetadata, - Quote, - QuoteSavings, + SwapsControllerMessenger, SwapsControllerOptions, SwapsControllerState, - SwapsControllerStore, + Quote, + QuoteSavings, Trade, } from './swaps.types'; -import { - calculateGasEstimateWithRefund, - getMedianEthValueQuote, -} from './swaps.utils'; -export default class SwapsController { - public store: SwapsControllerStore; +const metadata: StateMetadata = { + swapsState: { + persist: false, + anonymous: false, + }, +}; +export default class SwapsController extends BaseController< + typeof controllerName, + SwapsControllerState, + SwapsControllerMessenger +> { public getBufferedGasLimit: ( params: { txParams: { @@ -86,19 +95,6 @@ export default class SwapsController { factor: number, ) => Promise<{ gasLimit: string; simulationFails: boolean }>; - public getProviderConfig: () => ProviderConfig; - - public getTokenRatesState: () => { - marketData: Record< - string, - { - [tokenAddress: string]: { - price: number; - }; - } - >; - }; - public resetState: () => void; public trackMetaMetricsEvent: (event: { @@ -107,17 +103,24 @@ export default class SwapsController { properties: Record; }) => void; - private _ethersProvider: Web3Provider; + #ethersProvider: Web3Provider; + + #ethersProviderChainId: ChainId; - private _ethersProviderChainId: ChainId; + #indexOfNewestCallInFlight: number; - private _indexOfNewestCallInFlight: number; + #pollCount: number; - private _pollCount: number; + #pollingTimeout: ReturnType | null = null; - private _pollingTimeout: ReturnType | null = null; + #provider: ExternalProvider | JsonRpcFetchFunc; - private _provider: ExternalProvider | JsonRpcFetchFunc; + #getEIP1559GasFeeEstimates: () => Promise; + + #getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; private _fetchTradesInfo: ( fetchParams: FetchTradesInfoParams, @@ -126,94 +129,198 @@ export default class SwapsController { [aggId: string]: Quote; }> = defaultFetchTradesInfo; - private _getCurrentChainId: () => ChainId; + constructor(opts: SwapsControllerOptions, state: SwapsControllerState) { + super({ + name: controllerName, + metadata, + messenger: opts.messenger, + state: { + swapsState: { + ...getDefaultSwapsControllerState().swapsState, + swapsFeatureFlags: state?.swapsState?.swapsFeatureFlags || {}, + }, + }, + }); - private _getEIP1559GasFeeEstimates: () => Promise; + this.messagingSystem.registerActionHandler( + `SwapsController:fetchAndSetQuotes`, + this.fetchAndSetQuotes.bind(this), + ); - private _getLayer1GasFee: (params: { - transactionParams: TransactionParams; - chainId: ChainId; - }) => Promise; + this.messagingSystem.registerActionHandler( + `SwapsController:setSelectedQuoteAggId`, + this.setSelectedQuoteAggId.bind(this), + ); - constructor( - opts: SwapsControllerOptions, - state: { swapsState: SwapsControllerState }, - ) { - // The store is initialized with the initial state, and then updated with the state from storage - this.store = new ObservableStore({ - swapsState: { - ...swapsControllerInitialState.swapsState, - swapsFeatureFlags: state?.swapsState?.swapsFeatureFlags || {}, - }, - }); + this.messagingSystem.registerActionHandler( + `SwapsController:resetSwapsState`, + this.resetSwapsState.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsTokens`, + this.setSwapsTokens.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:clearSwapsQuotes`, + this.clearSwapsQuotes.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setApproveTxId`, + this.setApproveTxId.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setTradeTxId`, + this.setTradeTxId.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsTxGasPrice`, + this.setSwapsTxGasPrice.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsTxGasLimit`, + this.setSwapsTxGasLimit.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsTxMaxFeePerGas`, + this.setSwapsTxMaxFeePerGas.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsTxMaxFeePriorityPerGas`, + this.setSwapsTxMaxFeePriorityPerGas.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:safeRefetchQuotes`, + this.safeRefetchQuotes.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:stopPollingForQuotes`, + this.stopPollingForQuotes.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setBackgroundSwapRouteState`, + this.setBackgroundSwapRouteState.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:resetPostFetchState`, + this.resetPostFetchState.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsErrorKey`, + this.setSwapsErrorKey.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setInitialGasEstimate`, + this.setInitialGasEstimate.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setCustomApproveTxData`, + this.setCustomApproveTxData.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsLiveness`, + this.setSwapsLiveness.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsFeatureFlags`, + this.setSwapsFeatureFlags.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsUserFeeLevel`, + this.setSwapsUserFeeLevel.bind(this), + ); + + this.messagingSystem.registerActionHandler( + `SwapsController:setSwapsQuotesPollingLimitEnabled`, + this.setSwapsQuotesPollingLimitEnabled.bind(this), + ); this.getBufferedGasLimit = opts.getBufferedGasLimit; - this.getTokenRatesState = opts.getTokenRatesState; - this.getProviderConfig = opts.getProviderConfig; this.trackMetaMetricsEvent = opts.trackMetaMetricsEvent; // The resetState function is used to reset the state to the initial state, but keep the swapsFeatureFlags this.resetState = () => { - this.store.updateState({ - swapsState: { - ...swapsControllerInitialState.swapsState, - swapsFeatureFlags: state?.swapsState?.swapsFeatureFlags, - }, + this.update((_state) => { + _state.swapsState = { + ...getDefaultSwapsControllerState().swapsState, + swapsFeatureFlags: _state?.swapsState.swapsFeatureFlags, + }; }); }; + this.#getEIP1559GasFeeEstimates = opts.getEIP1559GasFeeEstimates; + this.#getLayer1GasFee = opts.getLayer1GasFee; + this.#ethersProvider = new Web3Provider(opts.provider); + this.#ethersProviderChainId = this._getCurrentChainId(); + this.#indexOfNewestCallInFlight = 0; + this.#pollCount = 0; + this.#provider = opts.provider; + + // TODO: this should be private, but since a lot of tests depends on spying on it + // we cannot enforce privacy 100% this._fetchTradesInfo = opts.fetchTradesInfo || defaultFetchTradesInfo; - this._getCurrentChainId = opts.getCurrentChainId; - this._getEIP1559GasFeeEstimates = opts.getEIP1559GasFeeEstimates; - this._getLayer1GasFee = opts.getLayer1GasFee; - this._ethersProvider = new Web3Provider(opts.provider); - this._ethersProviderChainId = this._getCurrentChainId(); - this._indexOfNewestCallInFlight = 0; - this._pollCount = 0; - this._provider = opts.provider; } public clearSwapsQuotes() { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, quotes: {} } }); + this.update((_state) => { + _state.swapsState.quotes = {}; + _state.swapsState.selectedAggId = null; + _state.swapsState.topAggId = null; + }); } public async fetchAndSetQuotes( fetchParams: FetchTradesInfoParams, fetchParamsMetaData: FetchTradesInfoParamsMetadata, isPolledRequest = false, - ) { + ): Promise<[Record | null, string | null] | null> { if (!fetchParams) { return null; } const { chainId } = fetchParamsMetaData; - if (chainId !== this._ethersProviderChainId) { - this._ethersProvider = new Web3Provider(this._provider); - this._ethersProviderChainId = chainId; + if (chainId !== this.#ethersProviderChainId) { + this.#ethersProvider = new Web3Provider(this.#provider); + this.#ethersProviderChainId = chainId; } - const { - swapsState: { quotesPollingLimitEnabled, saveFetchedQuotes }, - } = this.store.getState(); + const { quotesPollingLimitEnabled, saveFetchedQuotes } = + this.state.swapsState; // Every time we get a new request that is not from the polling, we reset the poll count so we can poll for up to three more sets of quotes with these new params. if (!isPolledRequest) { - this._pollCount = 0; + this.#pollCount = 0; } // If there are any pending poll requests, clear them so that they don't get call while this new fetch is in process - if (this._pollingTimeout) { - clearTimeout(this._pollingTimeout); + if (this.#pollingTimeout) { + clearTimeout(this.#pollingTimeout); } if (!isPolledRequest) { this.setSwapsErrorKey(''); } - const indexOfCurrentCall = this._indexOfNewestCallInFlight + 1; - this._indexOfNewestCallInFlight = indexOfCurrentCall; + const indexOfCurrentCall = this.#indexOfNewestCallInFlight + 1; + this.#indexOfNewestCallInFlight = indexOfCurrentCall; if (!saveFetchedQuotes) { this._setSaveFetchedQuotes(true); @@ -224,9 +331,8 @@ export default class SwapsController { this._setSwapsNetworkConfig(), ]); - const { - swapsState: { saveFetchedQuotes: saveFetchedQuotesAfterResponse }, - } = this.store.getState(); + const { saveFetchedQuotes: saveFetchedQuotesAfterResponse } = + this.state.swapsState; // If saveFetchedQuotesAfterResponse is false, it means a user left Swaps (we cleaned the state) // and we don't want to set any API response with quotes into state. @@ -250,7 +356,7 @@ export default class SwapsController { await Promise.all( Object.values(newQuotes).map(async (quote) => { if (quote.trade) { - const multiLayerL1TradeFeeTotal = await this._getLayer1GasFee({ + const multiLayerL1TradeFeeTotal = await this.#getLayer1GasFee({ transactionParams: quote.trade, chainId, }); @@ -322,7 +428,7 @@ export default class SwapsController { if (Object.values(newQuotes).length === 0) { this.setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR); } else { - const topQuoteAndSavings = await this._findTopQuoteAndCalculateSavings( + const topQuoteAndSavings = await this.getTopQuoteWithCalculatedSavings( newQuotes, ); if (Array.isArray(topQuoteAndSavings)) { @@ -333,34 +439,33 @@ export default class SwapsController { // If a newer call has been made, don't update state with old information // Prevents timing conflicts between fetches - if (this._indexOfNewestCallInFlight !== indexOfCurrentCall) { + if (this.#indexOfNewestCallInFlight !== indexOfCurrentCall) { throw new Error(SWAPS_FETCH_ORDER_CONFLICT); } - const { swapsState } = this.store.getState(); - let { selectedAggId } = swapsState; + let { selectedAggId } = this.state.swapsState; if (!selectedAggId || !newQuotes[selectedAggId]) { selectedAggId = null; } - this.store.updateState({ - swapsState: { - ...swapsState, - quotes: newQuotes, - fetchParams: { ...fetchParams, metaData: fetchParamsMetaData }, - quotesLastFetched, - selectedAggId, - topAggId, - }, + this.update((_state) => { + _state.swapsState.quotes = newQuotes; + _state.swapsState.fetchParams = { + ...fetchParams, + metaData: fetchParamsMetaData, + }; + _state.swapsState.quotesLastFetched = quotesLastFetched; + _state.swapsState.selectedAggId = selectedAggId; + _state.swapsState.topAggId = topAggId; }); if (quotesPollingLimitEnabled) { // We only want to do up to a maximum of three requests from polling if polling limit is enabled. - // Otherwise we won't increase _pollCount, so polling will run without a limit. - this._pollCount += 1; + // Otherwise we won't increase #pollCount, so polling will run without a limit. + this.#pollCount += 1; } - if (!quotesPollingLimitEnabled || this._pollCount < POLL_COUNT_LIMIT + 1) { + if (!quotesPollingLimitEnabled || this.#pollCount < POLL_COUNT_LIMIT + 1) { this._pollForNewQuotes(); } else { this.resetPostFetchState(); @@ -371,235 +476,15 @@ export default class SwapsController { return [newQuotes, topAggId]; } - public resetPostFetchState() { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { - ...swapsControllerInitialState.swapsState, - tokens: swapsState.tokens, - fetchParams: swapsState.fetchParams, - swapsFeatureIsLive: swapsState.swapsFeatureIsLive, - swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, - swapsQuotePrefetchingRefreshTime: - swapsState.swapsQuotePrefetchingRefreshTime, - swapsFeatureFlags: swapsState.swapsFeatureFlags, - }, - }); - if (this._pollingTimeout) { - clearTimeout(this._pollingTimeout); - } - } - - public resetSwapsState() { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { - ...swapsControllerInitialState.swapsState, - swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, - swapsQuotePrefetchingRefreshTime: - swapsState.swapsQuotePrefetchingRefreshTime, - swapsFeatureFlags: swapsState.swapsFeatureFlags, - }, - }); - if (this._pollingTimeout) { - clearTimeout(this._pollingTimeout); - } - } - - public safeRefetchQuotes() { - const { swapsState } = this.store.getState(); - if (!this._pollingTimeout && swapsState.fetchParams) { - this.fetchAndSetQuotes(swapsState.fetchParams, { - ...swapsState.fetchParams.metaData, - }); - } - } - - public setApproveTxId(approveTxId: string | null) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, approveTxId } }); - } - - public setBackgroundSwapRouteState(routeState: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, routeState } }); - } - - public setCustomApproveTxData(data: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, customApproveTxData: data }, - }); - } - - public async setInitialGasEstimate(initialAggId: string) { - const { swapsState } = this.store.getState(); - - const quoteToUpdate = { ...swapsState.quotes[initialAggId] }; - - const { gasLimit: newGasEstimate, simulationFails } = quoteToUpdate.trade - ? await this._timedoutGasReturn( - quoteToUpdate.trade, - quoteToUpdate.aggregator, - ) - : { gasLimit: null, simulationFails: true }; - - if (newGasEstimate && !simulationFails) { - const gasEstimateWithRefund = calculateGasEstimateWithRefund( - quoteToUpdate.maxGas, - quoteToUpdate.estimatedRefund, - newGasEstimate, - ); - - quoteToUpdate.gasEstimate = newGasEstimate; - quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund; - } - - this.store.updateState({ - swapsState: { - ...swapsState, - quotes: { ...swapsState.quotes, [initialAggId]: quoteToUpdate }, - }, - }); - } - - public setSelectedQuoteAggId(selectedAggId: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, selectedAggId } }); - } - - public setSwapsFeatureFlags(swapsFeatureFlags: Record) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, swapsFeatureFlags }, - }); - } - - public setSwapsErrorKey(errorKey: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, errorKey } }); - } - - public setSwapsLiveness(swapsLiveness: { swapsFeatureIsLive: boolean }) { - const { swapsState } = this.store.getState(); - const { swapsFeatureIsLive } = swapsLiveness; - this.store.updateState({ - swapsState: { ...swapsState, swapsFeatureIsLive }, - }); - } - - public setSwapsQuotesPollingLimitEnabled(quotesPollingLimitEnabled: boolean) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, quotesPollingLimitEnabled }, - }); - } - - public setSwapsTokens(tokens: string[]) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, tokens } }); - } - - public setSwapsTxGasLimit(gasLimit: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, customMaxGas: gasLimit }, - }); - } - - public setSwapsTxGasPrice(gasPrice: string | null) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, customGasPrice: gasPrice }, - }); - } - - public setSwapsTxMaxFeePerGas(maxFeePerGas: string | null) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, customMaxFeePerGas: maxFeePerGas }, - }); - } - - public setSwapsTxMaxFeePriorityPerGas(maxPriorityFeePerGas: string | null) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { - ...swapsState, - customMaxPriorityFeePerGas: maxPriorityFeePerGas, - }, - }); - } - - public setSwapsUserFeeLevel(swapsUserFeeLevel: string) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, swapsUserFeeLevel }, - }); - } - - public setTradeTxId(tradeTxId: string | null) { - const { swapsState } = this.store.getState(); - this.store.updateState({ swapsState: { ...swapsState, tradeTxId } }); - } - - /** - * Once quotes are fetched, we poll for new ones to keep the quotes up to date. - * Market and aggregator contract conditions can change fast enough that quotes - * will no longer be available after 1 or 2 minutes. When `fetchAndSetQuotes` is - * first called, it receives fetch parameters that are stored in state. These stored - * parameters are used on subsequent calls made during polling. - * - * Note: We stop polling after 3 requests, until new quotes are explicitly asked for. - * The logic that enforces that maximum is in the body of `fetchAndSetQuotes`. - */ - public stopPollingForQuotes() { - if (this._pollingTimeout) { - clearTimeout(this._pollingTimeout); - } - } - - // Private Methods - - private async _fetchSwapsNetworkConfig(chainId: ChainId) { - const response = await fetchWithCache({ - url: getBaseApi('network', chainId), - fetchOptions: { method: 'GET' }, - cacheOptions: { cacheRefreshTime: 600000 }, - functionName: '_fetchSwapsNetworkConfig', - }); - const { refreshRates, parameters = {} } = response || {}; - if ( - !refreshRates || - typeof refreshRates.quotes !== 'number' || - typeof refreshRates.quotesPrefetching !== 'number' - ) { - throw new Error( - `MetaMask - invalid response for refreshRates: ${response}`, - ); - } - // We presently use milliseconds in the UI. - return { - quotes: refreshRates.quotes * 1000, - quotesPrefetching: refreshRates.quotesPrefetching * 1000, - stxGetTransactions: refreshRates.stxGetTransactions * 1000, - stxBatchStatus: refreshRates.stxBatchStatus * 1000, - stxStatusDeadline: refreshRates.stxStatusDeadline, - stxMaxFeeMultiplier: parameters.stxMaxFeeMultiplier, - }; - } - - private async _findTopQuoteAndCalculateSavings( + public async getTopQuoteWithCalculatedSavings( quotes: Record = {}, ): Promise<[string | null, Record] | Record> { - const { marketData } = this.getTokenRatesState(); + const { marketData } = this._getTokenRatesState(); const chainId = this._getCurrentChainId(); - const tokenConversionRates = marketData?.[chainId] ?? {}; - const { - swapsState: { customGasPrice, customMaxPriorityFeePerGas }, - } = this.store.getState(); + const { customGasPrice, customMaxPriorityFeePerGas } = + this.state.swapsState; const numQuotes = Object.keys(quotes).length; if (numQuotes === 0) { @@ -609,7 +494,7 @@ export default class SwapsController { const newQuotes = cloneDeep(quotes); const { gasFeeEstimates, gasEstimateType } = - await this._getEIP1559GasFeeEstimates(); + await this.#getEIP1559GasFeeEstimates(); let usedGasPrice = '0x0'; @@ -651,7 +536,7 @@ export default class SwapsController { aggregator, approvalNeeded, averageGas, - destinationAmount = 0, + destinationAmount, destinationToken, destinationTokenInfo, gasEstimateWithRefund, @@ -711,7 +596,7 @@ export default class SwapsController { : totalEthCost; const decimalAdjustedDestinationAmount = calcTokenAmount( - destinationAmount, + destinationAmount ?? '0', destinationTokenInfo.decimals, ); @@ -833,6 +718,227 @@ export default class SwapsController { return [topAggId, newQuotes]; } + public resetPostFetchState() { + this.update((_state) => { + _state.swapsState = { + ...getDefaultSwapsControllerState().swapsState, + tokens: _state.swapsState.tokens, + fetchParams: _state.swapsState.fetchParams, + swapsFeatureIsLive: _state.swapsState.swapsFeatureIsLive, + swapsQuoteRefreshTime: _state.swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + _state.swapsState.swapsQuotePrefetchingRefreshTime, + swapsFeatureFlags: _state.swapsState.swapsFeatureFlags, + }; + }); + if (this.#pollingTimeout) { + clearTimeout(this.#pollingTimeout); + } + } + + public resetSwapsState() { + this.update((_state) => { + _state.swapsState = { + ...getDefaultSwapsControllerState().swapsState, + swapsQuoteRefreshTime: _state.swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + _state.swapsState.swapsQuotePrefetchingRefreshTime, + swapsFeatureFlags: _state.swapsState.swapsFeatureFlags, + }; + }); + + if (this.#pollingTimeout) { + clearTimeout(this.#pollingTimeout); + } + } + + public safeRefetchQuotes() { + if (!this.#pollingTimeout && this.state.swapsState.fetchParams) { + this.fetchAndSetQuotes(this.state.swapsState.fetchParams, { + ...this.state.swapsState.fetchParams.metaData, + }); + } + } + + public setApproveTxId(approveTxId: string | null) { + this.update((_state) => { + _state.swapsState.approveTxId = approveTxId; + }); + } + + public setBackgroundSwapRouteState(routeState: string) { + this.update((_state) => { + _state.swapsState.routeState = routeState; + }); + } + + public setCustomApproveTxData(customApproveTxData: string) { + this.update((_state) => { + _state.swapsState.customApproveTxData = customApproveTxData; + }); + } + + public async setInitialGasEstimate(initialAggId: string) { + const quoteToUpdate = { ...this.state.swapsState.quotes[initialAggId] }; + + const { gasLimit: newGasEstimate, simulationFails } = quoteToUpdate.trade + ? await this._timedoutGasReturn( + quoteToUpdate.trade, + quoteToUpdate.aggregator, + ) + : { gasLimit: null, simulationFails: true }; + + if (newGasEstimate && !simulationFails) { + const gasEstimateWithRefund = calculateGasEstimateWithRefund( + quoteToUpdate.maxGas, + quoteToUpdate.estimatedRefund, + newGasEstimate, + ); + + quoteToUpdate.gasEstimate = newGasEstimate; + quoteToUpdate.gasEstimateWithRefund = gasEstimateWithRefund; + } + + this.update((_state) => { + _state.swapsState.quotes = { + ..._state.swapsState.quotes, + [initialAggId]: quoteToUpdate, + }; + }); + } + + public setSelectedQuoteAggId(selectedAggId: string) { + this.update((_state) => { + _state.swapsState.selectedAggId = selectedAggId; + }); + } + + public setSwapsFeatureFlags(swapsFeatureFlags: Record) { + this.update((_state) => { + _state.swapsState.swapsFeatureFlags = swapsFeatureFlags; + }); + } + + public setSwapsErrorKey(errorKey: string) { + this.update((_state) => { + _state.swapsState.errorKey = errorKey; + }); + } + + public setSwapsLiveness(swapsLiveness: { swapsFeatureIsLive: boolean }) { + const { swapsFeatureIsLive } = swapsLiveness; + this.update((_state) => { + _state.swapsState.swapsFeatureIsLive = swapsFeatureIsLive; + }); + } + + public setSwapsQuotesPollingLimitEnabled(quotesPollingLimitEnabled: boolean) { + this.update((_state) => { + _state.swapsState.quotesPollingLimitEnabled = quotesPollingLimitEnabled; + }); + } + + public setSwapsTokens(tokens: string[]) { + this.update((_state) => { + _state.swapsState.tokens = tokens; + }); + } + + public setSwapsTxGasLimit(customMaxGas: string) { + this.update((_state) => { + _state.swapsState.customMaxGas = customMaxGas; + }); + } + + public setSwapsTxGasPrice(customGasPrice: string | null) { + this.update((_state) => { + _state.swapsState.customGasPrice = customGasPrice; + }); + } + + public setSwapsTxMaxFeePerGas(customMaxFeePerGas: string | null) { + this.update((_state) => { + _state.swapsState.customMaxFeePerGas = customMaxFeePerGas; + }); + } + + public setSwapsTxMaxFeePriorityPerGas( + customMaxPriorityFeePerGas: string | null, + ) { + this.update((_state) => { + _state.swapsState.customMaxPriorityFeePerGas = customMaxPriorityFeePerGas; + }); + } + + public setSwapsUserFeeLevel(swapsUserFeeLevel: string) { + this.update((_state) => { + _state.swapsState.swapsUserFeeLevel = swapsUserFeeLevel; + }); + } + + public setTradeTxId(tradeTxId: string | null) { + this.update((_state) => { + _state.swapsState.tradeTxId = tradeTxId; + }); + } + + /** + * Once quotes are fetched, we poll for new ones to keep the quotes up to date. + * Market and aggregator contract conditions can change fast enough that quotes + * will no longer be available after 1 or 2 minutes. When `fetchAndSetQuotes` is + * first called, it receives fetch parameters that are stored in state. These stored + * parameters are used on subsequent calls made during polling. + * + * Note: We stop polling after 3 requests, until new quotes are explicitly asked for. + * The logic that enforces that maximum is in the body of `fetchAndSetQuotes`. + */ + public stopPollingForQuotes() { + if (this.#pollingTimeout) { + clearTimeout(this.#pollingTimeout); + } + } + + /** + * This method is used to update the state of the controller for testing purposes. + * DO NOT USE OUTSIDE OF TESTING + * + * @param newState - The new state to set + */ + public __test__updateState = (newState: Partial) => { + this.update((oldState) => { + return { swapsState: { ...oldState.swapsState, ...newState.swapsState } }; + }); + }; + + // Private Methods + private async _fetchSwapsNetworkConfig(chainId: ChainId) { + const response = await fetchWithCache({ + url: getBaseApi('network', chainId), + fetchOptions: { method: 'GET' }, + cacheOptions: { cacheRefreshTime: 600000 }, + functionName: '_fetchSwapsNetworkConfig', + }); + const { refreshRates, parameters = {} } = response || {}; + if ( + !refreshRates || + typeof refreshRates.quotes !== 'number' || + typeof refreshRates.quotesPrefetching !== 'number' + ) { + throw new Error( + `MetaMask - invalid response for refreshRates: ${response}`, + ); + } + // We presently use milliseconds in the UI. + return { + quotes: refreshRates.quotes * 1000, + quotesPrefetching: refreshRates.quotesPrefetching * 1000, + stxGetTransactions: refreshRates.stxGetTransactions * 1000, + stxBatchStatus: refreshRates.stxBatchStatus * 1000, + stxStatusDeadline: refreshRates.stxStatusDeadline, + stxMaxFeeMultiplier: parameters.stxMaxFeeMultiplier, + }; + } + private async _getAllQuotesWithGasEstimates(quotes: Record) { const quoteGasData = await Promise.all( Object.values(quotes).map(async (quote) => { @@ -877,12 +983,25 @@ export default class SwapsController { return newQuotes; } + private _getCurrentChainId(): ChainId { + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); + return chainId as ChainId; + } + private async _getERC20Allowance( contractAddress: string, walletAddress: string, chainId: ChainId, ) { - const contract = new Contract(contractAddress, abi, this._ethersProvider); + const contract = new Contract(contractAddress, abi, this.#ethersProvider); return await contract.allowance( walletAddress, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[ @@ -891,65 +1010,82 @@ export default class SwapsController { ); } + private _getTokenRatesState(): { + marketData: Record< + string, + { + [tokenAddress: string]: { + price: number; + }; + } + >; + } { + const { marketData } = this.messagingSystem.call( + 'TokenRatesController:getState', + ); + return { marketData }; + } + private _pollForNewQuotes() { const { - swapsState: { - swapsQuoteRefreshTime, - swapsQuotePrefetchingRefreshTime, - quotesPollingLimitEnabled, - }, - } = this.store.getState(); + swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, + quotesPollingLimitEnabled, + } = this.state.swapsState; // swapsQuoteRefreshTime is used on the View Quote page, swapsQuotePrefetchingRefreshTime is used on the Build Quote page. const quotesRefreshRateInMs = quotesPollingLimitEnabled ? swapsQuoteRefreshTime : swapsQuotePrefetchingRefreshTime; - this._pollingTimeout = setTimeout(() => { - const { swapsState } = this.store.getState(); + this.#pollingTimeout = setTimeout(() => { this.fetchAndSetQuotes( - swapsState.fetchParams as FetchTradesInfoParams, - swapsState.fetchParams?.metaData as FetchTradesInfoParamsMetadata, + this.state.swapsState.fetchParams as FetchTradesInfoParams, + this.state.swapsState.fetchParams + ?.metaData as FetchTradesInfoParamsMetadata, true, ); }, quotesRefreshRateInMs); } private _setSaveFetchedQuotes(status: boolean) { - const { swapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...swapsState, saveFetchedQuotes: status }, + this.update((_state) => { + _state.swapsState.saveFetchedQuotes = status; }); } // Sets the network config from the MetaSwap API. private async _setSwapsNetworkConfig() { const chainId = this._getCurrentChainId(); - let swapsNetworkConfig; + let swapsNetworkConfig: { + quotes: number; + quotesPrefetching: number; + stxGetTransactions: number; + stxBatchStatus: number; + stxStatusDeadline: number; + stxMaxFeeMultiplier: number; + } | null = null; + try { swapsNetworkConfig = await this._fetchSwapsNetworkConfig(chainId); } catch (e) { console.error('Request for Swaps network config failed: ', e); } - const { swapsState: latestSwapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { - ...latestSwapsState, - swapsQuoteRefreshTime: - swapsNetworkConfig?.quotes || FALLBACK_QUOTE_REFRESH_TIME, - swapsQuotePrefetchingRefreshTime: - swapsNetworkConfig?.quotesPrefetching || FALLBACK_QUOTE_REFRESH_TIME, - swapsStxGetTransactionsRefreshTime: - swapsNetworkConfig?.stxGetTransactions || - FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, - swapsStxBatchStatusRefreshTime: - swapsNetworkConfig?.stxBatchStatus || - FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, - swapsStxMaxFeeMultiplier: - swapsNetworkConfig?.stxMaxFeeMultiplier || - FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, - swapsStxStatusDeadline: - swapsNetworkConfig?.stxStatusDeadline || - FALLBACK_SMART_TRANSACTIONS_DEADLINE, - }, + this.update((_state) => { + _state.swapsState.swapsQuoteRefreshTime = + swapsNetworkConfig?.quotes || FALLBACK_QUOTE_REFRESH_TIME; + _state.swapsState.swapsQuotePrefetchingRefreshTime = + swapsNetworkConfig?.quotesPrefetching || FALLBACK_QUOTE_REFRESH_TIME; + _state.swapsState.swapsStxGetTransactionsRefreshTime = + swapsNetworkConfig?.stxGetTransactions || + FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME; + _state.swapsState.swapsStxBatchStatusRefreshTime = + swapsNetworkConfig?.stxBatchStatus || + FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME; + _state.swapsState.swapsStxMaxFeeMultiplier = + swapsNetworkConfig?.stxMaxFeeMultiplier || + FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER; + _state.swapsState.swapsStxStatusDeadline = + swapsNetworkConfig?.stxStatusDeadline || + FALLBACK_SMART_TRANSACTIONS_DEADLINE; }); } diff --git a/app/scripts/controllers/swaps.constants.ts b/app/scripts/controllers/swaps/swaps.constants.ts similarity index 88% rename from app/scripts/controllers/swaps.constants.ts rename to app/scripts/controllers/swaps/swaps.constants.ts index 6228dd2bcb66..1e5b566387b8 100644 --- a/app/scripts/controllers/swaps.constants.ts +++ b/app/scripts/controllers/swaps/swaps.constants.ts @@ -2,11 +2,13 @@ import { FALLBACK_SMART_TRANSACTIONS_DEADLINE, FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, -} from '../../../shared/constants/smartTransactions'; -import { MINUTE } from '../../../shared/constants/time'; +} from '../../../../shared/constants/smartTransactions'; +import { MINUTE } from '../../../../shared/constants/time'; import type { SwapsControllerState } from './swaps.types'; +export const controllerName = 'SwapsController'; + // The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator export const MAX_GAS_LIMIT = 2500000; @@ -18,8 +20,8 @@ export const POLL_COUNT_LIMIT = 3; // provide a reasonable fallback to avoid further errors export const FALLBACK_QUOTE_REFRESH_TIME = MINUTE; -export const swapsControllerInitialState: { swapsState: SwapsControllerState } = - { +export function getDefaultSwapsControllerState(): SwapsControllerState { + return { swapsState: { quotes: {}, quotesPollingLimitEnabled: false, @@ -50,3 +52,4 @@ export const swapsControllerInitialState: { swapsState: SwapsControllerState } = swapsFeatureFlags: {}, }, }; +} diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps/swaps.test.ts similarity index 58% rename from app/scripts/controllers/swaps.test.js rename to app/scripts/controllers/swaps/swaps.test.ts index 63b910f35b0e..3fa4f1ff9409 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps/swaps.test.ts @@ -1,19 +1,23 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { mapValues } from 'lodash'; +import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'; +import { ChainId } from '@metamask/controller-utils'; import BigNumberjs from 'bignumber.js'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; -import { createTestProviderTools } from '../../../test/stub/provider'; -import { SECOND } from '../../../shared/constants/time'; -import { GasEstimateTypes } from '../../../shared/constants/gas'; +import { mapValues } from 'lodash'; +import { GasEstimateTypes } from '../../../../shared/constants/gas'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../shared/constants/swaps'; +import { createTestProviderTools } from '../../../../test/stub/provider'; +import { getDefaultSwapsControllerState } from './swaps.constants'; import { - FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, - FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, -} from '../../../shared/constants/smartTransactions'; -import SwapsController from './swaps'; + FetchTradesInfoParams, + FetchTradesInfoParamsMetadata, + Quote, + SwapsControllerMessenger, +} from './swaps.types'; import { getMedianEthValueQuote } from './swaps.utils'; +import SwapsController from '.'; -const MOCK_FETCH_PARAMS = { +const MOCK_FETCH_PARAMS: FetchTradesInfoParams = { slippage: 3, sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', sourceDecimals: 18, @@ -21,6 +25,7 @@ const MOCK_FETCH_PARAMS = { value: '1000000000000000000', fromAddress: '0x7F18BB4Dd92CF2404C54CBa1A9BE4A1153bdb078', exchangeList: 'zeroExV1', + balanceError: false, }; const TEST_AGG_ID_1 = 'TEST_AGG_1'; @@ -32,7 +37,7 @@ const TEST_AGG_ID_6 = 'TEST_AGG_6'; const TEST_AGG_ID_BEST = 'TEST_AGG_BEST'; const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL'; -const POLLING_TIMEOUT = SECOND * 1000; +// const POLLING_TIMEOUT = SECOND * 1000; const MOCK_APPROVAL_NEEDED = { data: '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00', @@ -70,88 +75,90 @@ const MOCK_QUOTES_APPROVAL_REQUIRED = { }, }; -const MOCK_FETCH_METADATA = { +const MOCK_FETCH_METADATA: FetchTradesInfoParamsMetadata = { destinationTokenInfo: { symbol: 'FOO', decimals: 18, + address: '0xSomeAddress', + }, + sourceTokenInfo: { + symbol: 'BAR', + decimals: 18, + address: '0xSomeOtherAddress', }, chainId: CHAIN_IDS.MAINNET, }; -const MOCK_TOKEN_RATES_STORE = () => ({ - marketData: { - '0x1': { - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 2 }, - '0x1111111111111111111111111111111111111111': { price: 0.1 }, - }, - }, -}); - -const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' }); - const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({ - gasLimit: 2000000, - simulationFails: undefined, + gasLimit: '2000000', + simulationFails: false, }); -const EMPTY_INIT_STATE = { - swapsState: { - quotes: {}, - quotesPollingLimitEnabled: false, - fetchParams: null, - tokens: null, - tradeTxId: null, - approveTxId: null, - quotesLastFetched: null, - customMaxFeePerGas: null, - customMaxGas: '', - customMaxPriorityFeePerGas: null, - customGasPrice: null, - selectedAggId: null, - customApproveTxData: '', - errorKey: '', - topAggId: null, - routeState: '', - swapsFeatureIsLive: true, - swapsFeatureFlags: {}, - swapsQuoteRefreshTime: 60000, - swapsQuotePrefetchingRefreshTime: 60000, - swapsStxBatchStatusRefreshTime: FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, - swapsStxGetTransactionsRefreshTime: - FALLBACK_SMART_TRANSACTIONS_REFRESH_TIME, - swapsStxMaxFeeMultiplier: FALLBACK_SMART_TRANSACTIONS_MAX_FEE_MULTIPLIER, - swapsStxStatusDeadline: 180, - swapsUserFeeLevel: '', - saveFetchedQuotes: false, - }, -}; - const fetchTradesInfoStub = jest.fn(); -const getCurrentChainIdStub = jest.fn().mockReturnValue(CHAIN_IDS.MAINNET); const getLayer1GasFeeStub = jest.fn().mockReturnValue('0x1'); -const getNetworkClientIdStub = jest.fn().mockReturnValue('1'); const getEIP1559GasFeeEstimatesStub = jest.fn().mockReturnValue({ gasFeeEstimates: { high: '150', }, gasEstimateType: GasEstimateTypes.legacy, }); +const trackMetaMetricsEventStub = jest.fn(); + +// Create a single mock object +const messengerMock = { + call: jest.fn(), + registerActionHandler: jest.fn(), + registerInitialEventPayload: jest.fn(), + publish: jest.fn(), +} as unknown as jest.Mocked; + +const networkControllerGetStateCallbackMock = jest + .fn() + .mockReturnValue({ selectedNetworkClientId: 'metamask' }); + +const networkControllerGetNetworkClientByIdCallbackMock = jest + .fn() + .mockReturnValue({ configuration: { chainId: CHAIN_IDS.MAINNET } }); + +const tokenRatesControllerGetStateCallbackMock = jest.fn().mockReturnValue({ + marketData: { + '0x1': { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { price: 2 }, + '0x1111111111111111111111111111111111111111': { price: 0.1 }, + }, + }, +}); + +messengerMock.call.mockImplementation((actionName, ..._rest) => { + if (actionName === 'NetworkController:getState') { + return networkControllerGetStateCallbackMock(); + } + if (actionName === 'NetworkController:getNetworkClientById') { + return networkControllerGetNetworkClientByIdCallbackMock(); + } + if (actionName === 'TokenRatesController:getState') { + return tokenRatesControllerGetStateCallbackMock(); + } + return undefined; +}); describe('SwapsController', function () { - let provider; - - const getSwapsController = (_provider = provider) => { - return new SwapsController({ - getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, - provider: _provider, - getProviderConfig: MOCK_GET_PROVIDER_CONFIG, - getTokenRatesState: MOCK_TOKEN_RATES_STORE, - fetchTradesInfo: fetchTradesInfoStub, - getCurrentChainId: getCurrentChainIdStub, - getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, - getNetworkClientId: getNetworkClientIdStub, - getLayer1GasFee: getLayer1GasFeeStub, - }); + let provider: ExternalProvider | JsonRpcFetchFunc; + const getSwapsController = ( + _provider: ExternalProvider | JsonRpcFetchFunc = provider, + ) => { + return new SwapsController( + { + getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, + provider: _provider, + fetchTradesInfo: fetchTradesInfoStub, + getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, + getLayer1GasFee: getLayer1GasFeeStub, + trackMetaMetricsEvent: trackMetaMetricsEventStub, + messenger: messengerMock, + }, + getDefaultSwapsControllerState(), + ); }; beforeEach(function () { @@ -164,7 +171,7 @@ describe('SwapsController', function () { provider = createTestProviderTools({ scaffold: providerResultStub, networkId: 1, - chainId: 1, + chainId: CHAIN_IDS.MAINNET as ChainId, }).provider; jest.useFakeTimers(); }); @@ -177,19 +184,17 @@ describe('SwapsController', function () { describe('constructor', function () { it('should setup correctly', function () { const swapsController = getSwapsController(); - expect(swapsController.store.getState()).toStrictEqual(EMPTY_INIT_STATE); + expect(swapsController.state).toStrictEqual( + getDefaultSwapsControllerState(), + ); expect(swapsController.getBufferedGasLimit).toStrictEqual( MOCK_GET_BUFFERED_GAS_LIMIT, ); - expect(swapsController._pollCount).toStrictEqual(0); - expect(swapsController.getProviderConfig).toStrictEqual( - MOCK_GET_PROVIDER_CONFIG, - ); }); }); describe('API', function () { - let swapsController; + let swapsController: SwapsController; beforeEach(function () { swapsController = getSwapsController(); }); @@ -198,79 +203,91 @@ describe('SwapsController', function () { it('should set selected quote agg id', function () { const selectedAggId = 'test'; swapsController.setSelectedQuoteAggId(selectedAggId); - expect( - swapsController.store.getState().swapsState.selectedAggId, - ).toStrictEqual(selectedAggId); + expect(swapsController.state.swapsState.selectedAggId).toStrictEqual( + selectedAggId, + ); }); it('should set swaps tokens', function () { - const tokens = []; + const tokens: string[] = []; swapsController.setSwapsTokens(tokens); - expect( - swapsController.store.getState().swapsState.tokens, - ).toStrictEqual(tokens); + expect(swapsController.state.swapsState.tokens).toStrictEqual(tokens); }); it('should set trade tx id', function () { const tradeTxId = 'test'; swapsController.setTradeTxId(tradeTxId); - expect( - swapsController.store.getState().swapsState.tradeTxId, - ).toStrictEqual(tradeTxId); + expect(swapsController.state.swapsState.tradeTxId).toStrictEqual( + tradeTxId, + ); }); it('should set swaps tx gas price', function () { - const gasPrice = 1; + const gasPrice = '1'; swapsController.setSwapsTxGasPrice(gasPrice); - expect( - swapsController.store.getState().swapsState.customGasPrice, - ).toStrictEqual(gasPrice); + expect(swapsController.state.swapsState.customGasPrice).toStrictEqual( + gasPrice, + ); }); it('should set swaps tx gas limit', function () { const gasLimit = '1'; swapsController.setSwapsTxGasLimit(gasLimit); - expect( - swapsController.store.getState().swapsState.customMaxGas, - ).toStrictEqual(gasLimit); + expect(swapsController.state.swapsState.customMaxGas).toStrictEqual( + gasLimit, + ); }); it('should set background swap route state', function () { const routeState = 'test'; swapsController.setBackgroundSwapRouteState(routeState); - expect( - swapsController.store.getState().swapsState.routeState, - ).toStrictEqual(routeState); + expect(swapsController.state.swapsState.routeState).toStrictEqual( + routeState, + ); }); it('should set swaps error key', function () { const errorKey = 'test'; swapsController.setSwapsErrorKey(errorKey); - expect( - swapsController.store.getState().swapsState.errorKey, - ).toStrictEqual(errorKey); + expect(swapsController.state.swapsState.errorKey).toStrictEqual( + errorKey, + ); }); it('should set initial gas estimate', async function () { const initialAggId = TEST_AGG_ID_1; - const baseGasEstimate = 10; - const { maxGas, estimatedRefund } = getMockQuotes()[TEST_AGG_ID_1]; + const { maxGas, estimatedRefund, trade } = + getMockQuotes()[TEST_AGG_ID_1]; - const { swapsState } = swapsController.store.getState(); - // Set mock quotes in order to have data for the test agg - swapsController.store.updateState({ - swapsState: { ...swapsState, quotes: getMockQuotes() }, + // eslint-disable-next-line jest/no-if + if (!trade) { + throw new Error('Trade data is required'); + } + + // Override state with mock quotes in order to have data for the test agg + swapsController.__test__updateState({ + swapsState: { + ...swapsController.state.swapsState, + quotes: getMockQuotes(), + }, }); - await swapsController.setInitialGasEstimate( - initialAggId, - baseGasEstimate, - ); + await swapsController.setInitialGasEstimate(initialAggId); const { gasLimit: bufferedGasLimit } = - await swapsController.getBufferedGasLimit(); + await swapsController.getBufferedGasLimit( + { + txParams: { + value: trade.value, + data: trade.data, + from: trade.from, + to: trade.to, + }, + }, + 1, + ); const { gasEstimate, gasEstimateWithRefund } = - swapsController.store.getState().swapsState.quotes[initialAggId]; + swapsController.state.swapsState.quotes[initialAggId]; expect(gasEstimate).toStrictEqual(bufferedGasLimit); expect(gasEstimateWithRefund).toStrictEqual( @@ -284,34 +301,39 @@ describe('SwapsController', function () { const data = 'test'; swapsController.setCustomApproveTxData(data); expect( - swapsController.store.getState().swapsState.customApproveTxData, + swapsController.state.swapsState.customApproveTxData, ).toStrictEqual(data); }); }); - describe('_findTopQuoteAndCalculateSavings', function () { + describe('getTopQuoteWithCalculatedSavings', function () { beforeEach(function () { - const { swapsState } = swapsController.store.getState(); - swapsController.store.updateState({ - swapsState: { ...swapsState, customGasPrice: '0x174876e800' }, + swapsController.__test__updateState({ + swapsState: { + ...swapsController.state.swapsState, + customGasPrice: '0x174876e800', + }, }); }); it('returns empty object if passed undefined or empty object', async function () { expect( - await swapsController._findTopQuoteAndCalculateSavings(), + await swapsController.getTopQuoteWithCalculatedSavings(), ).toStrictEqual({}); expect( - await swapsController._findTopQuoteAndCalculateSavings({}), + await swapsController.getTopQuoteWithCalculatedSavings({}), ).toStrictEqual({}); }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and an even number of quotes', async function () { - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings( + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings( getTopQuoteAndSavingsMockQuotes(), ); + + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; expect(topAggId).toStrictEqual(TEST_AGG_ID_1); expect(resultQuotes).toStrictEqual( getTopQuoteAndSavingsBaseExpectedResults(), @@ -319,11 +341,26 @@ describe('SwapsController', function () { }); it('returns the top aggId and quotes with savings and fee values if passed necessary data and an odd number of quotes', async function () { - const testInput = getTopQuoteAndSavingsMockQuotes(); - delete testInput[TEST_AGG_ID_6]; - const expectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults(); - delete expectedResultQuotes[TEST_AGG_ID_6]; - expectedResultQuotes[TEST_AGG_ID_1].savings = { + const completeTestInput = getTopQuoteAndSavingsMockQuotes(); + const partialTestInput = { + [TEST_AGG_ID_1]: completeTestInput[TEST_AGG_ID_1], + [TEST_AGG_ID_2]: completeTestInput[TEST_AGG_ID_2], + [TEST_AGG_ID_3]: completeTestInput[TEST_AGG_ID_3], + [TEST_AGG_ID_4]: completeTestInput[TEST_AGG_ID_4], + [TEST_AGG_ID_5]: completeTestInput[TEST_AGG_ID_5], + }; + + const completeExpectedResultQuotes = + getTopQuoteAndSavingsBaseExpectedResults(); + const partialExpectedResultQuotes = { + [TEST_AGG_ID_1]: completeExpectedResultQuotes[TEST_AGG_ID_1], + [TEST_AGG_ID_2]: completeExpectedResultQuotes[TEST_AGG_ID_2], + [TEST_AGG_ID_3]: completeExpectedResultQuotes[TEST_AGG_ID_3], + [TEST_AGG_ID_4]: completeExpectedResultQuotes[TEST_AGG_ID_4], + [TEST_AGG_ID_5]: completeExpectedResultQuotes[TEST_AGG_ID_5], + }; + + completeExpectedResultQuotes[TEST_AGG_ID_1].savings = { total: '0.0092', performance: '0.0297', fee: '0', @@ -331,10 +368,15 @@ describe('SwapsController', function () { medianMetaMaskFee: '0.0202', }; - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings(testInput); + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings( + partialTestInput, + ); + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; + expect(topAggId).toStrictEqual(TEST_AGG_ID_1); - expect(resultQuotes).toStrictEqual(expectedResultQuotes); + expect(resultQuotes).toStrictEqual(partialExpectedResultQuotes); }); it('returns the top aggId, without best quote flagged, and quotes with fee values if passed necessary data but no custom convert rate exists', async function () { @@ -372,8 +414,10 @@ describe('SwapsController', function () { }, }; - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings(testInput); + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings(testInput); + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; expect(topAggId).toStrictEqual(TEST_AGG_ID_1); expect(resultQuotes).toStrictEqual(expectedResultQuotes); }); @@ -385,7 +429,9 @@ describe('SwapsController', function () { ...quote, sourceToken: ETH_SWAPS_TOKEN_OBJECT.address, destinationToken: '0x1111111111111111111111111111111111111111', - trade: { value: '0x8ac7230489e80000' }, + trade: { + value: '0x8ac7230489e80000', + }, }), ); const baseExpectedResultQuotes = @@ -435,8 +481,12 @@ describe('SwapsController', function () { }, }; - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings(testInput); + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings( + testInput as Record, + ); + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; expect(topAggId).toStrictEqual(TEST_AGG_ID_1); expect(resultQuotes).toStrictEqual(expectedResultQuotes); }); @@ -448,7 +498,9 @@ describe('SwapsController', function () { ...quote, sourceToken: ETH_SWAPS_TOKEN_OBJECT.address, destinationToken: '0x1111111111111111111111111111111111111111', - trade: { value: '0x8ac7230489e80000' }, + trade: { + value: '0x8ac7230489e80000', + }, }), ); // 0.04 ETH fee included in trade value @@ -508,11 +560,18 @@ describe('SwapsController', function () { overallValueOfQuote: '1.6805', }, }; + // @ts-expect-error - we are removing a property that we know exists even if its optional in the type definition delete expectedResultQuotes[TEST_AGG_ID_1].isBestQuote; + // @ts-expect-error - we are removing a property that we know exists even if its optional in the type definition delete expectedResultQuotes[TEST_AGG_ID_1].savings; - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings(testInput); + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings( + testInput as Record, + ); + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; + expect(topAggId).toStrictEqual(TEST_AGG_ID_2); expect(resultQuotes).toStrictEqual(expectedResultQuotes); }); @@ -520,6 +579,7 @@ describe('SwapsController', function () { it('returns the top aggId and quotes with savings and fee values if passed necessary data and the source token is not ETH and an ETH fee is included in the trade value of what would be the best quote', async function () { const testInput = getTopQuoteAndSavingsMockQuotes(); // 0.04 ETH fee included in trade value + // @ts-expect-error - trade can be undefined but in this case since its mocked it will always be defined testInput[TEST_AGG_ID_1].trade.value = '0x8e1bc9bf040000'; const baseExpectedResultQuotes = getTopQuoteAndSavingsBaseExpectedResults(); @@ -543,11 +603,16 @@ describe('SwapsController', function () { }, }, }; + // @ts-expect-error - we are removing a property that we know exists even if its optional in the type definition delete expectedResultQuotes[TEST_AGG_ID_1].isBestQuote; + // @ts-expect-error - we are removing a property that we know exists even if its optional in the type definition delete expectedResultQuotes[TEST_AGG_ID_1].savings; - const [topAggId, resultQuotes] = - await swapsController._findTopQuoteAndCalculateSavings(testInput); + const topQuoteAndSavings = + await swapsController.getTopQuoteWithCalculatedSavings(testInput); + const topAggId = topQuoteAndSavings[0]; + const resultQuotes = topQuoteAndSavings[1]; + expect(topAggId).toStrictEqual(TEST_AGG_ID_2); expect(resultQuotes).toStrictEqual(expectedResultQuotes); }); @@ -555,38 +620,65 @@ describe('SwapsController', function () { describe('fetchAndSetQuotes', function () { it('returns null if fetchParams is not provided', async function () { + // @ts-expect-error - we are testing the case where fetchParams is not provided const quotes = await swapsController.fetchAndSetQuotes(undefined); expect(quotes).toStrictEqual(null); }); it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { + fetchTradesInfoStub.mockReset(); + const providerResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0x', + }; + const mainnetProvider = createTestProviderTools({ + scaffold: providerResultStub, + networkId: 1, + chainId: CHAIN_IDS.MAINNET as ChainId, + }).provider; + + swapsController = getSwapsController(mainnetProvider); + const fetchTradesInfoSpy = jest - .spyOn(swapsController, '_fetchTradesInfo') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') .mockReturnValue(getMockQuotes()); // Make it so approval is not required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); - const [newQuotes] = await swapsController.fetchAndSetQuotes( + const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, MOCK_FETCH_METADATA, ); + if (!fetchResponse?.[0]) { + throw new Error('Quotes should be defined'); + } + + const [newQuotes] = fetchResponse; + expect(newQuotes[TEST_AGG_ID_BEST]).toStrictEqual({ ...getMockQuotes()[TEST_AGG_ID_BEST], - sourceTokenInfo: undefined, destinationTokenInfo: { + address: '0xSomeAddress', symbol: 'FOO', decimals: 18, }, isBestQuote: true, // TODO: find a way to calculate these values dynamically - gasEstimate: 2000000, + gasEstimate: '2000000', gasEstimateWithRefund: '0xb8cae', savings: { fee: '-0.061067', @@ -599,6 +691,11 @@ describe('SwapsController', function () { overallValueOfQuote: '49.886464', metaMaskFeeInEth: '0.50505050505050505050505050505050505', ethValueOfTokens: '50', + sourceTokenInfo: { + address: '0xSomeOtherAddress', + decimals: 18, + symbol: 'BAR', + }, }); expect(fetchTradesInfoSpy).toHaveBeenCalledTimes(1); @@ -611,7 +708,7 @@ describe('SwapsController', function () { fetchTradesInfoStub.mockReset(); const OPTIMISM_MOCK_FETCH_METADATA = { ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.OPTIMISM, + chainId: CHAIN_IDS.OPTIMISM as ChainId, }; const optimismProviderResultStub = { // 1 gwei @@ -624,38 +721,49 @@ describe('SwapsController', function () { const optimismProvider = createTestProviderTools({ scaffold: optimismProviderResultStub, networkId: 10, - chainId: 10, + chainId: CHAIN_IDS.OPTIMISM as ChainId, }).provider; swapsController = getSwapsController(optimismProvider); const fetchTradesInfoSpy = jest - .spyOn(swapsController, '_fetchTradesInfo') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') .mockReturnValue(getMockQuotes()); // Make it so approval is not required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); - const [newQuotes] = await swapsController.fetchAndSetQuotes( + const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, OPTIMISM_MOCK_FETCH_METADATA, ); + if (!fetchResponse?.[0]) { + throw new Error('Quotes should be defined'); + } + + const [newQuotes] = fetchResponse; + expect(newQuotes[TEST_AGG_ID_BEST]).toStrictEqual({ ...getMockQuotes()[TEST_AGG_ID_BEST], - sourceTokenInfo: undefined, destinationTokenInfo: { + address: '0xSomeAddress', symbol: 'FOO', decimals: 18, }, isBestQuote: true, // TODO: find a way to calculate these values dynamically - gasEstimate: 2000000, + gasEstimate: '2000000', gasEstimateWithRefund: '0xb8cae', savings: { fee: '-0.061067', @@ -669,6 +777,11 @@ describe('SwapsController', function () { overallValueOfQuote: '49.886464', metaMaskFeeInEth: '0.50505050505050505050505050505050505', ethValueOfTokens: '50', + sourceTokenInfo: { + address: '0xSomeOtherAddress', + decimals: 18, + symbol: 'BAR', + }, }); expect(fetchTradesInfoSpy).toHaveBeenCalledTimes(1); @@ -678,17 +791,17 @@ describe('SwapsController', function () { }); it('performs the allowance check', async function () { - jest - .spyOn(swapsController, '_fetchTradesInfo') - .mockReturnValue(getMockQuotes()); - // Make it so approval is not required const getERC20AllowanceSpy = jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, @@ -705,20 +818,26 @@ describe('SwapsController', function () { it('gets the gas limit if approval is required', async function () { jest - .spyOn(swapsController, '_fetchTradesInfo') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') .mockReturnValue(MOCK_QUOTES_APPROVAL_REQUIRED); // Ensure approval is required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(0)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); const timedoutGasReturnResult = { gasLimit: 1000000 }; const timedoutGasReturnSpy = jest - .spyOn(swapsController, '_timedoutGasReturn') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_timedoutGasReturn') .mockReturnValue(timedoutGasReturnResult); await swapsController.fetchAndSetQuotes( @@ -736,22 +855,33 @@ describe('SwapsController', function () { it('marks the best quote', async function () { jest - .spyOn(swapsController, '_fetchTradesInfo') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') .mockReturnValue(getMockQuotes()); // Make it so approval is not required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); - const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes( + const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, MOCK_FETCH_METADATA, ); + if (!fetchResponse?.[0] || !fetchResponse[1]) { + throw new Error('newQuotes and topAggId should be defined'); + } + + const [newQuotes, topAggId] = fetchResponse; + expect(topAggId).toStrictEqual(TEST_AGG_ID_BEST); expect(newQuotes[topAggId].isBestQuote).toStrictEqual(true); }); @@ -769,234 +899,273 @@ describe('SwapsController', function () { .add((100e18).toString()) .toString(), }; + const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote }; - jest.spyOn(swapsController, '_fetchTradesInfo').mockReturnValue(quotes); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') + .mockReturnValue(quotes); // Make it so approval is not required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); - const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes( + const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, MOCK_FETCH_METADATA, ); + if (!fetchResponse?.[0] || !fetchResponse[1]) { + throw new Error('newQuotes and topAggId should be defined'); + } + + const [newQuotes, topAggId] = fetchResponse; + expect(topAggId).toStrictEqual(bestAggId); expect(newQuotes[topAggId].isBestQuote).toStrictEqual(true); }); it('does not mark as best quote if no conversion rate exists for destination token', async function () { jest - .spyOn(swapsController, '_fetchTradesInfo') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_fetchTradesInfo') .mockReturnValue(getMockQuotes()); // Make it so approval is not required jest - .spyOn(swapsController, '_getERC20Allowance') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_getERC20Allowance') .mockReturnValue(BigNumber.from(1)); // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + jest + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .spyOn(swapsController as any, '_setSwapsNetworkConfig') + .mockReturnValue(undefined); - swapsController.getTokenRatesState = () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (swapsController as any)._getTokenRatesState = () => ({ marketData: { '0x1': {}, }, }); - const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes( + const fetchResponse = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, MOCK_FETCH_METADATA, ); - expect(newQuotes[topAggId].isBestQuote).toStrictEqual(undefined); - }); - - it('should replace ethers instance when called with a different chainId than was current when the controller was instantiated', async function () { - fetchTradesInfoStub.mockReset(); - - const _swapsController = getSwapsController(); - - const currentEthersInstance = _swapsController._ethersProvider; - - // Make the network fetch error message disappear - jest - .spyOn(_swapsController, '_setSwapsNetworkConfig') - .mockReturnValue(); - - await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.GOERLI, - }); - - const newEthersInstance = _swapsController._ethersProvider; - expect(currentEthersInstance).not.toStrictEqual(newEthersInstance); - }); - - it('should not replace ethers instance when called with the same chainId that was current when the controller was instantiated', async function () { - const _swapsController = new SwapsController({ - getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, - provider, - getProviderConfig: MOCK_GET_PROVIDER_CONFIG, - getTokenRatesState: MOCK_TOKEN_RATES_STORE, - fetchTradesInfo: fetchTradesInfoStub, - getCurrentChainId: getCurrentChainIdStub, - }); - const currentEthersInstance = _swapsController._ethersProvider; - - // Make the network fetch error message disappear - jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + if (!fetchResponse?.[0] || !fetchResponse[1]) { + throw new Error('newQuotes and topAggId should be defined'); + } - await swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.MAINNET, - }); + const [newQuotes, topAggId] = fetchResponse; - const newEthersInstance = _swapsController._ethersProvider; - expect(currentEthersInstance).toStrictEqual(newEthersInstance); + expect(newQuotes[topAggId].isBestQuote).toStrictEqual(undefined); }); - it('should replace ethers instance, and _ethersProviderChainId, twice when called twice with two different chainIds, and successfully set the _ethersProviderChainId when returning to the original chain', async function () { - const _swapsController = new SwapsController({ - getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, - provider, - getProviderConfig: MOCK_GET_PROVIDER_CONFIG, - getTokenRatesState: MOCK_TOKEN_RATES_STORE, - fetchTradesInfo: fetchTradesInfoStub, - getCurrentChainId: getCurrentChainIdStub, - getLayer1GasFee: getLayer1GasFeeStub, - getNetworkClientId: getNetworkClientIdStub, - }); - const firstEthersInstance = _swapsController._ethersProvider; - const firstEthersProviderChainId = - _swapsController._ethersProviderChainId; - - // Make the network fetch error message disappear - jest - .spyOn(_swapsController, '_setSwapsNetworkConfig') - .mockReturnValue(); - - await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.GOERLI, - }); - - const secondEthersInstance = _swapsController._ethersProvider; - const secondEthersProviderChainId = - _swapsController._ethersProviderChainId; - - expect(firstEthersInstance).not.toStrictEqual(secondEthersInstance); - expect(firstEthersInstance).not.toStrictEqual( - secondEthersProviderChainId, - ); - - await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.LOCALHOST, - }); - - const thirdEthersInstance = _swapsController._ethersProvider; - const thirdEthersProviderChainId = - _swapsController._ethersProviderChainId; - - expect(firstEthersProviderChainId).not.toStrictEqual( - thirdEthersInstance, - ); - expect(secondEthersInstance).not.toStrictEqual(thirdEthersInstance); - expect(firstEthersInstance).not.toStrictEqual( - thirdEthersProviderChainId, - ); - expect(secondEthersProviderChainId).not.toStrictEqual( - thirdEthersProviderChainId, - ); - - await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { - ...MOCK_FETCH_METADATA, - chainId: CHAIN_IDS.MAINNET, - }); - - const lastEthersProviderChainId = - _swapsController._ethersProviderChainId; - - expect(firstEthersProviderChainId).toStrictEqual( - lastEthersProviderChainId, - ); - }); + // TODO: Re think how to test this without exposing internal state + + // it('should replace ethers instance when called with a different chainId than was current when the controller was instantiated', async function () { + // fetchTradesInfoStub.mockReset(); + + // const _swapsController = getSwapsController(); + + // const currentEthersInstance = _swapsController._ethersProvider; + + // // Make the network fetch error message disappear + // jest + // .spyOn(_swapsController, '_setSwapsNetworkConfig') + // .mockReturnValue(); + + // await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + // ...MOCK_FETCH_METADATA, + // chainId: CHAIN_IDS.GOERLI, + // }); + + // const newEthersInstance = _swapsController._ethersProvider; + // expect(currentEthersInstance).not.toStrictEqual(newEthersInstance); + // }); + + // it('should not replace ethers instance when called with the same chainId that was current when the controller was instantiated', async function () { + // const _swapsController = new SwapsController({ + // getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, + // provider, + // fetchTradesInfo: fetchTradesInfoStub, + // }); + // const currentEthersInstance = _swapsController._ethersProvider; + + // // Make the network fetch error message disappear + // jest.spyOn(swapsController, '_setSwapsNetworkConfig').mockReturnValue(); + + // await swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + // ...MOCK_FETCH_METADATA, + // chainId: CHAIN_IDS.MAINNET, + // }); + + // const newEthersInstance = _swapsController._ethersProvider; + // expect(currentEthersInstance).toStrictEqual(newEthersInstance); + // }); + + // it('should replace ethers instance, and _ethersProviderChainId, twice when called twice with two different chainIds, and successfully set the _ethersProviderChainId when returning to the original chain', async function () { + // const _swapsController = new SwapsController({ + // getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, + // provider, + // fetchTradesInfo: fetchTradesInfoStub, + // getLayer1GasFee: getLayer1GasFeeStub, + // }); + // const firstEthersInstance = _swapsController._ethersProvider; + // const firstEthersProviderChainId = + // _swapsController._ethersProviderChainId; + + // // Make the network fetch error message disappear + // jest + // .spyOn(_swapsController, '_setSwapsNetworkConfig') + // .mockReturnValue(); + + // await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + // ...MOCK_FETCH_METADATA, + // chainId: CHAIN_IDS.GOERLI, + // }); + + // const secondEthersInstance = _swapsController._ethersProvider; + // const secondEthersProviderChainId = + // _swapsController._ethersProviderChainId; + + // expect(firstEthersInstance).not.toStrictEqual(secondEthersInstance); + // expect(firstEthersInstance).not.toStrictEqual( + // secondEthersProviderChainId, + // ); + + // await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + // ...MOCK_FETCH_METADATA, + // chainId: CHAIN_IDS.LOCALHOST, + // }); + + // const thirdEthersInstance = _swapsController._ethersProvider; + // const thirdEthersProviderChainId = + // _swapsController._ethersProviderChainId; + + // expect(firstEthersProviderChainId).not.toStrictEqual( + // thirdEthersInstance, + // ); + // expect(secondEthersInstance).not.toStrictEqual(thirdEthersInstance); + // expect(firstEthersInstance).not.toStrictEqual( + // thirdEthersProviderChainId, + // ); + // expect(secondEthersProviderChainId).not.toStrictEqual( + // thirdEthersProviderChainId, + // ); + + // await _swapsController.fetchAndSetQuotes(MOCK_FETCH_PARAMS, { + // ...MOCK_FETCH_METADATA, + // chainId: CHAIN_IDS.MAINNET, + // }); + + // const lastEthersProviderChainId = + // _swapsController._ethersProviderChainId; + + // expect(firstEthersProviderChainId).toStrictEqual( + // lastEthersProviderChainId, + // ); + // }); }); describe('resetSwapsState', function () { it('resets the swaps state correctly', function () { - const { swapsState: old } = swapsController.store.getState(); + const oldState = swapsController.state; swapsController.resetSwapsState(); - const { swapsState } = swapsController.store.getState(); + const newState = swapsController.state; - expect(swapsState).toStrictEqual({ - ...EMPTY_INIT_STATE.swapsState, - tokens: old.tokens, - swapsQuoteRefreshTime: old.swapsQuoteRefreshTime, + expect(newState.swapsState).toStrictEqual({ + ...getDefaultSwapsControllerState().swapsState, + tokens: oldState.swapsState.tokens, + swapsQuoteRefreshTime: oldState.swapsState.swapsQuoteRefreshTime, swapsQuotePrefetchingRefreshTime: - old.swapsQuotePrefetchingRefreshTime, + oldState.swapsState.swapsQuotePrefetchingRefreshTime, swapsStxGetTransactionsRefreshTime: - old.swapsStxGetTransactionsRefreshTime, - swapsStxBatchStatusRefreshTime: old.swapsStxBatchStatusRefreshTime, + oldState.swapsState.swapsStxGetTransactionsRefreshTime, + swapsStxBatchStatusRefreshTime: + oldState.swapsState.swapsStxBatchStatusRefreshTime, + swapsStxStatusDeadline: oldState.swapsState.swapsStxStatusDeadline, }); }); - it('clears polling timeout', function () { - swapsController._pollingTimeout = setTimeout(() => { - throw new Error('Polling timeout not cleared'); - }, POLLING_TIMEOUT); + // it('clears polling timeout', function () { + // swapsController._pollingTimeout = setTimeout(() => { + // throw new Error('Polling timeout not cleared'); + // }, POLLING_TIMEOUT); - // Reseting swaps state should clear the polling timeout - swapsController.resetSwapsState(); + // // Reseting swaps state should clear the polling timeout + // swapsController.resetSwapsState(); - // Verify by ensuring the error is not thrown, indicating that the timer was cleared - expect(jest.runOnlyPendingTimers).not.toThrow(); - }); + // // Verify by ensuring the error is not thrown, indicating that the timer was cleared + // expect(jest.runOnlyPendingTimers).not.toThrow(); + // }); }); describe('stopPollingForQuotes', function () { - it('clears polling timeout', function () { - swapsController._pollingTimeout = setTimeout(() => { - throw new Error('Polling timeout not cleared'); - }, POLLING_TIMEOUT); + // TODO: Re think how to test this without exposing internal state - // Stop polling for quotes should clear the polling timeout - swapsController.stopPollingForQuotes(); + // it('clears polling timeout', function () { + // swapsController._pollingTimeout = setTimeout(() => { + // throw new Error('Polling timeout not cleared'); + // }, POLLING_TIMEOUT); - // Verify by ensuring the error is not thrown, indicating that the timer was cleared - expect(jest.runOnlyPendingTimers).not.toThrow(); - }); + // // Stop polling for quotes should clear the polling timeout + // swapsController.stopPollingForQuotes(); + + // // Verify by ensuring the error is not thrown, indicating that the timer was cleared + // expect(jest.runOnlyPendingTimers).not.toThrow(); + // }); it('resets quotes state correctly', function () { swapsController.stopPollingForQuotes(); - const { swapsState } = swapsController.store.getState(); - expect(swapsState.quotes).toStrictEqual({}); - expect(swapsState.quotesLastFetched).toStrictEqual(null); + const swapsState = swapsController.state; + expect(swapsState.swapsState.quotes).toStrictEqual({}); + expect(swapsState.swapsState.quotesLastFetched).toStrictEqual(null); }); }); describe('resetPostFetchState', function () { - it('clears polling timeout', function () { - swapsController._pollingTimeout = setTimeout(() => { - throw new Error('Polling timeout not cleared'); - }, POLLING_TIMEOUT); + // TODO: Re think how to test this without exposing internal state - // Reset post fetch state should clear the polling timeout - swapsController.resetPostFetchState(); + // it('clears polling timeout', function () { + // swapsController._pollingTimeout = setTimeout(() => { + // throw new Error('Polling timeout not cleared'); + // }, POLLING_TIMEOUT); - // Verify by ensuring the error is not thrown, indicating that the timer was cleared - expect(jest.runOnlyPendingTimers).not.toThrow(); - }); + // // Reset post fetch state should clear the polling timeout + // swapsController.resetPostFetchState(); + + // // Verify by ensuring the error is not thrown, indicating that the timer was cleared + // expect(jest.runOnlyPendingTimers).not.toThrow(); + // }); it('updates state correctly', function () { - const tokens = 'test'; - const fetchParams = 'test'; + const tokens = ['']; + const fetchParams: FetchTradesInfoParams & { + metaData: FetchTradesInfoParamsMetadata; + } = { + sourceToken: '', + destinationToken: '', + sourceDecimals: 18, + slippage: 2, + value: '0x0', + fromAddress: '', + exchangeList: 'zeroExV1', + balanceError: false, + metaData: {} as FetchTradesInfoParamsMetadata, + }; const swapsFeatureIsLive = false; const swapsFeatureFlags = {}; const swapsQuoteRefreshTime = 0; @@ -1004,8 +1173,9 @@ describe('SwapsController', function () { const swapsStxBatchStatusRefreshTime = 0; const swapsStxGetTransactionsRefreshTime = 0; const swapsStxStatusDeadline = 0; - swapsController.store.updateState({ + swapsController.__test__updateState({ swapsState: { + ...swapsController.state.swapsState, tokens, fetchParams, swapsFeatureIsLive, @@ -1020,9 +1190,9 @@ describe('SwapsController', function () { swapsController.resetPostFetchState(); - const { swapsState } = swapsController.store.getState(); + const { swapsState } = swapsController.state; expect(swapsState).toStrictEqual({ - ...EMPTY_INIT_STATE.swapsState, + ...getDefaultSwapsControllerState().swapsState, tokens, fetchParams, swapsFeatureIsLive, @@ -1061,7 +1231,7 @@ describe('SwapsController', function () { metaMaskFeeInEth: '6', ethValueOfTokens: '0.6', }, - ]; + ] as Quote[]; const median = getMedianEthValueQuote(values); expect(median).toStrictEqual(expectedResult); @@ -1099,7 +1269,7 @@ describe('SwapsController', function () { metaMaskFeeInEth: '6', ethValueOfTokens: '0.6', }, - ]; + ] as Quote[]; const median = getMedianEthValueQuote(values); expect(median).toStrictEqual(expectedResult); @@ -1155,7 +1325,7 @@ describe('SwapsController', function () { ethFee: '2', metaMaskFeeInEth: '0.8', }, - ]; + ] as Quote[]; const median = getMedianEthValueQuote(values); expect(median).toStrictEqual(expectedResult); @@ -1205,136 +1375,181 @@ describe('SwapsController', function () { ethFee: '2', metaMaskFeeInEth: '0.8', }, - ]; + ] as Quote[]; const median = getMedianEthValueQuote(values); expect(median).toStrictEqual(expectedResult); }); - it('throws on empty or non-array sample', function () { + it('throws on empty array', function () { expect(() => getMedianEthValueQuote([])).toThrow( 'Expected non-empty array param.', ); - - expect(() => getMedianEthValueQuote()).toThrow( - 'Expected non-empty array param.', - ); - - expect(() => getMedianEthValueQuote({})).toThrow( - 'Expected non-empty array param.', - ); }); }); }); }); -function getMockQuotes() { +function getMockQuotes(): Record { return { [TEST_AGG_ID_1]: { - trade: { - from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', - value: '0x0', - gas: '0x61a80', // 4e5 - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', - }, - sourceAmount: '10000000000000000000', // 10e18 - destinationAmount: '20000000000000000000', // 20e18 - error: null, - sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - approvalNeeded: null, - maxGas: 600000, - averageGas: 120000, - estimatedRefund: 80000, - fetchTime: 607, aggregator: TEST_AGG_ID_1, aggType: 'AGG', - slippage: 2, - sourceTokenInfo: { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - symbol: 'DAI', - decimals: 18, - iconUrl: 'https://foo.bar/logo.png', - }, + approvalNeeded: null, + averageGas: 120000, + destinationAmount: '20000000000000000000', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', destinationTokenInfo: { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', symbol: 'USDC', decimals: 18, }, + destinationTokenRate: 2, + error: null, + estimatedRefund: '80000', + ethFee: '0.006', + ethValueOfTokens: '0.1', fee: 1, - }, - - [TEST_AGG_ID_BEST]: { + fetchTime: 607, + gasEstimate: '120000', + gasEstimateWithRefund: '100000', + gasMultiplier: 1.1, + hasRoute: true, + maxGas: 600000, + metaMaskFeeInEth: '0.001', + overallValueOfQuote: '19.994', + priceSlippage: { + bucket: 'low', + calculationError: '', + destinationAmountInETH: 0.1, + destinationAmountInNativeCurrency: 20, + ratio: 0.995, + sourceAmountInETH: 0.05, + sourceAmountInNativeCurrency: 10, + sourceAmountInUSD: 10, + destinationAmountInUSD: 20, + }, + quoteRefreshSeconds: 60, + sourceAmount: '10000000000000000000', + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + sourceTokenRate: 1, trade: { + data: '0x', from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', value: '0x0', gas: '0x61a80', - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', }, - sourceAmount: '10000000000000000000', - destinationAmount: '25000000000000000000', // 25e18 - error: null, - sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - approvalNeeded: null, - maxGas: 1100000, - averageGas: 411000, - estimatedRefund: 343090, - fetchTime: 1003, + }, + [TEST_AGG_ID_BEST]: { aggregator: TEST_AGG_ID_BEST, aggType: 'AGG', - slippage: 2, - sourceTokenInfo: { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - symbol: 'DAI', - decimals: 18, - iconUrl: 'https://foo.bar/logo.png', - }, + approvalNeeded: null, + averageGas: 411000, + destinationAmount: '25000000000000000000', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', destinationTokenInfo: { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', symbol: 'USDC', decimals: 18, }, + destinationTokenRate: 2.5, + error: null, + estimatedRefund: '343090', + ethFee: '0.008', + ethValueOfTokens: '0.125', fee: 1, - }, - - [TEST_AGG_ID_2]: { + fetchTime: 1003, + gasEstimate: '411000', + gasEstimateWithRefund: '380000', + gasMultiplier: 1.2, + hasRoute: true, + maxGas: 1100000, + metaMaskFeeInEth: '0.0015', + overallValueOfQuote: '24.9905', + priceSlippage: { + bucket: 'medium', + calculationError: '', + destinationAmountInETH: 0.125, + destinationAmountInNativeCurrency: 25, + ratio: 0.98, + sourceAmountInETH: 0.05, + sourceAmountInNativeCurrency: 10, + sourceAmountInUSD: 10, + destinationAmountInUSD: 25, + }, + quoteRefreshSeconds: 60, + sourceAmount: '10000000000000000000', + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + sourceTokenRate: 1, trade: { + data: '0x', from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', value: '0x0', gas: '0x61a80', - to: '0x881D40237659C251811CEC9c364ef91dC08D300C', }, - sourceAmount: '10000000000000000000', - destinationAmount: '22000000000000000000', // 22e18 - error: null, - sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - approvalNeeded: null, - maxGas: 368000, - averageGas: 197000, - estimatedRefund: 18205, - fetchTime: 1354, + savings: { + total: '0.005', + performance: '0.003', + fee: '0.002', + metaMaskFee: '0.001', + medianMetaMaskFee: '0.0012', + }, + }, + [TEST_AGG_ID_2]: { aggregator: TEST_AGG_ID_2, aggType: 'AGG', - slippage: 2, - sourceTokenInfo: { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - symbol: 'DAI', - decimals: 18, - iconUrl: 'https://foo.bar/logo.png', - }, + approvalNeeded: null, + averageGas: 197000, + destinationAmount: '22000000000000000000', + destinationToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', destinationTokenInfo: { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', symbol: 'USDC', decimals: 18, }, + destinationTokenRate: 2.2, + error: null, + estimatedRefund: '18205', + ethFee: '0.007', + ethValueOfTokens: '0.11', fee: 1, + fetchTime: 1354, + gasEstimate: '197000', + gasEstimateWithRefund: '190000', + gasMultiplier: 1.15, + hasRoute: true, + maxGas: 368000, + metaMaskFeeInEth: '0.00125', + overallValueOfQuote: '21.99175', + priceSlippage: { + bucket: 'high', + calculationError: '', + destinationAmountInETH: 0.11, + destinationAmountInNativeCurrency: 22, + ratio: 0.99, + sourceAmountInETH: 0.05, + sourceAmountInNativeCurrency: 10, + sourceAmountInUSD: 10, + destinationAmountInUSD: 22, + }, + quoteRefreshSeconds: 60, + sourceAmount: '10000000000000000000', + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + sourceTokenRate: 1, + trade: { + data: '0x', + from: '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', + to: '0x881D40237659C251811CEC9c364ef91dC08D300C', + value: '0x0', + gas: '0x61a80', + }, }, }; } -function getTopQuoteAndSavingsMockQuotes() { +function getTopQuoteAndSavingsMockQuotes(): Record { // These destination amounts are calculated using the following "pre-fee" amounts // TEST_AGG_ID_1: 20.5 // TEST_AGG_ID_2: 20.4 @@ -1350,84 +1565,108 @@ function getTopQuoteAndSavingsMockQuotes() { gasEstimate: '0x186a0', destinationAmount: '20295000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, [TEST_AGG_ID_2]: { aggregator: TEST_AGG_ID_2, approvalNeeded: null, gasEstimate: '0x30d40', destinationAmount: '20196000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, [TEST_AGG_ID_3]: { aggregator: TEST_AGG_ID_3, approvalNeeded: null, gasEstimate: '0x493e0', destinationAmount: '19998000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, [TEST_AGG_ID_4]: { aggregator: TEST_AGG_ID_4, approvalNeeded: null, gasEstimate: '0x61a80', destinationAmount: '19800000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, [TEST_AGG_ID_5]: { aggregator: TEST_AGG_ID_5, approvalNeeded: null, gasEstimate: '0x7a120', destinationAmount: '19602000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, [TEST_AGG_ID_6]: { aggregator: TEST_AGG_ID_6, approvalNeeded: null, gasEstimate: '0x927c0', destinationAmount: '19305000000000000000', destinationToken: '0x1111111111111111111111111111111111111111', - destinationTokenInfo: { decimals: 18 }, + destinationTokenInfo: { + decimals: 18, + address: '0xsomeERC20TokenAddress', + symbol: 'FOO', + }, sourceAmount: '10000000000000000000', sourceToken: '0xsomeERC20TokenAddress', trade: { value: '0x0', }, fee: 1, - }, + } as Quote, }; } diff --git a/app/scripts/controllers/swaps/swaps.types.ts b/app/scripts/controllers/swaps/swaps.types.ts new file mode 100644 index 000000000000..44e4d4939742 --- /dev/null +++ b/app/scripts/controllers/swaps/swaps.types.ts @@ -0,0 +1,430 @@ +import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'; +import { TokenRatesControllerGetStateAction } from '@metamask/assets-controllers'; +import { + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import type { ChainId } from '@metamask/controller-utils'; +import { GasFeeState } from '@metamask/gas-fee-controller'; +import { + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, +} from '@metamask/network-controller'; +import { TransactionParams } from '@metamask/transaction-controller'; +import type { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { fetchTradesInfo as defaultFetchTradesInfo } from '../../../../shared/lib/swaps-utils'; +import { controllerName } from './swaps.constants'; +import SwapsController from '.'; + +export type SwapsControllerState = { + swapsState: { + quotes: Record; + quotesPollingLimitEnabled: boolean; + fetchParams: + | (FetchTradesInfoParams & { + metaData: FetchTradesInfoParamsMetadata; + }) + | null; + tokens: string[] | null; + tradeTxId: string | null; + approveTxId: string | null; + quotesLastFetched: number | null; + customMaxGas: string; + customGasPrice: string | null; + customMaxFeePerGas: string | null; + customMaxPriorityFeePerGas: string | null; + swapsUserFeeLevel: string; + selectedAggId: string | null; + customApproveTxData: string; + errorKey: string; + topAggId: string | null; + routeState: string; + swapsFeatureIsLive: boolean; + saveFetchedQuotes: boolean; + swapsQuoteRefreshTime: number; + swapsQuotePrefetchingRefreshTime: number; + swapsStxBatchStatusRefreshTime: number; + swapsStxStatusDeadline?: number; + swapsStxGetTransactionsRefreshTime: number; + swapsStxMaxFeeMultiplier: number; + swapsFeatureFlags: Record; + }; +}; + +/** + * The action that fetches the state of the {@link SwapsController}. + */ +export type SwapsControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + SwapsControllerState +>; + +/** + * The event that {@link SwapsController} can emit. + */ +export type SwapsControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + SwapsControllerState +>; + +/** + * The external actions available to the {@link SwapsController}. + */ +export type AllowedActions = + | NetworkControllerGetStateAction + | NetworkControllerGetNetworkClientByIdAction + | TokenRatesControllerGetStateAction; + +/** + * The internal actions available to the SwapsController. + */ +export type SwapsControllerActions = + | SwapsControllerGetStateAction + | SwapsControllerFetchAndSetQuotesAction + | SwapsControllerSetSelectedQuoteAggIdAction + | SwapsControllerResetSwapsStateAction + | SwapsControllerSetSwapsTokensAction + | SwapsControllerClearSwapsQuotesAction + | SwapsControllerSetApproveTxIdAction + | SwapsControllerSetTradeTxIdAction + | SwapsControllerSetSwapsTxGasPriceAction + | SwapsControllerSetSwapsTxGasLimitAction + | SwapsControllerSetSwapsTxMaxFeePerGasAction + | SwapsControllerSetSwapsTxMaxFeePriorityPerGasAction + | SwapsControllerSafeRefetchQuotesAction + | SwapsControllerStopPollingForQuotesAction + | SwapsControllerSetBackgroundSwapRouteStateAction + | SwapsControllerResetPostFetchStateAction + | SwapsControllerSetSwapsErrorKeyAction + | SwapsControllerSetInitialGasEstimateAction + | SwapsControllerSetCustomApproveTxDataAction + | SwapsControllerSetSwapsLivenessAction + | SwapsControllerSetSwapsFeatureFlagsAction + | SwapsControllerSetSwapsUserFeeLevelAction + | SwapsControllerSetSwapsQuotesPollingLimitEnabledAction; + +/** + * The internal events available to the SwapsController. + */ +export type SwapsControllerEvents = SwapsControllerStateChangeEvent; + +/** + * The messenger for the SwapsController. + */ +export type SwapsControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + SwapsControllerActions | AllowedActions, + SwapsControllerEvents, + AllowedActions['type'], + never +>; + +/** + * The action that fetches and sets quotes in the {@link SwapsController}. + */ +export type SwapsControllerFetchAndSetQuotesAction = { + type: `SwapsController:fetchAndSetQuotes`; + handler: SwapsController['fetchAndSetQuotes']; +}; + +/** + * The action that sets the selected quote aggregation ID in the {@link SwapsController}. + */ +export type SwapsControllerSetSelectedQuoteAggIdAction = { + type: `SwapsController:setSelectedQuoteAggId`; + handler: SwapsController['setSelectedQuoteAggId']; +}; + +/** + * The action that resets the swaps state in the {@link SwapsController}. + */ +export type SwapsControllerResetSwapsStateAction = { + type: `SwapsController:resetSwapsState`; + handler: SwapsController['resetSwapsState']; +}; + +/** + * The action that sets the swaps tokens in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsTokensAction = { + type: `SwapsController:setSwapsTokens`; + handler: SwapsController['setSwapsTokens']; +}; + +/** + * The action that clears the swaps quotes in the {@link SwapsController}. + */ +export type SwapsControllerClearSwapsQuotesAction = { + type: `SwapsController:clearSwapsQuotes`; + handler: SwapsController['clearSwapsQuotes']; +}; + +/** + * The action that sets the approve transaction ID in the {@link SwapsController}. + */ +export type SwapsControllerSetApproveTxIdAction = { + type: `SwapsController:setApproveTxId`; + handler: SwapsController['setApproveTxId']; +}; + +/** + * The action that sets the trade transaction ID in the {@link SwapsController}. + */ +export type SwapsControllerSetTradeTxIdAction = { + type: `SwapsController:setTradeTxId`; + handler: SwapsController['setTradeTxId']; +}; + +/** + * The action that sets the swaps transaction gas price in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsTxGasPriceAction = { + type: `SwapsController:setSwapsTxGasPrice`; + handler: SwapsController['setSwapsTxGasPrice']; +}; + +/** + * The action that sets the swaps transaction gas limit in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsTxGasLimitAction = { + type: `SwapsController:setSwapsTxGasLimit`; + handler: SwapsController['setSwapsTxGasLimit']; +}; + +/** + * The action that sets the swaps transaction max fee per gas in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsTxMaxFeePerGasAction = { + type: `SwapsController:setSwapsTxMaxFeePerGas`; + handler: SwapsController['setSwapsTxMaxFeePerGas']; +}; + +/** + * The action that sets the swaps transaction max fee priority per gas in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsTxMaxFeePriorityPerGasAction = { + type: `SwapsController:setSwapsTxMaxFeePriorityPerGas`; + handler: SwapsController['setSwapsTxMaxFeePriorityPerGas']; +}; + +/** + * The action that safely refetches quotes in the {@link SwapsController}. + */ +export type SwapsControllerSafeRefetchQuotesAction = { + type: `SwapsController:safeRefetchQuotes`; + handler: SwapsController['safeRefetchQuotes']; +}; + +/** + * The action that stops polling for quotes in the {@link SwapsController}. + */ +export type SwapsControllerStopPollingForQuotesAction = { + type: `SwapsController:stopPollingForQuotes`; + handler: SwapsController['stopPollingForQuotes']; +}; + +/** + * The action that sets the background swap route state in the {@link SwapsController}. + */ +export type SwapsControllerSetBackgroundSwapRouteStateAction = { + type: `SwapsController:setBackgroundSwapRouteState`; + handler: SwapsController['setBackgroundSwapRouteState']; +}; + +/** + * The action that resets the post-fetch state in the {@link SwapsController}. + */ +export type SwapsControllerResetPostFetchStateAction = { + type: `SwapsController:resetPostFetchState`; + handler: SwapsController['resetPostFetchState']; +}; + +/** + * The action that sets the swaps error key in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsErrorKeyAction = { + type: `SwapsController:setSwapsErrorKey`; + handler: SwapsController['setSwapsErrorKey']; +}; + +/** + * The action that sets the initial gas estimate in the {@link SwapsController}. + */ +export type SwapsControllerSetInitialGasEstimateAction = { + type: `SwapsController:setInitialGasEstimate`; + handler: SwapsController['setInitialGasEstimate']; +}; + +/** + * The action that sets custom approve transaction data in the {@link SwapsController}. + */ +export type SwapsControllerSetCustomApproveTxDataAction = { + type: `SwapsController:setCustomApproveTxData`; + handler: SwapsController['setCustomApproveTxData']; +}; + +/** + * The action that sets the swaps liveness in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsLivenessAction = { + type: `SwapsController:setSwapsLiveness`; + handler: SwapsController['setSwapsLiveness']; +}; + +/** + * The action that sets the swaps feature flags in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsFeatureFlagsAction = { + type: `SwapsController:setSwapsFeatureFlags`; + handler: SwapsController['setSwapsFeatureFlags']; +}; + +/** + * The action that sets the swaps user fee level in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsUserFeeLevelAction = { + type: `SwapsController:setSwapsUserFeeLevel`; + handler: SwapsController['setSwapsUserFeeLevel']; +}; + +/** + * The action that sets the swaps quotes polling limit enabled in the {@link SwapsController}. + */ +export type SwapsControllerSetSwapsQuotesPollingLimitEnabledAction = { + type: `SwapsController:setSwapsQuotesPollingLimitEnabled`; + handler: SwapsController['setSwapsQuotesPollingLimitEnabled']; +}; + +export type FetchTradesInfoParams = { + slippage: number; + sourceToken: string; + sourceDecimals: number; + destinationToken: string; + value: string; + fromAddress: string; + exchangeList: string; + balanceError: boolean; +}; + +export type FetchTradesInfoParamsMetadata = { + chainId: ChainId; + sourceTokenInfo: { + address: string; + symbol: string; + decimals: number; + iconUrl?: string; + }; + destinationTokenInfo: { + address: string; + symbol: string; + decimals: number; + iconUrl?: string; + }; +}; + +export type QuoteRequest = { + chainId: number; + destinationToken: string; + slippage: number; + sourceAmount: string; + sourceToken: string; + walletAddress: string; +}; + +export type SwapsControllerOptions = { + getBufferedGasLimit: ( + params: { + txParams: { + value: string; + data: string; + to: string; + from: string; + }; + }, + factor: number, + ) => Promise<{ gasLimit: string; simulationFails: boolean }>; + provider: ExternalProvider | JsonRpcFetchFunc; + fetchTradesInfo: typeof defaultFetchTradesInfo; + getLayer1GasFee: (params: { + transactionParams: TransactionParams; + chainId: ChainId; + }) => Promise; + getEIP1559GasFeeEstimates: () => Promise; + trackMetaMetricsEvent: (event: { + event: MetaMetricsEventName; + category: MetaMetricsEventCategory; + properties: Record; + }) => void; + messenger: SwapsControllerMessenger; +}; + +export type AggType = 'DEX' | 'RFQ' | 'CONTRACT' | 'CNT' | 'AGG'; + +export type PriceSlippage = { + bucket: 'low' | 'medium' | 'high'; + calculationError: string; + destinationAmountInETH: number | null; + destinationAmountInNativeCurrency: number | null; + ratio: number | null; + sourceAmountInETH: number | null; + sourceAmountInNativeCurrency: number | null; + sourceAmountInUSD: number | null; + destinationAmountInUSD: number | null; +}; + +export type Trade = { + data: string; + from: string; + to: string; + value: string; + gas?: string; +}; + +export type QuoteSavings = { + performance: string; + fee: string; + metaMaskFee: string; + medianMetaMaskFee: string; + total: string; +}; +export type Quote = { + aggregator: string; + aggType: AggType; + approvalNeeded?: Trade | null; + averageGas: number; + destinationAmount: string | null; + destinationToken: string; + destinationTokenInfo: { + address: string; + symbol: string; + decimals: number; + iconUrl?: string; + }; + destinationTokenRate: number | null; + error: null | string; + estimatedRefund: string; + ethFee: string; + ethValueOfTokens: string; + fee: number; + fetchTime: number; + gasEstimate: string; + gasEstimateWithRefund: string; + gasMultiplier: number; + hasRoute: boolean; + isBestQuote?: boolean; + maxGas: number; + metaMaskFeeInEth: string; + multiLayerL1TradeFeeTotal?: string; + overallValueOfQuote: string; + priceSlippage: PriceSlippage; + quoteRefreshSeconds: number; + savings?: QuoteSavings; + sourceAmount: string; + sourceToken: string; + sourceTokenRate: number; + trade: null | Trade; +}; diff --git a/app/scripts/controllers/swaps.utils.ts b/app/scripts/controllers/swaps/swaps.utils.ts similarity index 100% rename from app/scripts/controllers/swaps.utils.ts rename to app/scripts/controllers/swaps/swaps.utils.ts diff --git a/app/scripts/controllers/user-storage/encryption/cache.ts b/app/scripts/controllers/user-storage/encryption/cache.ts deleted file mode 100644 index 7e9d80c78442..000000000000 --- a/app/scripts/controllers/user-storage/encryption/cache.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { base64ToByteArray, byteArrayToBase64 } from './utils'; - -type CachedEntry = { - salt: Uint8Array; - base64Salt: string; - key: Uint8Array; -}; - -const MAX_PASSWORD_CACHES = 3; -const MAX_SALT_CACHES = 10; - -/** - * In-Memory Caching derived keys based from a given salt and password. - */ -type PasswordMemCachedKDF = { - [hashedPassword: string]: Map; -}; -let inMemCachedKDF: PasswordMemCachedKDF = {}; -const getPasswordCache = (hashedPassword: string) => { - inMemCachedKDF[hashedPassword] ??= new Map(); - return inMemCachedKDF[hashedPassword]; -}; - -/** - * Returns a given cached derived key from a hashed password and salt - * - * @param hashedPassword - hashed password for cache lookup - * @param salt - provide salt to receive cached key - * @returns cached key - */ -export function getCachedKeyBySalt( - hashedPassword: string, - salt: Uint8Array, -): CachedEntry | undefined { - const cache = getPasswordCache(hashedPassword); - const base64Salt = byteArrayToBase64(salt); - const cachedKey = cache.get(base64Salt); - if (!cachedKey) { - return undefined; - } - - return { - salt, - base64Salt, - key: cachedKey, - }; -} - -/** - * Gets any cached key for a given hashed password - * - * @param hashedPassword - hashed password for cache lookup - * @returns any (the first) cached key - */ -export function getAnyCachedKey( - hashedPassword: string, -): CachedEntry | undefined { - const cache = getPasswordCache(hashedPassword); - - // Takes 1 item from an Iterator via Map.entries() - const cachedEntry: [string, Uint8Array] | undefined = cache - .entries() - .next().value; - - if (!cachedEntry) { - return undefined; - } - - const base64Salt = cachedEntry[0]; - const bytesSalt = base64ToByteArray(base64Salt); - return { - salt: bytesSalt, - base64Salt, - key: cachedEntry[1], - }; -} - -/** - * Sets a key to the in memory cache. - * We have set an arbitrary size of 10 cached keys per hashed password. - * - * @param hashedPassword - hashed password for cache lookup - * @param salt - salt to set new derived key - * @param key - derived key we are setting - */ -export function setCachedKey( - hashedPassword: string, - salt: Uint8Array, - key: Uint8Array, -): void { - // Max password caches - if (Object.keys(inMemCachedKDF).length > MAX_PASSWORD_CACHES) { - inMemCachedKDF = {}; - } - - const cache = getPasswordCache(hashedPassword); - const base64Salt = byteArrayToBase64(salt); - - // Max salt caches - if (cache.size > MAX_SALT_CACHES) { - cache.clear(); - } - - cache.set(base64Salt, key); -} diff --git a/app/scripts/controllers/user-storage/encryption/encryption.test.ts b/app/scripts/controllers/user-storage/encryption/encryption.test.ts deleted file mode 100644 index dbe34a73a5e8..000000000000 --- a/app/scripts/controllers/user-storage/encryption/encryption.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createMockFullUserStorage } from '../../metamask-notifications/mocks/mock-notification-user-storage'; -import encryption, { createSHA256Hash } from './encryption'; - -describe('encryption tests', () => { - const PASSWORD = '123'; - const DATA1 = 'Hello World'; - const DATA2 = JSON.stringify({ foo: 'bar' }); - const DATA3 = JSON.stringify(createMockFullUserStorage()); - - it('Should encrypt and decrypt data', () => { - function actEncryptDecrypt(data: string) { - const encryptedString = encryption.encryptString(data, PASSWORD); - const decryptString = encryption.decryptString(encryptedString, PASSWORD); - return decryptString; - } - - expect(actEncryptDecrypt(DATA1)).toBe(DATA1); - expect(actEncryptDecrypt(DATA2)).toBe(DATA2); - expect(actEncryptDecrypt(DATA3)).toBe(DATA3); - }); - - it('Should decrypt some existing data', () => { - const encryptedData = `{"v":"1","t":"scrypt","d":"WNEp1QXUZsxCfW9b27uzZ18CtsMvKP6+cqLq8NLAItXeYcFcUjtKprfvedHxf5JN9Q7pe50qnA==","o":{"N":131072,"r":8,"p":1,"dkLen":16},"saltLen":16}`; - const result = encryption.decryptString(encryptedData, PASSWORD); - expect(result).toBe(DATA1); - }); - - it('Should sha-256 hash a value and should be deterministic', () => { - const DATA = 'Hello World'; - const EXPECTED_HASH = - 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'; - - const hash1 = createSHA256Hash(DATA); - expect(hash1).toBe(EXPECTED_HASH); - - // Hash should be deterministic (same output with same input) - const hash2 = createSHA256Hash(DATA); - expect(hash1).toBe(hash2); - }); -}); diff --git a/app/scripts/controllers/user-storage/encryption/encryption.ts b/app/scripts/controllers/user-storage/encryption/encryption.ts deleted file mode 100644 index cdd8feaf77b3..000000000000 --- a/app/scripts/controllers/user-storage/encryption/encryption.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { scrypt } from '@noble/hashes/scrypt'; -import { sha256 } from '@noble/hashes/sha256'; -import { utf8ToBytes, concatBytes, bytesToHex } from '@noble/hashes/utils'; -import { gcm } from '@noble/ciphers/aes'; -import { randomBytes } from '@noble/ciphers/webcrypto'; -import { getAnyCachedKey, getCachedKeyBySalt, setCachedKey } from './cache'; -import { base64ToByteArray, byteArrayToBase64, bytesToUtf8 } from './utils'; - -export type EncryptedPayload = { - // version - v: '1'; - - // key derivation function algorithm - scrypt - t: 'scrypt'; - - // data - d: string; - - // encryption options - scrypt - o: { - N: number; - r: number; - p: number; - dkLen: number; - }; - - // Salt options - saltLen: number; -}; - -class EncryptorDecryptor { - #ALGORITHM_NONCE_SIZE: number = 12; // 12 bytes - - #ALGORITHM_KEY_SIZE: number = 16; // 16 bytes - - #SCRYPT_SALT_SIZE: number = 16; // 16 bytes - - // see: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt - #SCRYPT_N: number = 2 ** 17; // CPU/memory cost parameter (must be a power of 2, > 1) - - #SCRYPT_r: number = 8; // Block size parameter - - #SCRYPT_p: number = 1; // Parallelization parameter - - encryptString(plaintext: string, password: string): string { - try { - return this.#encryptStringV1(plaintext, password); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : e; - throw new Error(`Unable to encrypt string - ${errorMessage}`); - } - } - - decryptString(encryptedDataStr: string, password: string): string { - try { - const encryptedData: EncryptedPayload = JSON.parse(encryptedDataStr); - if (encryptedData.v === '1') { - if (encryptedData.t === 'scrypt') { - return this.#decryptStringV1(encryptedData, password); - } - } - throw new Error( - `Unsupported encrypted data payload - ${encryptedDataStr}`, - ); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : e; - throw new Error(`Unable to decrypt string - ${errorMessage}`); - } - } - - #encryptStringV1(plaintext: string, password: string): string { - const { key, salt } = this.#getOrGenerateScryptKey(password, { - N: this.#SCRYPT_N, - r: this.#SCRYPT_r, - p: this.#SCRYPT_p, - dkLen: this.#ALGORITHM_KEY_SIZE, - }); - - // Encrypt and prepend salt. - const plaintextRaw = utf8ToBytes(plaintext); - const ciphertextAndNonceAndSalt = concatBytes( - salt, - this.#encrypt(plaintextRaw, key), - ); - - // Convert to Base64 - const encryptedData = byteArrayToBase64(ciphertextAndNonceAndSalt); - - const encryptedPayload: EncryptedPayload = { - v: '1', - t: 'scrypt', - d: encryptedData, - o: { - N: this.#SCRYPT_N, - r: this.#SCRYPT_r, - p: this.#SCRYPT_p, - dkLen: this.#ALGORITHM_KEY_SIZE, - }, - saltLen: this.#SCRYPT_SALT_SIZE, - }; - - return JSON.stringify(encryptedPayload); - } - - #decryptStringV1(data: EncryptedPayload, password: string): string { - const { o, d: base64CiphertextAndNonceAndSalt, saltLen } = data; - - // Decode the base64. - const ciphertextAndNonceAndSalt = base64ToByteArray( - base64CiphertextAndNonceAndSalt, - ); - - // Create buffers of salt and ciphertextAndNonce. - const salt = ciphertextAndNonceAndSalt.slice(0, saltLen); - const ciphertextAndNonce = ciphertextAndNonceAndSalt.slice( - saltLen, - ciphertextAndNonceAndSalt.length, - ); - - // Derive the key. - const { key } = this.#getOrGenerateScryptKey( - password, - { - N: o.N, - r: o.r, - p: o.p, - dkLen: o.dkLen, - }, - salt, - ); - - // Decrypt and return result. - return bytesToUtf8(this.#decrypt(ciphertextAndNonce, key)); - } - - #encrypt(plaintext: Uint8Array, key: Uint8Array): Uint8Array { - const nonce = randomBytes(this.#ALGORITHM_NONCE_SIZE); - - // Encrypt and prepend nonce. - const ciphertext = gcm(key, nonce).encrypt(plaintext); - - return concatBytes(nonce, ciphertext); - } - - #decrypt(ciphertextAndNonce: Uint8Array, key: Uint8Array): Uint8Array { - // Create buffers of nonce and ciphertext. - const nonce = ciphertextAndNonce.slice(0, this.#ALGORITHM_NONCE_SIZE); - const ciphertext = ciphertextAndNonce.slice( - this.#ALGORITHM_NONCE_SIZE, - ciphertextAndNonce.length, - ); - - // Decrypt and return result. - return gcm(key, nonce).decrypt(ciphertext); - } - - #getOrGenerateScryptKey( - password: string, - o: EncryptedPayload['o'], - salt?: Uint8Array, - ) { - const hashedPassword = createSHA256Hash(password); - const cachedKey = salt - ? getCachedKeyBySalt(hashedPassword, salt) - : getAnyCachedKey(hashedPassword); - - if (cachedKey) { - return { - key: cachedKey.key, - salt: cachedKey.salt, - }; - } - - const newSalt = salt ?? randomBytes(this.#SCRYPT_SALT_SIZE); - const newKey = scrypt(password, newSalt, { - N: o.N, - r: o.r, - p: o.p, - dkLen: o.dkLen, - }); - setCachedKey(hashedPassword, newSalt, newKey); - - return { - key: newKey, - salt: newSalt, - }; - } -} - -const encryption = new EncryptorDecryptor(); -export default encryption; - -export function createSHA256Hash(data: string): string { - const hashedData = sha256(data); - return bytesToHex(hashedData); -} diff --git a/app/scripts/controllers/user-storage/encryption/index.ts b/app/scripts/controllers/user-storage/encryption/index.ts deleted file mode 100644 index 3582e3b9e2a1..000000000000 --- a/app/scripts/controllers/user-storage/encryption/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Encryption from './encryption'; - -export * from './encryption'; -export default Encryption; diff --git a/app/scripts/controllers/user-storage/encryption/utils.ts b/app/scripts/controllers/user-storage/encryption/utils.ts deleted file mode 100644 index 76ceda77eb7b..000000000000 --- a/app/scripts/controllers/user-storage/encryption/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function byteArrayToBase64(byteArray: Uint8Array) { - return Buffer.from(byteArray).toString('base64'); -} - -export function base64ToByteArray(base64: string) { - return new Uint8Array(Buffer.from(base64, 'base64')); -} - -export function bytesToUtf8(byteArray: Uint8Array) { - const decoder = new TextDecoder('utf-8'); - return decoder.decode(byteArray); -} diff --git a/app/scripts/controllers/user-storage/mocks/mockResponses.ts b/app/scripts/controllers/user-storage/mocks/mockResponses.ts deleted file mode 100644 index c1b3896f446f..000000000000 --- a/app/scripts/controllers/user-storage/mocks/mockResponses.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createEntryPath } from '../schema'; -import { GetUserStorageResponse, USER_STORAGE_ENDPOINT } from '../services'; -import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage'; - -type MockResponse = { - url: string; - requestMethod: 'GET' | 'POST' | 'PUT'; - response: unknown; -}; - -export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath( - 'notification_settings', - MOCK_STORAGE_KEY, -)}`; - -const MOCK_GET_USER_STORAGE_RESPONSE: GetUserStorageResponse = { - HashedKey: 'HASHED_KEY', - Data: MOCK_ENCRYPTED_STORAGE_DATA, -}; - -export function getMockUserStorageGetResponse() { - return { - url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT, - requestMethod: 'GET', - response: MOCK_GET_USER_STORAGE_RESPONSE, - } satisfies MockResponse; -} - -export function getMockUserStoragePutResponse() { - return { - url: MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT, - requestMethod: 'PUT', - response: null, - } satisfies MockResponse; -} diff --git a/app/scripts/controllers/user-storage/mocks/mockServices.ts b/app/scripts/controllers/user-storage/mocks/mockServices.ts deleted file mode 100644 index d612ebe6ed55..000000000000 --- a/app/scripts/controllers/user-storage/mocks/mockServices.ts +++ /dev/null @@ -1,34 +0,0 @@ -import nock from 'nock'; -import { - getMockUserStorageGetResponse, - getMockUserStoragePutResponse, -} from './mockResponses'; - -type MockReply = { - status: nock.StatusCode; - body?: nock.Body; -}; - -export function mockEndpointGetUserStorage(mockReply?: MockReply) { - const mockResponse = getMockUserStorageGetResponse(); - const reply = mockReply ?? { - status: 200, - body: mockResponse.response, - }; - - const mockEndpoint = nock(mockResponse.url) - .get('') - .reply(reply.status, reply.body); - - return mockEndpoint; -} - -export function mockEndpointUpsertUserStorage( - mockReply?: Pick, -) { - const mockResponse = getMockUserStoragePutResponse(); - const mockEndpoint = nock(mockResponse.url) - .put('') - .reply(mockReply?.status ?? 204); - return mockEndpoint; -} diff --git a/app/scripts/controllers/user-storage/mocks/mockStorage.ts b/app/scripts/controllers/user-storage/mocks/mockStorage.ts deleted file mode 100644 index 4a43a80556e1..000000000000 --- a/app/scripts/controllers/user-storage/mocks/mockStorage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import encryption, { createSHA256Hash } from '../encryption'; - -export const MOCK_STORAGE_KEY_SIGNATURE = 'mockStorageKey'; -export const MOCK_STORAGE_KEY = createSHA256Hash(MOCK_STORAGE_KEY_SIGNATURE); -export const MOCK_STORAGE_DATA = JSON.stringify({ hello: 'world' }); -export const MOCK_ENCRYPTED_STORAGE_DATA = encryption.encryptString( - MOCK_STORAGE_DATA, - MOCK_STORAGE_KEY, -); diff --git a/app/scripts/controllers/user-storage/schema.test.ts b/app/scripts/controllers/user-storage/schema.test.ts deleted file mode 100644 index d08b0802bf49..000000000000 --- a/app/scripts/controllers/user-storage/schema.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { USER_STORAGE_ENTRIES, createEntryPath } from './schema'; - -describe('schema.ts - createEntryPath()', () => { - const MOCK_STORAGE_KEY = 'MOCK_STORAGE_KEY'; - - test('creates a valid entry path', () => { - const result = createEntryPath('notification_settings', MOCK_STORAGE_KEY); - - // Ensures that the path and the entry name are correct. - // If this differs then indicates a potential change on how this path is computed - const expected = `/${USER_STORAGE_ENTRIES.notification_settings.path}/50f65447980018849b991e038d7ad87de5cf07fbad9736b0280e93972e17bac8`; - expect(result).toBe(expected); - }); - - test('Should throw if using an entry that does not exist', () => { - expect(() => { - // @ts-expect-error mocking a fake entry for testing. - createEntryPath('fake_entry'); - }).toThrow(); - }); -}); diff --git a/app/scripts/controllers/user-storage/schema.ts b/app/scripts/controllers/user-storage/schema.ts deleted file mode 100644 index 19bc0ccfae52..000000000000 --- a/app/scripts/controllers/user-storage/schema.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createSHA256Hash } from './encryption'; - -type UserStorageEntry = { path: string; entryName: string }; - -/** - * The User Storage Endpoint requires a path and an entry name. - * Developers can provide additional paths by extending this variable below - */ -export const USER_STORAGE_ENTRIES = { - notification_settings: { - path: 'notifications', - entryName: 'notification_settings', - }, -} satisfies Record; - -export type UserStorageEntryKeys = keyof typeof USER_STORAGE_ENTRIES; - -/** - * Constructs a unique entry path for a user. - * This can be done due to the uniqueness of the storage key (no users will share the same storage key). - * The users entry is a unique hash that cannot be reversed. - * - * @param entryKey - * @param storageKey - * @returns - */ -export function createEntryPath( - entryKey: UserStorageEntryKeys, - storageKey: string, -): string { - const entry = USER_STORAGE_ENTRIES[entryKey]; - if (!entry) { - throw new Error(`user-storage - invalid entry provided: ${entryKey}`); - } - - const hashedKey = createSHA256Hash(entry.entryName + storageKey); - return `/${entry.path}/${hashedKey}`; -} diff --git a/app/scripts/controllers/user-storage/services.test.ts b/app/scripts/controllers/user-storage/services.test.ts deleted file mode 100644 index a746dcee858f..000000000000 --- a/app/scripts/controllers/user-storage/services.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - mockEndpointGetUserStorage, - mockEndpointUpsertUserStorage, -} from './mocks/mockServices'; -import { - MOCK_ENCRYPTED_STORAGE_DATA, - MOCK_STORAGE_DATA, - MOCK_STORAGE_KEY, -} from './mocks/mockStorage'; -import { - GetUserStorageResponse, - getUserStorage, - upsertUserStorage, -} from './services'; - -describe('user-storage/services.ts - getUserStorage() tests', () => { - test('returns user storage data', async () => { - const mockGetUserStorage = mockEndpointGetUserStorage(); - const result = await actCallGetUserStorage(); - - mockGetUserStorage.done(); - expect(result).toBe(MOCK_STORAGE_DATA); - }); - - test('returns null if endpoint does not have entry', async () => { - const mockGetUserStorage = mockEndpointGetUserStorage({ status: 404 }); - const result = await actCallGetUserStorage(); - - mockGetUserStorage.done(); - expect(result).toBe(null); - }); - - test('returns null if endpoint fails', async () => { - const mockGetUserStorage = mockEndpointGetUserStorage({ status: 500 }); - const result = await actCallGetUserStorage(); - - mockGetUserStorage.done(); - expect(result).toBe(null); - }); - - test('returns null if unable to decrypt data', async () => { - const badResponseData: GetUserStorageResponse = { - HashedKey: 'MOCK_HASH', - Data: 'Bad Encrypted Data', - }; - const mockGetUserStorage = mockEndpointGetUserStorage({ - status: 200, - body: badResponseData, - }); - const result = await actCallGetUserStorage(); - - mockGetUserStorage.done(); - expect(result).toBe(null); - }); - - function actCallGetUserStorage() { - return getUserStorage({ - bearerToken: 'MOCK_BEARER_TOKEN', - entryKey: 'notification_settings', - storageKey: MOCK_STORAGE_KEY, - }); - } -}); - -describe('user-storage/services.ts - upsertUserStorage() tests', () => { - test('invokes upsert endpoint with no errors', async () => { - const mockUpsertUserStorage = mockEndpointUpsertUserStorage(); - await actCallUpsertUserStorage(); - - expect(mockUpsertUserStorage.isDone()).toBe(true); - }); - - test('throws error if unable to upsert user storage', async () => { - const mockUpsertUserStorage = mockEndpointUpsertUserStorage({ - status: 500, - }); - - await expect(actCallUpsertUserStorage()).rejects.toThrow(); - mockUpsertUserStorage.done(); - }); - - function actCallUpsertUserStorage() { - return upsertUserStorage(MOCK_ENCRYPTED_STORAGE_DATA, { - bearerToken: 'MOCK_BEARER_TOKEN', - entryKey: 'notification_settings', - storageKey: MOCK_STORAGE_KEY, - }); - } -}); diff --git a/app/scripts/controllers/user-storage/services.ts b/app/scripts/controllers/user-storage/services.ts deleted file mode 100644 index 269009850079..000000000000 --- a/app/scripts/controllers/user-storage/services.ts +++ /dev/null @@ -1,83 +0,0 @@ -import log from 'loglevel'; - -import encryption from './encryption'; -import { UserStorageEntryKeys, createEntryPath } from './schema'; - -export const USER_STORAGE_API = process.env.USER_STORAGE_API || ''; -export const USER_STORAGE_ENDPOINT = `${USER_STORAGE_API}/api/v1/userstorage`; - -export type GetUserStorageResponse = { - HashedKey: string; - Data: string; -}; - -export type UserStorageOptions = { - bearerToken: string; - entryKey: UserStorageEntryKeys; - storageKey: string; -}; - -export async function getUserStorage( - opts: UserStorageOptions, -): Promise { - try { - const path = createEntryPath(opts.entryKey, opts.storageKey); - const url = new URL(`${USER_STORAGE_ENDPOINT}${path}`); - - const userStorageResponse = await fetch(url.toString(), { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${opts.bearerToken}`, - }, - }); - - // Acceptable error - since indicates entry does not exist. - if (userStorageResponse.status === 404) { - return null; - } - - if (userStorageResponse.status !== 200) { - throw new Error('Unable to get User Storage'); - } - - const userStorage: GetUserStorageResponse | null = - await userStorageResponse.json(); - const encryptedData = userStorage?.Data ?? null; - - if (!encryptedData) { - return null; - } - - const decryptedData = encryption.decryptString( - encryptedData, - opts.storageKey, - ); - - return decryptedData; - } catch (e) { - log.error('Failed to get user storage', e); - return null; - } -} - -export async function upsertUserStorage( - data: string, - opts: UserStorageOptions, -): Promise { - const encryptedData = encryption.encryptString(data, opts.storageKey); - const path = createEntryPath(opts.entryKey, opts.storageKey); - const url = new URL(`${USER_STORAGE_ENDPOINT}${path}`); - - const res = await fetch(url.toString(), { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${opts.bearerToken}`, - }, - body: JSON.stringify({ data: encryptedData }), - }); - - if (!res.ok) { - throw new Error('user-storage - unable to upsert data'); - } -} diff --git a/app/scripts/controllers/user-storage/user-storage-controller.test.ts b/app/scripts/controllers/user-storage/user-storage-controller.test.ts deleted file mode 100644 index 8d3f61d6ab21..000000000000 --- a/app/scripts/controllers/user-storage/user-storage-controller.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import nock from 'nock'; -import { ControllerMessenger } from '@metamask/base-controller'; -import { - AuthenticationControllerGetBearerToken, - AuthenticationControllerGetSessionProfile, - AuthenticationControllerIsSignedIn, - AuthenticationControllerPerformSignIn, -} from '../authentication/authentication-controller'; -import { - MetamaskNotificationsControllerDisableMetamaskNotifications, - MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled, -} from '../metamask-notifications/metamask-notifications'; -import { - MOCK_STORAGE_DATA, - MOCK_STORAGE_KEY, - MOCK_STORAGE_KEY_SIGNATURE, -} from './mocks/mockStorage'; -import UserStorageController, { - AllowedActions, - AllowedEvents, -} from './user-storage-controller'; -import { - mockEndpointGetUserStorage, - mockEndpointUpsertUserStorage, -} from './mocks/mockServices'; - -const typedMockFn = unknown>() => - jest.fn, Parameters>(); - -describe('user-storage/user-storage-controller - constructor() tests', () => { - test('Creates UserStorage with default state', () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - expect(controller.state.isProfileSyncingEnabled).toBe(true); - }); - - function arrangeMocks() { - return { - messengerMocks: mockUserStorageMessenger(), - }; - } -}); - -describe('user-storage/user-storage-controller - performGetStorage() tests', () => { - test('returns users notification storage', async () => { - const { messengerMocks, mockAPI } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - const result = await controller.performGetStorage('notification_settings'); - mockAPI.done(); - expect(result).toBe(MOCK_STORAGE_DATA); - }); - - test('rejects if UserStorage is not enabled', async () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - }, - }); - - await expect( - controller.performGetStorage('notification_settings'), - ).rejects.toThrow(); - }); - - test('rejects if wallet is locked', async () => { - const { messengerMocks } = arrangeMocks(); - - // Mock wallet is locked - messengerMocks.mockKeyringControllerGetState.mockReturnValue({ - isUnlocked: false, - }); - - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - await expect( - controller.performGetStorage('notification_settings'), - ).rejects.toThrow(); - }); - - // @ts-expect-error This is missing from the Mocha type definitions - test.each([ - [ - 'fails when no bearer token is found (auth errors)', - (messengerMocks: ReturnType) => - messengerMocks.mockAuthGetBearerToken.mockRejectedValue( - new Error('MOCK FAILURE'), - ), - ], - [ - 'fails when no session identifier is found (auth errors)', - (messengerMocks: ReturnType) => - messengerMocks.mockAuthGetSessionProfile.mockRejectedValue( - new Error('MOCK FAILURE'), - ), - ], - ])( - 'rejects on auth failure - %s', - async ( - _: string, - arrangeFailureCase: ( - messengerMocks: ReturnType, - ) => void, - ) => { - const { messengerMocks } = arrangeMocks(); - arrangeFailureCase(messengerMocks); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - await expect( - controller.performGetStorage('notification_settings'), - ).rejects.toThrow(); - }, - ); - - function arrangeMocks() { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: mockEndpointGetUserStorage(), - }; - } -}); - -describe('user-storage/user-storage-controller - performSetStorage() tests', () => { - test('saves users storage', async () => { - const { messengerMocks, mockAPI } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - await controller.performSetStorage('notification_settings', 'new data'); - mockAPI.done(); - }); - - test('rejects if UserStorage is not enabled', async () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - }, - }); - - await expect( - controller.performSetStorage('notification_settings', 'new data'), - ).rejects.toThrow(); - }); - - test('rejects if wallet is locked', async () => { - const { messengerMocks } = arrangeMocks(); - - // Mock wallet is locked - messengerMocks.mockKeyringControllerGetState.mockReturnValue({ - isUnlocked: false, - }); - - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - await expect( - controller.performSetStorage('notification_settings', 'new data'), - ).rejects.toThrow(); - }); - - // @ts-expect-error This is missing from the Mocha type definitions - test.each([ - [ - 'fails when no bearer token is found (auth errors)', - (messengerMocks: ReturnType) => - messengerMocks.mockAuthGetBearerToken.mockRejectedValue( - new Error('MOCK FAILURE'), - ), - ], - [ - 'fails when no session identifier is found (auth errors)', - (messengerMocks: ReturnType) => - messengerMocks.mockAuthGetSessionProfile.mockRejectedValue( - new Error('MOCK FAILURE'), - ), - ], - ])( - 'rejects on auth failure - %s', - async ( - _: string, - arrangeFailureCase: ( - messengerMocks: ReturnType, - ) => void, - ) => { - const { messengerMocks } = arrangeMocks(); - arrangeFailureCase(messengerMocks); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - await expect( - controller.performSetStorage('notification_settings', 'new data'), - ).rejects.toThrow(); - }, - ); - - test('rejects if api call fails', async () => { - const { messengerMocks } = arrangeMocks({ - mockAPI: mockEndpointUpsertUserStorage({ status: 500 }), - }); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - await expect( - controller.performSetStorage('notification_settings', 'new data'), - ).rejects.toThrow(); - }); - - function arrangeMocks(overrides?: { mockAPI?: nock.Scope }) { - return { - messengerMocks: mockUserStorageMessenger(), - mockAPI: overrides?.mockAPI ?? mockEndpointUpsertUserStorage(), - }; - } -}); - -describe('user-storage/user-storage-controller - performSetStorage() tests', () => { - test('Should return a storage key', async () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - const result = await controller.getStorageKey(); - expect(result).toBe(MOCK_STORAGE_KEY); - }); - - test('rejects if UserStorage is not enabled', async () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - }, - }); - - await expect(controller.getStorageKey()).rejects.toThrow(); - }); - - function arrangeMocks() { - return { - messengerMocks: mockUserStorageMessenger(), - }; - } -}); - -describe('user-storage/user-storage-controller - disableProfileSyncing() tests', () => { - test('should disable user storage / profile syncing when called', async () => { - const { messengerMocks } = arrangeMocks(); - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - }); - - expect(controller.state.isProfileSyncingEnabled).toBe(true); - await controller.disableProfileSyncing(); - expect(controller.state.isProfileSyncingEnabled).toBe(false); - }); - - function arrangeMocks() { - return { - messengerMocks: mockUserStorageMessenger(), - }; - } -}); - -describe('user-storage/user-storage-controller - enableProfileSyncing() tests', () => { - test('should enable user storage / profile syncing', async () => { - const { messengerMocks } = arrangeMocks(); - messengerMocks.mockAuthIsSignedIn.mockReturnValue(false); // mock that auth is not enabled - - const controller = new UserStorageController({ - messenger: messengerMocks.messenger, - getMetaMetricsState: () => true, - state: { - isProfileSyncingEnabled: false, - isProfileSyncingUpdateLoading: false, - }, - }); - - expect(controller.state.isProfileSyncingEnabled).toBe(false); - await controller.enableProfileSyncing(); - expect(controller.state.isProfileSyncingEnabled).toBe(true); - expect(messengerMocks.mockAuthIsSignedIn).toBeCalled(); - expect(messengerMocks.mockAuthPerformSignIn).toBeCalled(); - }); - - function arrangeMocks() { - return { - messengerMocks: mockUserStorageMessenger(), - }; - } -}); - -function mockUserStorageMessenger() { - const messenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >().getRestricted({ - name: 'UserStorageController', - allowedActions: [ - 'KeyringController:getState', - 'SnapController:handleRequest', - 'AuthenticationController:getBearerToken', - 'AuthenticationController:getSessionProfile', - 'AuthenticationController:isSignedIn', - 'AuthenticationController:performSignIn', - 'AuthenticationController:performSignOut', - 'MetamaskNotificationsController:disableMetamaskNotifications', - 'MetamaskNotificationsController:selectIsMetamaskNotificationsEnabled', - ], - allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], - }); - - const mockSnapGetPublicKey = jest.fn().mockResolvedValue('MOCK_PUBLIC_KEY'); - const mockSnapSignMessage = jest - .fn() - .mockResolvedValue(MOCK_STORAGE_KEY_SIGNATURE); - - const mockAuthGetBearerToken = - typedMockFn< - AuthenticationControllerGetBearerToken['handler'] - >().mockResolvedValue('MOCK_BEARER_TOKEN'); - - const mockAuthGetSessionProfile = typedMockFn< - AuthenticationControllerGetSessionProfile['handler'] - >().mockResolvedValue({ - identifierId: '', - profileId: 'MOCK_PROFILE_ID', - }); - - const mockAuthPerformSignIn = - typedMockFn< - AuthenticationControllerPerformSignIn['handler'] - >().mockResolvedValue('New Access Token'); - - const mockAuthIsSignedIn = - typedMockFn< - AuthenticationControllerIsSignedIn['handler'] - >().mockReturnValue(true); - - const mockAuthPerformSignOut = - typedMockFn< - AuthenticationControllerIsSignedIn['handler'] - >().mockReturnValue(true); - - const mockMetamaskNotificationsIsMetamaskNotificationsEnabled = - typedMockFn< - MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled['handler'] - >().mockReturnValue(true); - - const mockMetamaskNotificationsDisableNotifications = - typedMockFn< - MetamaskNotificationsControllerDisableMetamaskNotifications['handler'] - >().mockResolvedValue(); - - const mockKeyringControllerGetState = typedMockFn< - () => { isUnlocked: boolean } - >().mockReturnValue({ isUnlocked: true }); - - jest.spyOn(messenger, 'call').mockImplementation((...args) => { - const [actionType, params] = args; - if (actionType === 'SnapController:handleRequest') { - if (params?.request.method === 'getPublicKey') { - return mockSnapGetPublicKey(); - } - - if (params?.request.method === 'signMessage') { - return mockSnapSignMessage(); - } - - throw new Error( - `MOCK_FAIL - unsupported SnapController:handleRequest call: ${params?.request.method}`, - ); - } - - if (actionType === 'AuthenticationController:getBearerToken') { - return mockAuthGetBearerToken(); - } - - if (actionType === 'AuthenticationController:getSessionProfile') { - return mockAuthGetSessionProfile(); - } - - if (actionType === 'AuthenticationController:performSignIn') { - return mockAuthPerformSignIn(); - } - - if (actionType === 'AuthenticationController:isSignedIn') { - return mockAuthIsSignedIn(); - } - - if ( - actionType === - 'MetamaskNotificationsController:selectIsMetamaskNotificationsEnabled' - ) { - return mockMetamaskNotificationsIsMetamaskNotificationsEnabled(); - } - - if ( - actionType === - 'MetamaskNotificationsController:disableMetamaskNotifications' - ) { - return mockMetamaskNotificationsDisableNotifications(); - } - - if (actionType === 'AuthenticationController:performSignOut') { - return mockAuthPerformSignOut(); - } - - if (actionType === 'KeyringController:getState') { - return mockKeyringControllerGetState(); - } - - function exhaustedMessengerMocks(action: never) { - throw new Error(`MOCK_FAIL - unsupported messenger call: ${action}`); - } - - return exhaustedMessengerMocks(actionType); - }); - - return { - messenger, - mockSnapGetPublicKey, - mockSnapSignMessage, - mockAuthGetBearerToken, - mockAuthGetSessionProfile, - mockAuthPerformSignIn, - mockAuthIsSignedIn, - mockMetamaskNotificationsIsMetamaskNotificationsEnabled, - mockMetamaskNotificationsDisableNotifications, - mockAuthPerformSignOut, - mockKeyringControllerGetState, - }; -} diff --git a/app/scripts/controllers/user-storage/user-storage-controller.ts b/app/scripts/controllers/user-storage/user-storage-controller.ts deleted file mode 100644 index 9a286f801330..000000000000 --- a/app/scripts/controllers/user-storage/user-storage-controller.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { - BaseController, - RestrictedControllerMessenger, - StateMetadata, -} from '@metamask/base-controller'; -import type { - KeyringControllerGetStateAction, - KeyringControllerLockEvent, - KeyringControllerUnlockEvent, -} from '@metamask/keyring-controller'; -import { HandleSnapRequest } from '@metamask/snaps-controllers'; -import { - AuthenticationControllerGetBearerToken, - AuthenticationControllerGetSessionProfile, - AuthenticationControllerIsSignedIn, - AuthenticationControllerPerformSignIn, - AuthenticationControllerPerformSignOut, -} from '../authentication/authentication-controller'; -import { - MetamaskNotificationsControllerDisableMetamaskNotifications, - MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled, -} from '../metamask-notifications/metamask-notifications'; -import { createSnapSignMessageRequest } from '../authentication/auth-snap-requests'; -import { getUserStorage, upsertUserStorage } from './services'; -import { UserStorageEntryKeys } from './schema'; -import { createSHA256Hash } from './encryption'; - -const controllerName = 'UserStorageController'; - -// State -export type UserStorageControllerState = { - /** - * Condition used by UI and to determine if we can use some of the User Storage methods. - */ - isProfileSyncingEnabled: boolean | null; - /** - * Loading state for the profile syncing update - */ - isProfileSyncingUpdateLoading: boolean; -}; - -const defaultState: UserStorageControllerState = { - isProfileSyncingEnabled: true, - isProfileSyncingUpdateLoading: false, -}; - -const metadata: StateMetadata = { - isProfileSyncingEnabled: { - persist: true, - anonymous: true, - }, - isProfileSyncingUpdateLoading: { - persist: false, - anonymous: false, - }, -}; - -// Messenger Actions -type CreateActionsObj = { - [K in T]: { - type: `${typeof controllerName}:${K}`; - handler: UserStorageController[K]; - }; -}; -type ActionsObj = CreateActionsObj< - | 'performGetStorage' - | 'performSetStorage' - | 'getStorageKey' - | 'enableProfileSyncing' - | 'disableProfileSyncing' ->; -export type Actions = ActionsObj[keyof ActionsObj]; -export type UserStorageControllerPerformGetStorage = - ActionsObj['performGetStorage']; -export type UserStorageControllerPerformSetStorage = - ActionsObj['performSetStorage']; -export type UserStorageControllerGetStorageKey = ActionsObj['getStorageKey']; -export type UserStorageControllerEnableProfileSyncing = - ActionsObj['enableProfileSyncing']; -export type UserStorageControllerDisableProfileSyncing = - ActionsObj['disableProfileSyncing']; - -export type AllowedActions = - // Keyring Requests - | KeyringControllerGetStateAction - // Snap Requests - | HandleSnapRequest - // Auth Requests - | AuthenticationControllerGetBearerToken - | AuthenticationControllerGetSessionProfile - | AuthenticationControllerPerformSignIn - | AuthenticationControllerIsSignedIn - | AuthenticationControllerPerformSignOut - // Metamask Notifications - | MetamaskNotificationsControllerDisableMetamaskNotifications - | MetamaskNotificationsControllerSelectIsMetamaskNotificationsEnabled; - -export type AllowedEvents = - | KeyringControllerLockEvent - | KeyringControllerUnlockEvent; - -// Messenger -export type UserStorageControllerMessenger = RestrictedControllerMessenger< - typeof controllerName, - Actions | AllowedActions, - AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] ->; - -/** - * Reusable controller that allows any team to store synchronized data for a given user. - * These can be settings shared cross MetaMask clients, or data we want to persist when uninstalling/reinstalling. - * - * NOTE: - * - data stored on UserStorage is FULLY encrypted, with the only keys stored/managed on the client. - * - No one can access this data unless they are have the SRP and are able to run the signing snap. - */ -export default class UserStorageController extends BaseController< - typeof controllerName, - UserStorageControllerState, - UserStorageControllerMessenger -> { - #auth = { - getBearerToken: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:getBearerToken', - ); - }, - getProfileId: async () => { - const sessionProfile = await this.messagingSystem.call( - 'AuthenticationController:getSessionProfile', - ); - return sessionProfile?.profileId; - }, - isAuthEnabled: () => { - return this.messagingSystem.call('AuthenticationController:isSignedIn'); - }, - signIn: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:performSignIn', - ); - }, - signOut: async () => { - return await this.messagingSystem.call( - 'AuthenticationController:performSignOut', - ); - }, - }; - - #metamaskNotifications = { - disableMetamaskNotifications: async () => { - return await this.messagingSystem.call( - 'MetamaskNotificationsController:disableMetamaskNotifications', - ); - }, - selectIsMetamaskNotificationsEnabled: async () => { - return await this.messagingSystem.call( - 'MetamaskNotificationsController:selectIsMetamaskNotificationsEnabled', - ); - }, - }; - - #isUnlocked = false; - - #keyringController = { - setupLockedStateSubscriptions: () => { - const { isUnlocked } = this.messagingSystem.call( - 'KeyringController:getState', - ); - this.#isUnlocked = isUnlocked; - - this.messagingSystem.subscribe('KeyringController:unlock', () => { - this.#isUnlocked = true; - }); - - this.messagingSystem.subscribe('KeyringController:lock', () => { - this.#isUnlocked = false; - }); - }, - }; - - getMetaMetricsState: () => boolean; - - constructor(params: { - messenger: UserStorageControllerMessenger; - state?: UserStorageControllerState; - getMetaMetricsState: () => boolean; - }) { - super({ - messenger: params.messenger, - metadata, - name: controllerName, - state: { ...defaultState, ...params.state }, - }); - - this.getMetaMetricsState = params.getMetaMetricsState; - this.#keyringController.setupLockedStateSubscriptions(); - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering this controller's messaging system - * actions. - */ - #registerMessageHandlers(): void { - this.messagingSystem.registerActionHandler( - 'UserStorageController:performGetStorage', - this.performGetStorage.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'UserStorageController:performSetStorage', - this.performSetStorage.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'UserStorageController:getStorageKey', - this.getStorageKey.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'UserStorageController:enableProfileSyncing', - this.enableProfileSyncing.bind(this), - ); - - this.messagingSystem.registerActionHandler( - 'UserStorageController:disableProfileSyncing', - this.disableProfileSyncing.bind(this), - ); - } - - public async enableProfileSyncing(): Promise { - try { - this.#setIsProfileSyncingUpdateLoading(true); - - const authEnabled = this.#auth.isAuthEnabled(); - if (!authEnabled) { - await this.#auth.signIn(); - } - - this.update((state) => { - state.isProfileSyncingEnabled = true; - }); - - this.#setIsProfileSyncingUpdateLoading(false); - } catch (e) { - this.#setIsProfileSyncingUpdateLoading(false); - const errorMessage = e instanceof Error ? e.message : e; - throw new Error( - `${controllerName} - failed to enable profile syncing - ${errorMessage}`, - ); - } - } - - public async setIsProfileSyncingEnabled( - isProfileSyncingEnabled: boolean, - ): Promise { - this.update((state) => { - state.isProfileSyncingEnabled = isProfileSyncingEnabled; - }); - } - - public async disableProfileSyncing(): Promise { - const isAlreadyDisabled = !this.state.isProfileSyncingEnabled; - if (isAlreadyDisabled) { - return; - } - - try { - this.#setIsProfileSyncingUpdateLoading(true); - - const isMetamaskNotificationsEnabled = - await this.#metamaskNotifications.selectIsMetamaskNotificationsEnabled(); - - if (isMetamaskNotificationsEnabled) { - await this.#metamaskNotifications.disableMetamaskNotifications(); - } - - const isMetaMetricsParticipation = this.getMetaMetricsState(); - - if (!isMetaMetricsParticipation) { - await this.messagingSystem.call( - 'AuthenticationController:performSignOut', - ); - } - - this.#setIsProfileSyncingUpdateLoading(false); - - this.update((state) => { - state.isProfileSyncingEnabled = false; - }); - } catch (e) { - this.#setIsProfileSyncingUpdateLoading(false); - const errorMessage = e instanceof Error ? e.message : e; - throw new Error( - `${controllerName} - failed to disable profile syncing - ${errorMessage}`, - ); - } - } - - /** - * Allows retrieval of stored data. Data stored is string formatted. - * Developers can extend the entry path and entry name through the `schema.ts` file. - * - * @param entryKey - * @returns the decrypted string contents found from user storage (or null if not found) - */ - public async performGetStorage( - entryKey: UserStorageEntryKeys, - ): Promise { - this.#assertProfileSyncingEnabled(); - const { bearerToken, storageKey } = - await this.#getStorageKeyAndBearerToken(); - const result = await getUserStorage({ - entryKey, - bearerToken, - storageKey, - }); - - return result; - } - - /** - * Allows storage of user data. Data stored must be string formatted. - * Developers can extend the entry path and entry name through the `schema.ts` file. - * - * @param entryKey - * @param value - The string data you want to store. - * @returns nothing. NOTE that an error is thrown if fails to store data. - */ - public async performSetStorage( - entryKey: UserStorageEntryKeys, - value: string, - ): Promise { - this.#assertProfileSyncingEnabled(); - const { bearerToken, storageKey } = - await this.#getStorageKeyAndBearerToken(); - - await upsertUserStorage(value, { - entryKey, - bearerToken, - storageKey, - }); - } - - /** - * Retrieves the storage key, for internal use only! - * - * @returns the storage key - */ - public async getStorageKey(): Promise { - this.#assertProfileSyncingEnabled(); - const storageKey = await this.#createStorageKey(); - return storageKey; - } - - #assertProfileSyncingEnabled(): void { - if (!this.state.isProfileSyncingEnabled) { - throw new Error( - `${controllerName}: Unable to call method, user is not authenticated`, - ); - } - } - - /** - * Utility to get the bearer token and storage key - */ - async #getStorageKeyAndBearerToken(): Promise<{ - bearerToken: string; - storageKey: string; - }> { - const bearerToken = await this.#auth.getBearerToken(); - if (!bearerToken) { - throw new Error('UserStorageController - unable to get bearer token'); - } - const storageKey = await this.#createStorageKey(); - - return { bearerToken, storageKey }; - } - - /** - * Rather than storing the storage key, we can compute the storage key when needed. - * - * @returns the storage key - */ - async #createStorageKey(): Promise { - const id = await this.#auth.getProfileId(); - if (!id) { - throw new Error('UserStorageController - unable to create storage key'); - } - - const storageKeySignature = await this.#snapSignMessage(`metamask:${id}`); - const storageKey = createSHA256Hash(storageKeySignature); - return storageKey; - } - - #_snapSignMessageCache: Record<`metamask:${string}`, string> = {}; - - /** - * Signs a specific message using an underlying auth snap. - * - * @param message - A specific tagged message to sign. - * @returns A Signature created by the snap. - */ - async #snapSignMessage(message: `metamask:${string}`): Promise { - if (this.#_snapSignMessageCache[message]) { - return this.#_snapSignMessageCache[message]; - } - - if (!this.#isUnlocked) { - throw new Error( - '#snapSignMessage - unable to call snap, wallet is locked', - ); - } - - const result = (await this.messagingSystem.call( - 'SnapController:handleRequest', - createSnapSignMessageRequest(message), - )) as string; - - this.#_snapSignMessageCache[message] = result; - - return result; - } - - async #setIsProfileSyncingUpdateLoading( - isProfileSyncingUpdateLoading: boolean, - ): Promise { - this.update((state) => { - state.isProfileSyncingUpdateLoading = isProfileSyncingUpdateLoading; - }); - } -} diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index 02627c6aa201..e8ddd89f021e 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -14,12 +14,11 @@ import { defaultState, BalancesControllerMessenger, } from './BalancesController'; -import { Poller } from './Poller'; +import { BalancesTracker } from './BalancesTracker'; const mockBtcAccount = createMockInternalAccount({ address: '', name: 'Btc Account', - // @ts-expect-error - account type may be btc or eth, mock file is not typed type: BtcAccountType.P2wpkh, snapOptions: { id: 'mock-btc-snap', @@ -53,8 +52,14 @@ const setupController = ({ const balancesControllerMessenger: BalancesControllerMessenger = controllerMessenger.getRestricted({ name: 'BalancesController', - allowedActions: ['SnapController:handleRequest'], - allowedEvents: ['AccountsController:stateChange'], + allowedActions: [ + 'SnapController:handleRequest', + 'AccountsController:listMultichainAccounts', + ], + allowedEvents: [ + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], }); const mockSnapHandleRequest = jest.fn(); @@ -65,20 +70,22 @@ const setupController = ({ ), ); - // TODO: remove when listMultichainAccounts action is available - const mockListMultichainAccounts = jest - .fn() - .mockReturnValue(mocks?.listMultichainAccounts ?? [mockBtcAccount]); + const mockListMultichainAccounts = jest.fn(); + controllerMessenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + mockListMultichainAccounts.mockReturnValue( + mocks?.listMultichainAccounts ?? [mockBtcAccount], + ), + ); const controller = new BalancesController({ messenger: balancesControllerMessenger, state, - // TODO: remove when listMultichainAccounts action is available - listMultichainAccounts: mockListMultichainAccounts, }); return { controller, + messenger: controllerMessenger, mockSnapHandleRequest, mockListMultichainAccounts, }; @@ -90,19 +97,19 @@ describe('BalancesController', () => { expect(controller.state).toEqual({ balances: {} }); }); - it('starts polling when calling start', async () => { - const spyPoller = jest.spyOn(Poller.prototype, 'start'); + it('starts tracking when calling start', async () => { + const spyTracker = jest.spyOn(BalancesTracker.prototype, 'start'); const { controller } = setupController(); await controller.start(); - expect(spyPoller).toHaveBeenCalledTimes(1); + expect(spyTracker).toHaveBeenCalledTimes(1); }); - it('stops polling when calling stop', async () => { - const spyPoller = jest.spyOn(Poller.prototype, 'stop'); + it('stops tracking when calling stop', async () => { + const spyTracker = jest.spyOn(BalancesTracker.prototype, 'stop'); const { controller } = setupController(); await controller.start(); await controller.stop(); - expect(spyPoller).toHaveBeenCalledTimes(1); + expect(spyTracker).toHaveBeenCalledTimes(1); }); it('update balances when calling updateBalances', async () => { @@ -112,13 +119,49 @@ describe('BalancesController', () => { expect(controller.state).toEqual({ balances: { - [mockBtcAccount.id]: { - 'bip122:000000000933ea01ad0ee984209779ba/slip44:0': { - amount: '0.00000000', - unit: 'BTC', - }, + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + }); + + it('update balances when "AccountsController:accountAdded" is fired', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController({ + mocks: { + listMultichainAccounts: [], }, + }); + + controller.start(); + mockListMultichainAccounts.mockReturnValue([mockBtcAccount]); + messenger.publish('AccountsController:accountAdded', mockBtcAccount); + await controller.updateBalances(); + + expect(controller.state).toEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, }, }); }); + + it('update balances when "AccountsController:accountRemoved" is fired', async () => { + const { controller, messenger, mockListMultichainAccounts } = + setupController(); + + controller.start(); + await controller.updateBalances(); + expect(controller.state).toEqual({ + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }); + + messenger.publish('AccountsController:accountRemoved', mockBtcAccount.id); + mockListMultichainAccounts.mockReturnValue([]); + await controller.updateBalances(); + + expect(controller.state).toEqual({ + balances: {}, + }); + }); }); diff --git a/app/scripts/lib/accounts/BalancesController.ts b/app/scripts/lib/accounts/BalancesController.ts index ab1eb8c6cfe6..9f9ead59ed90 100644 --- a/app/scripts/lib/accounts/BalancesController.ts +++ b/app/scripts/lib/accounts/BalancesController.ts @@ -19,11 +19,12 @@ import type { SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import type { Draft } from 'immer'; import type { - AccountsControllerChangeEvent, - AccountsControllerState, + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRemovedEvent, + AccountsControllerListMultichainAccountsAction, } from '@metamask/accounts-controller'; import { isBtcMainnetAddress } from '../../../../shared/lib/multichain'; -import { Poller } from './Poller'; +import { BalancesTracker } from './BalancesTracker'; const controllerName = 'BalancesController'; @@ -85,12 +86,16 @@ export type BalancesControllerEvents = BalancesControllerStateChange; /** * Actions that this controller is allowed to call. */ -export type AllowedActions = HandleSnapRequest; +export type AllowedActions = + | HandleSnapRequest + | AccountsControllerListMultichainAccountsAction; /** * Events that this controller is allowed to subscribe. */ -export type AllowedEvents = AccountsControllerChangeEvent; +export type AllowedEvents = + | AccountsControllerAccountAddedEvent + | AccountsControllerAccountRemovedEvent; /** * Messenger type for the BalancesController. @@ -119,7 +124,11 @@ const balancesControllerMetadata = { const BTC_TESTNET_ASSETS = ['bip122:000000000933ea01ad0ee984209779ba/slip44:0']; const BTC_MAINNET_ASSETS = ['bip122:000000000019d6689c085ae165831e93/slip44:0']; -export const BTC_AVG_BLOCK_TIME = 600000; // 10 minutes in milliseconds +const BTC_AVG_BLOCK_TIME = 10 * 60 * 1000; // 10 minutes in milliseconds + +// NOTE: We set an interval of half the average block time to mitigate when our interval +// is de-synchronized with the actual block time. +export const BALANCES_UPDATE_TIME = BTC_AVG_BLOCK_TIME / 2; /** * The BalancesController is responsible for fetching and caching account @@ -130,19 +139,14 @@ export class BalancesController extends BaseController< BalancesControllerState, BalancesControllerMessenger > { - #poller: Poller; - - // TODO: remove once action is implemented - #listMultichainAccounts: () => InternalAccount[]; + #tracker: BalancesTracker; constructor({ messenger, state, - listMultichainAccounts, }: { messenger: BalancesControllerMessenger; state: BalancesControllerState; - listMultichainAccounts: () => InternalAccount[]; }) { super({ messenger, @@ -154,27 +158,50 @@ export class BalancesController extends BaseController< }, }); - this.messagingSystem.subscribe( - 'AccountsController:stateChange', - (newState) => this.#handleOnAccountsControllerChange(newState), + this.#tracker = new BalancesTracker( + async (accountId: string) => await this.#updateBalance(accountId), ); - this.#listMultichainAccounts = listMultichainAccounts; - this.#poller = new Poller(() => this.updateBalances(), BTC_AVG_BLOCK_TIME); + // Register all non-EVM accounts into the tracker + for (const account of this.#listAccounts()) { + if (this.#isNonEvmAccount(account)) { + this.#tracker.track(account.id, BALANCES_UPDATE_TIME); + } + } + + this.messagingSystem.subscribe( + 'AccountsController:accountAdded', + (account) => this.#handleOnAccountAdded(account), + ); + this.messagingSystem.subscribe( + 'AccountsController:accountRemoved', + (account) => this.#handleOnAccountRemoved(account), + ); } /** * Starts the polling process. */ async start(): Promise { - this.#poller.start(); + this.#tracker.start(); } /** * Stops the polling process. */ async stop(): Promise { - this.#poller.stop(); + this.#tracker.stop(); + } + + /** + * Lists the multichain accounts coming from the `AccountsController`. + * + * @returns A list of multichain accounts. + */ + #listMultichainAccounts(): InternalAccount[] { + return this.messagingSystem.call( + 'AccountsController:listMultichainAccounts', + ); } /** @@ -185,50 +212,122 @@ export class BalancesController extends BaseController< * * @returns A list of accounts that we should get balances for. */ - async #listAccounts(): Promise { + #listAccounts(): InternalAccount[] { const accounts = this.#listMultichainAccounts(); return accounts.filter((account) => account.type === BtcAccountType.P2wpkh); } /** - * Updates the balances of all supported accounts. This method doesn't return + * Get a non-EVM account from its ID. + * + * @param accountId - The account ID. + */ + #getAccount(accountId: string): InternalAccount { + const account: InternalAccount | undefined = + this.#listMultichainAccounts().find( + (multichainAccount) => multichainAccount.id === accountId, + ); + + if (!account) { + throw new Error(`Unknown account: ${accountId}`); + } + if (!this.#isNonEvmAccount(account)) { + throw new Error(`Account is not a non-EVM account: ${accountId}`); + } + return account; + } + + /** + * Updates the balances of one account. This method doesn't return * anything, but it updates the state of the controller. + * + * @param accountId - The account ID. */ - async updateBalances() { - const accounts = await this.#listAccounts(); + async #updateBalance(accountId: string) { + const account = this.#getAccount(accountId); const partialState: BalancesControllerState = { balances: {} }; - for (const account of accounts) { - if (account.metadata.snap) { - partialState.balances[account.id] = await this.#getBalances( - account.id, - account.metadata.snap.id, - isBtcMainnetAddress(account.address) - ? BTC_MAINNET_ASSETS - : BTC_TESTNET_ASSETS, - ); - } + if (account.metadata.snap) { + partialState.balances[account.id] = await this.#getBalances( + account.id, + account.metadata.snap.id, + isBtcMainnetAddress(account.address) + ? BTC_MAINNET_ASSETS + : BTC_TESTNET_ASSETS, + ); } this.update((state: Draft) => ({ ...state, - ...partialState, + balances: { + ...state.balances, + ...partialState.balances, + }, })); } /** - * Handles changes in the accounts state, specifically when new non-EVM accounts are added. + * Updates the balances of one account. This method doesn't return + * anything, but it updates the state of the controller. + * + * @param accountId - The account ID. + */ + async updateBalance(accountId: string) { + await this.#tracker.updateBalance(accountId); + } + + /** + * Updates the balances of all supported accounts. This method doesn't return + * anything, but it updates the state of the controller. + */ + async updateBalances() { + await this.#tracker.updateBalances(); + } + + /** + * Checks for non-EVM accounts. + * + * @param account - The new account to be checked. + * @returns True if the account is a non-EVM account, false otherwise. + */ + #isNonEvmAccount(account: InternalAccount): boolean { + return ( + !isEvmAccountType(account.type) && + // Non-EVM accounts are backed by a Snap for now + account.metadata.snap !== undefined + ); + } + + /** + * Handles changes when a new account has been added. + * + * @param account - The new account being added. + */ + async #handleOnAccountAdded(account: InternalAccount) { + if (!this.#isNonEvmAccount(account)) { + // Nothing to do here for EVM accounts + return; + } + + this.#tracker.track(account.id, BTC_AVG_BLOCK_TIME); + } + + /** + * Handles changes when a new account has been removed. * - * @param newState - The new state of the accounts controller. + * @param accountId - The account ID being removed. */ - #handleOnAccountsControllerChange(newState: AccountsControllerState) { - // If we have any new non-EVM accounts, we just update non-EVM balances - const newNonEvmAccounts = Object.values( - newState.internalAccounts.accounts, - ).filter((account) => !isEvmAccountType(account.type)); - if (newNonEvmAccounts.length) { - this.updateBalances(); + async #handleOnAccountRemoved(accountId: string) { + if (this.#tracker.isTracked(accountId)) { + this.#tracker.untrack(accountId); + } + + if (accountId in this.state.balances) { + this.update((state: Draft) => { + delete state.balances[accountId]; + return state; + }); } } diff --git a/app/scripts/lib/accounts/BalancesTracker.test.ts b/app/scripts/lib/accounts/BalancesTracker.test.ts new file mode 100644 index 000000000000..3f386a27a72d --- /dev/null +++ b/app/scripts/lib/accounts/BalancesTracker.test.ts @@ -0,0 +1,119 @@ +import { BtcAccountType } from '@metamask/keyring-api'; +import { createMockInternalAccount } from '../../../../test/jest/mocks'; +import { Poller } from './Poller'; +import { BalancesTracker } from './BalancesTracker'; + +const MOCK_TIMESTAMP = 1709983353; + +const mockBtcAccount = createMockInternalAccount({ + address: '', + name: 'Btc Account', + type: BtcAccountType.P2wpkh, + snapOptions: { + id: 'mock-btc-snap', + name: 'mock-btc-snap', + enabled: true, + }, +}); + +function setupTracker() { + const mockUpdateBalance = jest.fn(); + const tracker = new BalancesTracker(mockUpdateBalance); + + return { + tracker, + mockUpdateBalance, + }; +} + +describe('BalancesTracker', () => { + it('starts polling when calling start', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'start'); + + await tracker.start(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('stops polling when calling stop', async () => { + const { tracker } = setupTracker(); + const spyPoller = jest.spyOn(Poller.prototype, 'stop'); + + await tracker.start(); + await tracker.stop(); + expect(spyPoller).toHaveBeenCalledTimes(1); + }); + + it('is not tracking if none accounts have been registered', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + await tracker.start(); + await tracker.updateBalances(); + + expect(mockUpdateBalance).not.toHaveBeenCalled(); + }); + + it('tracks account balances', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + await tracker.start(); + // We must track account IDs explicitly + tracker.track(mockBtcAccount.id, 0); + // Trigger balances refresh (not waiting for the Poller here) + await tracker.updateBalances(); + + expect(mockUpdateBalance).toHaveBeenCalledWith(mockBtcAccount.id); + }); + + it('untracks account balances', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + await tracker.start(); + tracker.track(mockBtcAccount.id, 0); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledWith(mockBtcAccount.id); + + tracker.untrack(mockBtcAccount.id); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); // No second call after untracking + }); + + it('tracks account after being registered', async () => { + const { tracker } = setupTracker(); + + await tracker.start(); + tracker.track(mockBtcAccount.id, 0); + expect(tracker.isTracked(mockBtcAccount.id)).toBe(true); + }); + + it('does not track account if not registered', async () => { + const { tracker } = setupTracker(); + + await tracker.start(); + expect(tracker.isTracked(mockBtcAccount.id)).toBe(false); + }); + + it('does not refresh balance if they are considered up-to-date', async () => { + const { tracker, mockUpdateBalance } = setupTracker(); + + const blockTime = 10 * 60 * 1000; // 10 minutes in milliseconds. + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP).getTime()); + + await tracker.start(); + tracker.track(mockBtcAccount.id, blockTime); + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); + + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(1); // No second call since the balances is already still up-to-date + + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date(MOCK_TIMESTAMP + blockTime).getTime()); + + await tracker.updateBalances(); + expect(mockUpdateBalance).toHaveBeenCalledTimes(2); // Now the balance will update + }); +}); diff --git a/app/scripts/lib/accounts/BalancesTracker.ts b/app/scripts/lib/accounts/BalancesTracker.ts new file mode 100644 index 000000000000..48ecd6f84cca --- /dev/null +++ b/app/scripts/lib/accounts/BalancesTracker.ts @@ -0,0 +1,122 @@ +import { Poller } from './Poller'; + +type BalanceInfo = { + lastUpdated: number; + blockTime: number; +}; + +const BALANCES_TRACKING_INTERVAL = 30 * 1000; // Every 30s in milliseconds. + +export class BalancesTracker { + #poller: Poller; + + #updateBalance: (accountId: string) => Promise; + + #balances: Record = {}; + + constructor(updateBalanceCallback: (accountId: string) => Promise) { + this.#updateBalance = updateBalanceCallback; + + this.#poller = new Poller(() => { + this.updateBalances(); + }, BALANCES_TRACKING_INTERVAL); + } + + /** + * Starts the tracking process. + */ + async start(): Promise { + this.#poller.start(); + } + + /** + * Stops the tracking process. + */ + async stop(): Promise { + this.#poller.stop(); + } + + /** + * Checks if an account ID is being tracked. + * + * @param accountId - The account ID. + * @returns True if the account is being tracker, false otherwise. + */ + isTracked(accountId: string) { + return accountId in this.#balances; + } + + /** + * Asserts that an account ID is being tracked. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + assertBeingTracked(accountId: string) { + if (!this.isTracked(accountId)) { + throw new Error(`Account is not being tracked: ${accountId}`); + } + } + + /** + * Starts tracking a new account ID. This method has no effect on already tracked + * accounts. + * + * @param accountId - The account ID. + * @param blockTime - The block time (used when refreshing the account balances). + */ + track(accountId: string, blockTime: number) { + // Do not overwrite current info if already being tracked! + if (!this.isTracked(accountId)) { + this.#balances[accountId] = { + lastUpdated: 0, + blockTime, + }; + } + } + + /** + * Stops tracking a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + untrack(accountId: string) { + this.assertBeingTracked(accountId); + delete this.#balances[accountId]; + } + + /** + * Update the balances for a tracked account ID. + * + * @param accountId - The account ID. + * @throws If the account ID is not being tracked. + */ + async updateBalance(accountId: string) { + this.assertBeingTracked(accountId); + + // We check if the balance is outdated (by comparing to the block time associated + // with this kind of account). + // + // This might not be super accurate, but we could probably compute this differently + // and try to sync with the "real block time"! + const info = this.#balances[accountId]; + const isOutdated = Date.now() - info.lastUpdated >= info.blockTime; + if (isOutdated) { + await this.#updateBalance(accountId); + this.#balances[accountId].lastUpdated = Date.now(); + } + } + + /** + * Update the balances of all tracked accounts (only if the balances + * is considered outdated). + */ + async updateBalances() { + await Promise.allSettled( + Object.keys(this.#balances).map(async (accountId) => { + await this.updateBalance(accountId); + }), + ); + } +} diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 9f994ec52d55..947761449a36 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -3,6 +3,7 @@ */ import { EthAccountType } from '@metamask/keyring-api'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; +import { mockNetworkState } from '../../../test/stub/networks'; import Backup from './backup'; function getMockPreferencesController() { @@ -105,15 +106,17 @@ const jsonData = JSON.stringify({ }, }, network: { - networkConfigurations: { - 'network-configuration-id-1': { + ...mockNetworkState( + { + id: 'network-configuration-id-1', chainId: '0x539', nickname: 'Localhost 8545', rpcPrefs: {}, rpcUrl: 'http://localhost:8545', ticker: 'ETH', }, - 'network-configuration-id-2': { + { + id: 'network-configuration-id-2', chainId: '0x38', nickname: 'Binance Smart Chain Mainnet', rpcPrefs: { @@ -122,7 +125,8 @@ const jsonData = JSON.stringify({ rpcUrl: 'https://bsc-dataseed1.binance.org', ticker: 'BNB', }, - 'network-configuration-id-3': { + { + id: 'network-configuration-id-3', chainId: '0x61', nickname: 'Binance Smart Chain Testnet', rpcPrefs: { @@ -131,7 +135,8 @@ const jsonData = JSON.stringify({ rpcUrl: 'https://data-seed-prebsc-1-s1.binance.org:8545', ticker: 'tBNB', }, - 'network-configuration-id-4': { + { + id: 'network-configuration-id-4', chainId: '0x89', nickname: 'Polygon Mainnet', rpcPrefs: { @@ -140,7 +145,7 @@ const jsonData = JSON.stringify({ rpcUrl: 'https://polygon-rpc.com', ticker: 'MATIC', }, - }, + ), }, preferences: { useBlockie: false, @@ -164,7 +169,6 @@ const jsonData = JSON.stringify({ showTestNetworks: true, smartTransactionsOptInStatus: false, useNativeCurrencyAsPrimaryCurrency: true, - showTokenAutodetectModal: false, }, ipfsGateway: 'dweb.link', ledgerTransportType: 'webhid', @@ -299,7 +303,6 @@ describe('Backup', function () { }, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', diff --git a/app/scripts/lib/createDupeReqFilterStream.test.ts b/app/scripts/lib/createDupeReqFilterStream.test.ts index 4a3ff375b7e6..b42ba08f5f50 100644 --- a/app/scripts/lib/createDupeReqFilterStream.test.ts +++ b/app/scripts/lib/createDupeReqFilterStream.test.ts @@ -3,7 +3,7 @@ import OurReadableStream from 'readable-stream'; import ReadableStream2 from 'readable-stream-2'; import ReadableStream3 from 'readable-stream-3'; -import type { JsonRpcRequest } from '@metamask/utils'; +import type { JsonRpcNotification, JsonRpcRequest } from '@metamask/utils'; import createDupeReqFilterStream, { THREE_MINUTES, } from './createDupeReqFilterStream'; @@ -26,7 +26,7 @@ function createTestStream(output: JsonRpcRequest[] = [], S = Transform) { } function runStreamTest( - requests: JsonRpcRequest[] = [], + requests: (JsonRpcRequest | JsonRpcNotification)[] = [], advanceTimersTime = 10, S = Transform, ) { @@ -54,12 +54,12 @@ describe('createDupeReqFilterStream', () => { const requests = [ { id: 1, method: 'foo' }, { id: 2, method: 'bar' }, - ]; + ].map((request) => ({ ...request, jsonrpc: '2.0' as const })); const expectedOutput = [ { id: 1, method: 'foo' }, { id: 2, method: 'bar' }, - ]; + ].map((output) => ({ ...output, jsonrpc: '2.0' })); const output = await runStreamTest(requests); expect(output).toEqual(expectedOutput); @@ -69,18 +69,25 @@ describe('createDupeReqFilterStream', () => { const requests = [ { id: 1, method: 'foo' }, { id: 1, method: 'foo' }, // duplicate - ]; + ].map((request) => ({ ...request, jsonrpc: '2.0' as const })); - const expectedOutput = [{ id: 1, method: 'foo' }]; + const expectedOutput = [{ id: 1, method: 'foo' }].map((output) => ({ + ...output, + jsonrpc: '2.0', + })); const output = await runStreamTest(requests); expect(output).toEqual(expectedOutput); }); it("lets through requests if they don't have an id", async () => { - const requests = [{ method: 'notify1' }, { method: 'notify2' }]; + const requests = [{ method: 'notify1' }, { method: 'notify2' }].map( + (request) => ({ ...request, jsonrpc: '2.0' as const }), + ); - const expectedOutput = [{ method: 'notify1' }, { method: 'notify2' }]; + const expectedOutput = [{ method: 'notify1' }, { method: 'notify2' }].map( + (output) => ({ ...output, jsonrpc: '2.0' }), + ); const output = await runStreamTest(requests); expect(output).toEqual(expectedOutput); @@ -95,7 +102,7 @@ describe('createDupeReqFilterStream', () => { { method: 'notify2' }, { id: 2, method: 'bar' }, { id: 3, method: 'baz' }, - ]; + ].map((request) => ({ ...request, jsonrpc: '2.0' as const })); const expectedOutput = [ { id: 1, method: 'foo' }, @@ -103,7 +110,7 @@ describe('createDupeReqFilterStream', () => { { id: 2, method: 'bar' }, { method: 'notify2' }, { id: 3, method: 'baz' }, - ]; + ].map((output) => ({ ...output, jsonrpc: '2.0' })); const output = await runStreamTest(requests); expect(output).toEqual(expectedOutput); diff --git a/app/scripts/lib/createDupeReqFilterStream.ts b/app/scripts/lib/createDupeReqFilterStream.ts index 63d801e7f1e4..5868d70eec17 100644 --- a/app/scripts/lib/createDupeReqFilterStream.ts +++ b/app/scripts/lib/createDupeReqFilterStream.ts @@ -54,7 +54,8 @@ export default function createDupeReqFilterStream() { transform(chunk: JsonRpcRequest, _, cb) { // JSON-RPC notifications have no ids; our only recourse is to let them through. const hasNoId = chunk.id === undefined; - const requestNotYetSeen = seenRequestIds.add(chunk.id); + const requestNotYetSeen = + chunk.id !== null && seenRequestIds.add(chunk.id); if (hasNoId || requestNotYetSeen) { cb(null, chunk); diff --git a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts index 3b677225a148..a5e04f6b7834 100644 --- a/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts +++ b/app/scripts/lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware.test.ts @@ -32,11 +32,6 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { method: 'eth_sendTransaction', calledNext: false, }, - { - accountType: BtcAccountType.P2wpkh, - method: 'eth_sign', - calledNext: false, - }, { accountType: BtcAccountType.P2wpkh, method: 'eth_signTypedData', @@ -72,11 +67,6 @@ describe('createEvmMethodsToNonEvmAccountReqFilterMiddleware', () => { method: 'eth_sendTransaction', calledNext: true, }, - { - accountType: EthAccountType.Eoa, - method: 'eth_sign', - calledNext: true, - }, { accountType: EthAccountType.Eoa, method: 'eth_signTypedData', diff --git a/app/scripts/lib/createMetamaskMiddleware.js b/app/scripts/lib/createMetamaskMiddleware.js index c0114dd2323c..d48ae32dc4a3 100644 --- a/app/scripts/lib/createMetamaskMiddleware.js +++ b/app/scripts/lib/createMetamaskMiddleware.js @@ -9,7 +9,6 @@ export default function createMetamaskMiddleware({ version, getAccounts, processTransaction, - processEthSignMessage, processTypedMessage, processTypedMessageV3, processTypedMessageV4, @@ -27,7 +26,6 @@ export default function createMetamaskMiddleware({ createWalletMiddleware({ getAccounts, processTransaction, - processEthSignMessage, processTypedMessage, processTypedMessageV3, processTypedMessageV4, diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index 1d7f42710cd6..3e799cc4828f 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -14,9 +14,10 @@ import { BlockaidReason, } from '../../../shared/constants/security-provider'; import { - EIP712_PRIMARY_TYPE_PERMIT, - SIGNING_METHODS, -} from '../../../shared/constants/transaction'; + PRIMARY_TYPES_ORDER, + PRIMARY_TYPES_PERMIT, +} from '../../../shared/constants/signatures'; +import { SIGNING_METHODS } from '../../../shared/constants/transaction'; import { getBlockaidMetricsProps } from '../../../ui/helpers/utils/metrics'; import { REDESIGN_APPROVAL_TYPES } from '../../../ui/pages/confirmations/utils/confirm'; import { getSnapAndHardwareInfoForMetrics } from './snap-keyring/metrics'; @@ -37,7 +38,6 @@ const RATE_LIMIT_TYPES = { * default is RANDOM_SAMPLE */ const RATE_LIMIT_MAP = { - [MESSAGE_TYPE.ETH_SIGN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, @@ -56,7 +56,6 @@ const RATE_LIMIT_MAP = { const MESSAGE_TYPE_TO_APPROVAL_TYPE = { [MESSAGE_TYPE.PERSONAL_SIGN]: ApprovalType.PersonalSign, - [MESSAGE_TYPE.ETH_SIGN]: ApprovalType.Sign, [MESSAGE_TYPE.SIGN]: ApprovalType.SignTransaction, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: ApprovalType.EthSignTypedData, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V1]: ApprovalType.EthSignTypedData, @@ -70,12 +69,6 @@ const MESSAGE_TYPE_TO_APPROVAL_TYPE = { * appropriate event names. */ const EVENT_NAME_MAP = { - [MESSAGE_TYPE.ETH_SIGN]: { - APPROVED: MetaMetricsEventName.SignatureApproved, - FAILED: MetaMetricsEventName.SignatureFailed, - REJECTED: MetaMetricsEventName.SignatureRejected, - REQUESTED: MetaMetricsEventName.SignatureRequested, - }, [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: { APPROVED: MetaMetricsEventName.SignatureApproved, REJECTED: MetaMetricsEventName.SignatureRejected, @@ -148,7 +141,6 @@ let globalRateLimitCount = 0; * @param {Function} opts.getDeviceModel * @param {Function} opts.isConfirmationRedesignEnabled * @param {RestrictedControllerMessenger} opts.snapAndHardwareMessenger - * @param {AppStateController} opts.appStateController * @param {number} [opts.globalRateLimitTimeout] - time, in milliseconds, of the sliding * time window that should limit the number of method calls tracked to globalRateLimitMaxAmount. * @param {number} [opts.globalRateLimitMaxAmount] - max number of method calls that should @@ -300,11 +292,16 @@ export default function createRPCMethodTrackingMiddleware({ } else if (method === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4) { const { primaryType } = parseTypedDataMessage(data); eventProperties.eip712_primary_type = primaryType; - if (primaryType === EIP712_PRIMARY_TYPE_PERMIT) { + if (PRIMARY_TYPES_PERMIT.includes(primaryType)) { eventProperties.ui_customizations = [ ...(eventProperties.ui_customizations || []), MetaMetricsEventUiCustomization.Permit, ]; + } else if (PRIMARY_TYPES_ORDER.includes(primaryType)) { + eventProperties.ui_customizations = [ + ...(eventProperties.ui_customizations || []), + MetaMetricsEventUiCustomization.Order, + ]; } } } catch (e) { @@ -344,19 +341,10 @@ export default function createRPCMethodTrackingMiddleware({ if (shouldTrackEvent === false || typeof eventType === 'undefined') { return callback(); } - - // The rpc error methodNotFound implies that 'eth_sign' is disabled in Advanced Settings - const isDisabledEthSignAdvancedSetting = - method === MESSAGE_TYPE.ETH_SIGN && - res.error?.code === errorCodes.rpc.methodNotFound; - - const isDisabledRPCMethod = isDisabledEthSignAdvancedSetting; + const location = res.error?.data?.location; let event; - if (isDisabledRPCMethod) { - event = eventType.FAILED; - eventProperties.error = res.error; - } else if (res.error?.code === errorCodes.provider.userRejectedRequest) { + if (res.error?.code === errorCodes.provider.userRejectedRequest) { event = eventType.REJECTED; } else if ( res.error?.code === errorCodes.rpc.internal && @@ -370,22 +358,20 @@ export default function createRPCMethodTrackingMiddleware({ } let blockaidMetricProps = {}; - if (!isDisabledRPCMethod) { - if (SIGNING_METHODS.includes(method)) { - const securityAlertResponse = - appStateController.getSignatureSecurityAlertResponse( - req.securityAlertResponse?.securityAlertId, - ); - - blockaidMetricProps = getBlockaidMetricsProps({ - securityAlertResponse, - }); - } - } + if (SIGNING_METHODS.includes(method)) { + const securityAlertResponse = + appStateController.getSignatureSecurityAlertResponse( + req.securityAlertResponse?.securityAlertId, + ); + blockaidMetricProps = getBlockaidMetricsProps({ + securityAlertResponse, + }); + } const properties = { ...eventProperties, ...blockaidMetricProps, + location, // if security_alert_response from blockaidMetricProps is Benign, force set security_alert_reason to empty string security_alert_reason: blockaidMetricProps.security_alert_response === diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index a0c0da552db1..af3c1ee4768b 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -99,7 +99,7 @@ describe('createRPCMethodTrackingMiddleware', () => { describe('before participateInMetaMetrics is set', () => { it('should not track an event for a signature request', async () => { const req = { - method: MESSAGE_TYPE.ETH_SIGN, + method: MESSAGE_TYPE.PERSONAL_SIGN, origin: 'some.dapp', }; @@ -121,7 +121,7 @@ describe('createRPCMethodTrackingMiddleware', () => { it('should not track an event for a signature request', async () => { const req = { - method: MESSAGE_TYPE.ETH_SIGN, + method: MESSAGE_TYPE.PERSONAL_SIGN, origin: 'some.dapp', }; @@ -143,7 +143,7 @@ describe('createRPCMethodTrackingMiddleware', () => { it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => { const req = { - method: MESSAGE_TYPE.ETH_SIGN, + method: MESSAGE_TYPE.PERSONAL_SIGN, origin: 'some.dapp', securityAlertResponse: { result_type: BlockaidResultType.Malicious, @@ -163,7 +163,7 @@ describe('createRPCMethodTrackingMiddleware', () => { category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, security_alert_response: BlockaidResultType.Malicious, security_alert_reason: BlockaidReason.maliciousDomain, }, @@ -173,7 +173,7 @@ describe('createRPCMethodTrackingMiddleware', () => { it(`should track an event with correct blockaid parameters when providerRequestsCount is provided`, async () => { const req = { - method: MESSAGE_TYPE.ETH_SIGN, + method: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, origin: 'some.dapp', securityAlertResponse: { result_type: BlockaidResultType.Malicious, @@ -204,7 +204,7 @@ describe('createRPCMethodTrackingMiddleware', () => { category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, + signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, security_alert_response: BlockaidResultType.Malicious, security_alert_reason: BlockaidReason.maliciousDomain, ppom_eth_call_count: 5, @@ -245,7 +245,10 @@ describe('createRPCMethodTrackingMiddleware', () => { }; const res = { - error: { code: errorCodes.provider.userRejectedRequest }, + error: { + code: errorCodes.provider.userRejectedRequest, + data: { location: 'some_location' }, + }, }; const { next, executeMiddlewareStack } = getNext(); const handler = createHandler(); @@ -257,6 +260,7 @@ describe('createRPCMethodTrackingMiddleware', () => { event: MetaMetricsEventName.SignatureRejected, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + location: 'some_location', }, referrer: { url: 'some.dapp' }, }); @@ -537,40 +541,10 @@ describe('createRPCMethodTrackingMiddleware', () => { }); }); - describe(`when '${MESSAGE_TYPE.ETH_SIGN}' is disabled in advanced settings`, () => { - it(`should track ${MetaMetricsEventName.SignatureFailed} and include error property`, async () => { - const mockError = { code: errorCodes.rpc.methodNotFound }; - const req = { - method: MESSAGE_TYPE.ETH_SIGN, - origin: 'some.dapp', - }; - const res = { - error: mockError, - }; - const { next, executeMiddlewareStack } = getNext(); - const handler = createHandler(); - - await handler(req, res, next); - await executeMiddlewareStack(); - - expect(trackEvent).toHaveBeenCalledTimes(2); - - expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: MetaMetricsEventCategory.InpageProvider, - event: MetaMetricsEventName.SignatureFailed, - properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, - error: mockError, - }, - referrer: { url: 'some.dapp' }, - }); - }); - }); - describe('when request is flagged as safe by security provider', () => { it(`should immediately track a ${MetaMetricsEventName.SignatureRequested} event`, async () => { const req = { - method: MESSAGE_TYPE.ETH_SIGN, + method: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, origin: 'some.dapp', }; const res = { @@ -586,7 +560,7 @@ describe('createRPCMethodTrackingMiddleware', () => { category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { - signature_type: MESSAGE_TYPE.ETH_SIGN, + signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, }, referrer: { url: 'some.dapp' }, }); diff --git a/app/scripts/lib/createTracingMiddleware.test.ts b/app/scripts/lib/createTracingMiddleware.test.ts new file mode 100644 index 000000000000..ceb87e0f3ac5 --- /dev/null +++ b/app/scripts/lib/createTracingMiddleware.test.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { MESSAGE_TYPE } from '../../../shared/constants/app'; +import createTracingMiddleware from './createTracingMiddleware'; + +const REQUEST_MOCK = { + id: 'testId', + method: MESSAGE_TYPE.ETH_SEND_TRANSACTION, +} as any; + +const RESPONSE_MOCK = {}; +const NEXT_MOCK = jest.fn(); + +describe('createTracingMiddleware', () => { + let request: any; + + beforeEach(() => { + jest.resetAllMocks(); + + request = { ...REQUEST_MOCK }; + + global.sentry = { + getMetaMetricsEnabled: () => Promise.resolve(true), + }; + }); + + it('adds trace context to request if method is send transaction', async () => { + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + + expect(request.traceContext).toBeDefined(); + }); + + it('does not add trace context to request if method not supported', async () => { + request.method = MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4; + + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + + expect(request.traceContext).toBeUndefined(); + }); + + it('calls next', async () => { + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + + expect(NEXT_MOCK).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/scripts/lib/createTracingMiddleware.ts b/app/scripts/lib/createTracingMiddleware.ts new file mode 100644 index 000000000000..99e0a73795b1 --- /dev/null +++ b/app/scripts/lib/createTracingMiddleware.ts @@ -0,0 +1,31 @@ +// Request and repsones are currently untyped. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { MESSAGE_TYPE } from '../../../shared/constants/app'; +import { trace, TraceName } from '../../../shared/lib/trace'; + +export default function createTracingMiddleware() { + return async function tracingMiddleware( + req: any, + _res: any, + next: () => void, + ) { + const { id, method } = req; + + if (method === MESSAGE_TYPE.ETH_SEND_TRANSACTION) { + req.traceContext = await trace({ + name: TraceName.Transaction, + id, + tags: { source: 'dapp' }, + }); + + await trace({ + name: TraceName.Middleware, + id, + parentContext: req.traceContext, + }); + } + + next(); + }; +} diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index be997cdf1e98..b7415e2ac94c 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -62,7 +62,7 @@ export default function setupEnsIpfsResolver({ // We cannot show this if useAddressBarEnsResolution is off... if (useAddressBarEnsResolution && ipfsGateway) { - browser.tabs.update(tabId, { url: 'loading.html' }); + await browser.tabs.update(tabId, { url: 'loading.html' }); } let url = ensSiteUrl; @@ -71,7 +71,7 @@ export default function setupEnsIpfsResolver({ // we assume the ENS domains URL if (process.env.IN_TEST) { if (useAddressBarEnsResolution || ipfsGateway) { - browser.tabs.update(tabId, { url }); + await browser.tabs.update(tabId, { url }); } return; } @@ -137,7 +137,7 @@ export default function setupEnsIpfsResolver({ (useAddressBarEnsResolution || (!useAddressBarEnsResolution && url !== ensSiteUrl)) ) { - browser.tabs.update(tabId, { url }); + await browser.tabs.update(tabId, { url }); } } } diff --git a/app/scripts/lib/hardware-keyring-builder-factory.ts b/app/scripts/lib/hardware-keyring-builder-factory.ts index 4579c2a20375..ab88a103510f 100644 --- a/app/scripts/lib/hardware-keyring-builder-factory.ts +++ b/app/scripts/lib/hardware-keyring-builder-factory.ts @@ -1,13 +1,15 @@ import type { TrezorBridge } from '@metamask/eth-trezor-keyring'; import type { LedgerBridge } from '@metamask/eth-ledger-bridge-keyring'; import { KeyringClass, Json } from '@metamask/utils'; +import { FakeKeyringBridge } from '../../../test/stub/keyring-bridge'; /** * A transport bridge between the keyring and the hardware device. */ export type HardwareTransportBridgeClass = | (new () => TrezorBridge) - | (new () => LedgerBridge); + | (new () => LedgerBridge) + | (new () => FakeKeyringBridge); /** * Get builder function for Hardware keyrings which require an additional `opts` diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js index 40908af5e2f5..886693ff9469 100644 --- a/app/scripts/lib/local-store.js +++ b/app/scripts/lib/local-store.js @@ -17,6 +17,7 @@ export default class ExtensionStore { // data persistence errors to sentry this.dataPersistenceFailing = false; this.mostRecentRetrievedState = null; + this.isExtensionInitialized = false; } setMetadata(initMetaData) { @@ -51,6 +52,8 @@ export default class ExtensionStore { captureException(err); } log.error('error setting state in local store:', err); + } finally { + this.isExtensionInitialized = true; } } @@ -63,6 +66,7 @@ export default class ExtensionStore { if (!this.isSupported) { return undefined; } + const result = await this._get(); // extension.storage.local always returns an obj // if the object is empty, treat it as undefined @@ -70,7 +74,9 @@ export default class ExtensionStore { this.mostRecentRetrievedState = null; return undefined; } - this.mostRecentRetrievedState = result; + if (!this.isExtensionInitialized) { + this.mostRecentRetrievedState = result; + } return result; } @@ -114,6 +120,12 @@ export default class ExtensionStore { }); }); } + + cleanUpMostRecentRetrievedState() { + if (this.mostRecentRetrievedState) { + this.mostRecentRetrievedState = null; + } + } } /** diff --git a/app/scripts/lib/local-store.test.js b/app/scripts/lib/local-store.test.js index 8b786ca819f0..34289185de79 100644 --- a/app/scripts/lib/local-store.test.js +++ b/app/scripts/lib/local-store.test.js @@ -28,9 +28,13 @@ describe('LocalStore', () => { it('should initialize mostRecentRetrievedState to null', () => { const localStore = setup({ localMock: false }); - expect(localStore.mostRecentRetrievedState).toBeNull(); }); + + it('should initialize isExtensionInitialized to false', () => { + const localStore = setup({ localMock: false }); + expect(localStore.isExtensionInitialized).toBeFalsy(); + }); }); describe('setMetadata', () => { @@ -74,6 +78,13 @@ describe('LocalStore', () => { localStore.set({ appState: { test: true } }); }).not.toThrow(); }); + + it('should set isExtensionInitialized if data is set with no error', async () => { + const localStore = setup(); + localStore.setMetadata({ version: 74 }); + await localStore.set({ appState: { test: true } }); + expect(localStore.isExtensionInitialized).toBeTruthy(); + }); }); describe('get', () => { @@ -112,5 +123,44 @@ describe('LocalStore', () => { expect(localStore.mostRecentRetrievedState).toStrictEqual(null); }); + + it('should set mostRecentRetrievedState to current state if isExtensionInitialized is true', async () => { + const localStore = setup({ + localMock: { + get: jest.fn().mockImplementation(() => Promise.resolve({})), + }, + }); + localStore.setMetadata({ version: 74 }); + await localStore.set({ appState: { test: true } }); + await localStore.get(); + expect(localStore.mostRecentRetrievedState).toStrictEqual(null); + }); + }); + + describe('cleanUpMostRecentRetrievedState', () => { + it('should set mostRecentRetrievedState to null if it is defined', async () => { + const localStore = setup({ + localMock: { + get: jest + .fn() + .mockImplementation(() => + Promise.resolve({ appState: { test: true } }), + ), + }, + }); + await localStore.get(); + + // mostRecentRetrievedState should be { appState: { test: true } } at this stage + await localStore.cleanUpMostRecentRetrievedState(); + expect(localStore.mostRecentRetrievedState).toStrictEqual(null); + }); + + it('should not set mostRecentRetrievedState if it is null', async () => { + const localStore = setup(); + + expect(localStore.mostRecentRetrievedState).toStrictEqual(null); + await localStore.cleanUpMostRecentRetrievedState(); + expect(localStore.mostRecentRetrievedState).toStrictEqual(null); + }); }); }); diff --git a/app/scripts/lib/network-store.js b/app/scripts/lib/network-store.js index 3a0326a2b2e6..e167807c7d9d 100644 --- a/app/scripts/lib/network-store.js +++ b/app/scripts/lib/network-store.js @@ -89,4 +89,10 @@ export default class ReadOnlyNetworkStore { } this._state = { data: state, meta: this._metadata }; } + + cleanUpMostRecentRetrievedState() { + if (this.mostRecentRetrievedState) { + this.mostRecentRetrievedState = null; + } + } } diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index 354b2a8e4408..6b9c78a7ddc7 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -1,8 +1,6 @@ -import { - type Hex, - JsonRpcRequestStruct, - JsonRpcResponseStruct, -} from '@metamask/utils'; +import { type Hex, JsonRpcResponseStruct } from '@metamask/utils'; +import * as ControllerUtils from '@metamask/controller-utils'; + import { CHAIN_IDS } from '../../../../shared/constants/network'; import { @@ -10,10 +8,12 @@ import { BlockaidResultType, } from '../../../../shared/constants/security-provider'; import { flushPromises } from '../../../../test/lib/timer-helpers'; -import { createPPOMMiddleware } from './ppom-middleware'; +import { mockNetworkState } from '../../../../test/stub/networks'; +import { createPPOMMiddleware, PPOMMiddlewareRequest } from './ppom-middleware'; import { generateSecurityAlertId, handlePPOMError, + isChainSupported, validateRequestWithPPOM, } from './ppom-util'; import { SecurityAlertResponse } from './types'; @@ -21,6 +21,7 @@ import { SecurityAlertResponse } from './types'; jest.mock('./ppom-util'); const SECURITY_ALERT_ID_MOCK = '123'; +const INTERNAL_ACCOUNT_ADDRESS = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { securityAlertId: SECURITY_ALERT_ID_MOCK, @@ -28,6 +29,12 @@ const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { reason: BlockaidReason.permitFarming, }; +const REQUEST_MOCK = { + params: [], + id: '', + jsonrpc: '2.0' as const, +}; + const createMiddleware = ( options: { chainId?: Hex; @@ -48,8 +55,7 @@ const createMiddleware = ( const preferenceController = { store: { getState: () => ({ - securityAlertsEnabled: - securityAlertsEnabled === undefined ?? securityAlertsEnabled, + securityAlertsEnabled: securityAlertsEnabled ?? true, }), }, }; @@ -61,13 +67,17 @@ const createMiddleware = ( } const networkController = { - state: { providerConfig: { chainId: chainId || CHAIN_IDS.MAINNET } }, + state: mockNetworkState({ chainId: chainId || CHAIN_IDS.MAINNET }), }; const appStateController = { addSignatureSecurityAlertResponse: () => undefined, }; + const accountsController = { + listAccounts: () => [{ address: INTERNAL_ACCOUNT_ADDRESS }], + }; + return createPPOMMiddleware( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -81,6 +91,8 @@ const createMiddleware = ( // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any appStateController as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + accountsController as any, updateSecurityAlertResponse, ); }; @@ -89,6 +101,7 @@ describe('PPOMMiddleware', () => { const validateRequestWithPPOMMock = jest.mocked(validateRequestWithPPOM); const generateSecurityAlertIdMock = jest.mocked(generateSecurityAlertId); const handlePPOMErrorMock = jest.mocked(handlePPOMError); + const isChainSupportedMock = jest.mocked(isChainSupported); beforeEach(() => { jest.resetAllMocks(); @@ -96,6 +109,7 @@ describe('PPOMMiddleware', () => { validateRequestWithPPOMMock.mockResolvedValue(SECURITY_ALERT_RESPONSE_MOCK); generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK); handlePPOMErrorMock.mockReturnValue(SECURITY_ALERT_RESPONSE_MOCK); + isChainSupportedMock.mockResolvedValue(true); }); it('updates alert response after validating request', async () => { @@ -106,14 +120,14 @@ describe('PPOMMiddleware', () => { }); const req = { - ...JsonRpcRequestStruct, + ...REQUEST_MOCK, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; await middlewareFunction( req, - { ...JsonRpcResponseStruct }, + { ...JsonRpcResponseStruct.TYPE }, () => undefined, ); @@ -130,20 +144,20 @@ describe('PPOMMiddleware', () => { it('adds loading response to confirmation requests while validation is in progress', async () => { const middlewareFunction = createMiddleware(); - const req = { - ...JsonRpcRequestStruct, + const req: PPOMMiddlewareRequest<(string | { to: string })[]> = { + ...REQUEST_MOCK, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; await middlewareFunction( req, - { ...JsonRpcResponseStruct }, + { ...JsonRpcResponseStruct.TYPE }, () => undefined, ); - expect(req.securityAlertResponse.reason).toBe(BlockaidReason.inProgress); - expect(req.securityAlertResponse.result_type).toBe( + expect(req.securityAlertResponse?.reason).toBe(BlockaidReason.inProgress); + expect(req.securityAlertResponse?.result_type).toBe( BlockaidResultType.Loading, ); }); @@ -154,11 +168,12 @@ describe('PPOMMiddleware', () => { }); const req = { - ...JsonRpcRequestStruct, + ...REQUEST_MOCK, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; + // @ts-expect-error Passing in invalid input for testing purposes await middlewareFunction(req, undefined, () => undefined); expect(req.securityAlertResponse).toBeUndefined(); @@ -166,19 +181,20 @@ describe('PPOMMiddleware', () => { }); it('does not do validation if user is not on a supported network', async () => { + isChainSupportedMock.mockResolvedValue(false); const middlewareFunction = createMiddleware({ chainId: '0x2', }); const req = { - ...JsonRpcRequestStruct, + ...REQUEST_MOCK, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; await middlewareFunction( req, - { ...JsonRpcResponseStruct }, + { ...JsonRpcResponseStruct.TYPE }, () => undefined, ); @@ -190,14 +206,34 @@ describe('PPOMMiddleware', () => { const middlewareFunction = createMiddleware(); const req = { - ...JsonRpcRequestStruct, + ...REQUEST_MOCK, method: 'eth_someRequest', securityAlertResponse: undefined, }; await middlewareFunction( req, - { ...JsonRpcResponseStruct }, + { ...JsonRpcResponseStruct.TYPE }, + () => undefined, + ); + + expect(req.securityAlertResponse).toBeUndefined(); + expect(validateRequestWithPPOM).not.toHaveBeenCalled(); + }); + + it('does not do validation when request is send to users own account', async () => { + const middlewareFunction = createMiddleware(); + + const req = { + ...REQUEST_MOCK, + params: [{ to: INTERNAL_ACCOUNT_ADDRESS }], + method: 'eth_sendTransaction', + securityAlertResponse: undefined, + }; + + await middlewareFunction( + req, + { ...JsonRpcResponseStruct.TYPE }, () => undefined, ); @@ -205,13 +241,59 @@ describe('PPOMMiddleware', () => { expect(validateRequestWithPPOM).not.toHaveBeenCalled(); }); + it('does not do validation for SIWE signature', async () => { + const middlewareFunction = createMiddleware({ + securityAlertsEnabled: true, + }); + + const req = { + method: 'personal_sign', + params: [ + '0x6d6574616d61736b2e6769746875622e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078393335653733656462396666353265323362616337663765303433613165636430366430353437370a0a492061636365707420746865204d6574614d61736b205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a2068747470733a2f2f6d6574616d61736b2e6769746875622e696f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2033323839313735370a4973737565642041743a20323032312d30392d33305431363a32353a32342e3030305a', + '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + 'Example password', + ], + jsonrpc: '2.0' as const, + id: 2974202441, + origin: 'https://metamask.github.io', + networkClientId: 'mainnet', + tabId: 1048745900, + securityAlertResponse: undefined, + }; + jest.spyOn(ControllerUtils, 'detectSIWE').mockReturnValue({ + isSIWEMessage: true, + parsedMessage: { + address: '0x935e73edb9ff52e23bac7f7e049a1ecd06d05477', + chainId: 1, + domain: 'metamask.github.io', + expirationTime: null, + issuedAt: '2021-09-30T16:25:24.000Z', + nonce: '32891757', + notBefore: '2022-03-17T12:45:13.610Z', + requestId: 'some_id', + scheme: null, + statement: + 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', + uri: 'https://metamask.github.io', + version: '1', + resources: null, + }, + }); + + // @ts-expect-error Passing invalid input for testing purposes + await middlewareFunction(req, undefined, () => undefined); + + expect(req.securityAlertResponse).toBeUndefined(); + expect(validateRequestWithPPOM).not.toHaveBeenCalled(); + }); + it('calls next method', async () => { const middlewareFunction = createMiddleware(); const nextMock = jest.fn(); await middlewareFunction( - { ...JsonRpcRequestStruct, method: 'eth_sendTransaction' }, - { ...JsonRpcResponseStruct }, + { ...REQUEST_MOCK, method: 'eth_sendTransaction' }, + { ...JsonRpcResponseStruct.TYPE }, nextMock, ); @@ -227,12 +309,12 @@ describe('PPOMMiddleware', () => { const middlewareFunction = createMiddleware({ error }); const req = { - ...JsonRpcRequestStruct, + ...REQUEST_MOCK, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; - await middlewareFunction(req, { ...JsonRpcResponseStruct }, nextMock); + await middlewareFunction(req, { ...JsonRpcResponseStruct.TYPE }, nextMock); expect(req.securityAlertResponse).toStrictEqual( SECURITY_ALERT_RESPONSE_MOCK, diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index 0bdd46edba3f..76bc5daaf88c 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -1,3 +1,4 @@ +import { AccountsController } from '@metamask/accounts-controller'; import { PPOMController } from '@metamask/ppom-validator'; import { NetworkController } from '@metamask/network-controller'; import { @@ -6,17 +7,19 @@ import { JsonRpcRequest, JsonRpcResponse, } from '@metamask/utils'; +import { detectSIWE } from '@metamask/controller-utils'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { PreferencesController } from '../../controllers/preferences'; import { AppStateController } from '../../controllers/app-state'; -import { - LOADING_SECURITY_ALERT_RESPONSE, - SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, -} from '../../../../shared/constants/security-provider'; +import { LOADING_SECURITY_ALERT_RESPONSE } from '../../../../shared/constants/security-provider'; +import { getProviderConfig } from '../../../../ui/ducks/metamask/metamask'; +import { trace, TraceContext, TraceName } from '../../../../shared/lib/trace'; import { generateSecurityAlertId, handlePPOMError, + isChainSupported, validateRequestWithPPOM, } from './ppom-util'; import { SecurityAlertResponse } from './types'; @@ -27,6 +30,13 @@ const CONFIRMATION_METHODS = Object.freeze([ ...SIGNING_METHODS, ]); +export type PPOMMiddlewareRequest< + Params extends JsonRpcParams = JsonRpcParams, +> = Required> & { + securityAlertResponse?: SecurityAlertResponse | undefined; + traceContext?: TraceContext; +}; + /** * Middleware function that handles JSON RPC requests. * This function will be called for every JSON RPC request. @@ -40,17 +50,19 @@ const CONFIRMATION_METHODS = Object.freeze([ * @param preferencesController - Instance of PreferenceController. * @param networkController - Instance of NetworkController. * @param appStateController + * @param accountsController - Instance of AccountsController. * @param updateSecurityAlertResponse * @returns PPOMMiddleware function. */ export function createPPOMMiddleware< - Params extends JsonRpcParams, + Params extends (string | { to: string })[], Result extends Json, >( ppomController: PPOMController, preferencesController: PreferencesController, networkController: NetworkController, appStateController: AppStateController, + accountsController: AccountsController, updateSecurityAlertResponse: ( method: string, signatureAlertId: string, @@ -58,7 +70,7 @@ export function createPPOMMiddleware< ) => void, ) { return async ( - req: JsonRpcRequest, + req: PPOMMiddlewareRequest, _res: JsonRpcResponse, next: () => void, ) => { @@ -66,29 +78,54 @@ export function createPPOMMiddleware< const securityAlertsEnabled = preferencesController.store.getState()?.securityAlertsEnabled; - const { chainId } = networkController.state.providerConfig; + const { chainId } = getProviderConfig({ + metamask: networkController.state, + }); + const isSupportedChain = await isChainSupported(chainId); if ( !securityAlertsEnabled || !CONFIRMATION_METHODS.includes(req.method) || - !SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS.includes(chainId) + !isSupportedChain ) { return; } + const data = req.params[0]; + if (typeof data === 'string') { + const { isSIWEMessage } = detectSIWE({ data }); + if (isSIWEMessage) { + return; + } + } else if (req.method === MESSAGE_TYPE.ETH_SEND_TRANSACTION) { + const { to: toAddress } = data ?? {}; + const internalAccounts = accountsController.listAccounts(); + const isToInternalAccount = internalAccounts.some( + ({ address }) => address?.toLowerCase() === toAddress?.toLowerCase(), + ); + if (isToInternalAccount) { + return; + } + } + const securityAlertId = generateSecurityAlertId(); - validateRequestWithPPOM({ - ppomController, - request: req, - securityAlertId, - }).then((securityAlertResponse) => { - updateSecurityAlertResponse( - req.method, - securityAlertId, - securityAlertResponse, - ); - }); + trace( + { name: TraceName.PPOMValidation, parentContext: req.traceContext }, + () => + validateRequestWithPPOM({ + ppomController, + request: req, + securityAlertId, + chainId, + }).then((securityAlertResponse) => { + updateSecurityAlertResponse( + req.method, + securityAlertId, + securityAlertResponse, + ); + }), + ); const loadingSecurityAlertResponse: SecurityAlertResponse = { ...LOADING_SECURITY_ALERT_RESPONSE, diff --git a/app/scripts/lib/ppom/ppom-util.test.ts b/app/scripts/lib/ppom/ppom-util.test.ts index ac16022cee8a..fb93ee54e4b3 100644 --- a/app/scripts/lib/ppom/ppom-util.test.ts +++ b/app/scripts/lib/ppom/ppom-util.test.ts @@ -11,14 +11,17 @@ import { Message } from '@metamask/message-manager'; import { BlockaidReason, BlockaidResultType, + SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { AppStateController } from '../../controllers/app-state'; import { generateSecurityAlertId, + isChainSupported, updateSecurityAlertResponse, validateRequestWithPPOM, } from './ppom-util'; import { SecurityAlertResponse } from './types'; +import * as securityAlertAPI from './security-alerts-api'; jest.mock('@metamask/transaction-controller', () => ({ ...jest.requireActual('@metamask/transaction-controller'), @@ -27,15 +30,19 @@ jest.mock('@metamask/transaction-controller', () => ({ const SECURITY_ALERT_ID_MOCK = '1234-5678'; const TRANSACTION_ID_MOCK = '123'; +const CHAIN_ID_MOCK = '0x1'; const REQUEST_MOCK = { method: 'eth_signTypedData_v4', params: [], + id: '', + jsonrpc: '2.0' as const, }; const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { result_type: 'success', reason: 'success', + source: SecurityAlertSource.Local, }; const TRANSACTION_PARAMS_MOCK_1: TransactionParams = { @@ -95,10 +102,18 @@ describe('PPOM Utils', () => { const normalizeTransactionParamsMock = jest.mocked( normalizeTransactionParams, ); + const getSupportedChainIdsMock = jest.spyOn( + securityAlertAPI, + 'getSecurityAlertsAPISupportedChainIds', + ); + let isSecurityAlertsEnabledMock: jest.SpyInstance; beforeEach(() => { jest.resetAllMocks(); jest.spyOn(console, 'error').mockImplementation(() => undefined); + isSecurityAlertsEnabledMock = jest + .spyOn(securityAlertAPI, 'isSecurityAlertsAPIEnabled') + .mockReturnValue(false); }); describe('validateRequestWithPPOM', () => { @@ -109,15 +124,14 @@ describe('PPOM Utils', () => { ppom.validateJsonRpc.mockResolvedValue(SECURITY_ALERT_RESPONSE_MOCK); ppomController.usePPOM.mockImplementation( - (callback) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback(ppom as any) as any, + (callback) => callback(ppom as any) as any, ); const response = await validateRequestWithPPOM({ ppomController, request: REQUEST_MOCK, securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, }); expect(response).toStrictEqual({ @@ -145,6 +159,7 @@ describe('PPOM Utils', () => { ppomController, request: REQUEST_MOCK, securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, }); expect(response).toStrictEqual({ @@ -163,6 +178,7 @@ describe('PPOM Utils', () => { ppomController, request: REQUEST_MOCK, securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, }); expect(response).toStrictEqual({ @@ -194,6 +210,7 @@ describe('PPOM Utils', () => { ppomController, request, securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, }); expect(ppom.validateJsonRpc).toHaveBeenCalledTimes(1); @@ -238,8 +255,7 @@ describe('PPOM Utils', () => { securityAlertId: SECURITY_ALERT_ID_MOCK, securityAlertResponse: SECURITY_ALERT_RESPONSE_MOCK, signatureController, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transactionController: {} as any, + transactionController: {} as unknown as TransactionController, }); expect( @@ -261,7 +277,7 @@ describe('PPOM Utils', () => { }, }, ], - } as any); + } as unknown as TransactionController['state']); await updateSecurityAlertResponse({ appStateController: {} as any, @@ -281,4 +297,97 @@ describe('PPOM Utils', () => { ).toHaveBeenCalledWith(TRANSACTION_ID_MOCK, SECURITY_ALERT_RESPONSE_MOCK); }); }); + + describe('validateWithAPI', () => { + const request = { + ...REQUEST_MOCK, + method: 'eth_sendTransaction', + params: [TRANSACTION_PARAMS_MOCK_1], + }; + + it('uses security alerts API if enabled', async () => { + isSecurityAlertsEnabledMock.mockReturnValue(true); + normalizeTransactionParamsMock.mockReturnValue(TRANSACTION_PARAMS_MOCK_1); + const validateWithSecurityAlertsAPIMock = jest + .spyOn(securityAlertAPI, 'validateWithSecurityAlertsAPI') + .mockResolvedValue(SECURITY_ALERT_RESPONSE_MOCK); + + const ppom = createPPOMMock(); + const ppomController = createPPOMControllerMock(); + + await validateRequestWithPPOM({ + ppomController, + request, + securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + expect(ppomController.usePPOM).not.toHaveBeenCalled(); + expect(ppom.validateJsonRpc).not.toHaveBeenCalled(); + + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledTimes(1); + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledWith( + CHAIN_ID_MOCK, + request, + ); + }); + + it('uses controller if security alerts API throws', async () => { + isSecurityAlertsEnabledMock.mockReturnValue(true); + normalizeTransactionParamsMock.mockReturnValue(TRANSACTION_PARAMS_MOCK_1); + + const ppomController = createPPOMControllerMock(); + + const validateWithSecurityAlertsAPIMock = jest + .spyOn(securityAlertAPI, 'validateWithSecurityAlertsAPI') + .mockRejectedValue(new Error('Test Error')); + + await validateRequestWithPPOM({ + ppomController, + request, + securityAlertId: SECURITY_ALERT_ID_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + expect(ppomController.usePPOM).toHaveBeenCalledTimes(1); + + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledTimes(1); + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledWith( + CHAIN_ID_MOCK, + request, + ); + }); + }); + + describe('isChainSupported', () => { + describe('when security alerts API is enabled', () => { + beforeEach(async () => { + isSecurityAlertsEnabledMock.mockReturnValue(true); + getSupportedChainIdsMock.mockResolvedValue([CHAIN_ID_MOCK]); + }); + + it('returns true if chain is supported', async () => { + expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true); + }); + + it('returns false if chain is not supported', async () => { + expect(await isChainSupported('0x2')).toStrictEqual(false); + }); + + it('returns correctly if security alerts API throws', async () => { + getSupportedChainIdsMock.mockRejectedValue(new Error('Test Error')); + expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true); + }); + }); + + describe('when security alerts API is disabled', () => { + it('returns true if chain is supported', async () => { + expect(await isChainSupported(CHAIN_ID_MOCK)).toStrictEqual(true); + }); + + it('returns false if chain is not supported', async () => { + expect(await isChainSupported('0x2')).toStrictEqual(false); + }); + }); + }); }); diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index 8d64aacd3a91..193717d35ef1 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -4,17 +4,25 @@ import { TransactionParams, normalizeTransactionParams, } from '@metamask/transaction-controller'; -import { JsonRpcRequest } from '@metamask/utils'; +import { Hex, JsonRpcRequest } from '@metamask/utils'; import { v4 as uuid } from 'uuid'; import { PPOM } from '@blockaid/ppom_release'; import { SignatureController } from '@metamask/signature-controller'; import { BlockaidReason, BlockaidResultType, + SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, + SecurityAlertSource, } from '../../../../shared/constants/security-provider'; import { SIGNING_METHODS } from '../../../../shared/constants/transaction'; import { AppStateController } from '../../controllers/app-state'; import { SecurityAlertResponse } from './types'; +import { + getSecurityAlertsAPISupportedChainIds, + isSecurityAlertsAPIEnabled, + SecurityAlertsAPIRequest, + validateWithSecurityAlertsAPI, +} from './security-alerts-api'; const { sentry } = global; @@ -25,21 +33,34 @@ const SECURITY_ALERT_RESPONSE_ERROR = { reason: BlockaidReason.errored, }; +type PPOMRequest = Omit & { + method: typeof METHOD_SEND_TRANSACTION; + params: [TransactionParams]; +}; export async function validateRequestWithPPOM({ ppomController, request, securityAlertId, + chainId, }: { ppomController: PPOMController; request: JsonRpcRequest; securityAlertId: string; + chainId: Hex; }): Promise { try { - return await ppomController.usePPOM(async (ppom: PPOM) => { - return await usePPOM(request, securityAlertId, ppom); - }); - } catch (error) { - return handlePPOMError(error, 'Error validateRequestWithPPOM#usePPOM: '); + const normalizedRequest = normalizePPOMRequest(request); + + const ppomResponse = isSecurityAlertsAPIEnabled() + ? await validateWithAPI(ppomController, chainId, normalizedRequest) + : await validateWithController(ppomController, normalizedRequest); + + return { + ...ppomResponse, + securityAlertId, + }; + } catch (error: unknown) { + return handlePPOMError(error, 'Error validating JSON RPC using PPOM: '); } } @@ -97,30 +118,34 @@ export function handlePPOMError( }; } -async function usePPOM( - request: JsonRpcRequest, - securityAlertId: string, - ppom: PPOM, -): Promise { - try { - const normalizedRequest = normalizePPOMRequest(request); - const ppomResponse = await ppom.validateJsonRpc(normalizedRequest); +export async function isChainSupported(chainId: Hex): Promise { + let supportedChainIds = SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS; - return { - ...ppomResponse, - securityAlertId, - }; + try { + if (isSecurityAlertsAPIEnabled()) { + supportedChainIds = await getSecurityAlertsAPISupportedChainIds(); + } } catch (error: unknown) { - return handlePPOMError(error, 'Error validating JSON RPC using PPOM: '); + handlePPOMError( + error, + `Error fetching supported chains from security alerts API`, + ); } + return supportedChainIds.includes(chainId); } -function normalizePPOMRequest(request: JsonRpcRequest): JsonRpcRequest { - if (request.method !== METHOD_SEND_TRANSACTION) { +function normalizePPOMRequest( + request: PPOMRequest | JsonRpcRequest, +): PPOMRequest | JsonRpcRequest { + if ( + !((req): req is PPOMRequest => req.method === METHOD_SEND_TRANSACTION)( + request, + ) + ) { return request; } - const transactionParams = (request.params?.[0] || {}) as TransactionParams; + const transactionParams = request.params[0]; const normalizedParams = normalizeTransactionParams(transactionParams); return { @@ -178,3 +203,35 @@ async function findConfirmationBySecurityAlertId( await new Promise((resolve) => setTimeout(resolve, 100)); } } + +async function validateWithController( + ppomController: PPOMController, + request: SecurityAlertsAPIRequest | JsonRpcRequest, +): Promise { + const response = (await ppomController.usePPOM((ppom: PPOM) => + ppom.validateJsonRpc(request), + )) as SecurityAlertResponse; + + return { + ...response, + source: SecurityAlertSource.Local, + }; +} + +async function validateWithAPI( + ppomController: PPOMController, + chainId: string, + request: SecurityAlertsAPIRequest | JsonRpcRequest, +): Promise { + try { + const response = await validateWithSecurityAlertsAPI(chainId, request); + + return { + ...response, + source: SecurityAlertSource.API, + }; + } catch (error: unknown) { + handlePPOMError(error, `Error validating request with security alerts API`); + return await validateWithController(ppomController, request); + } +} diff --git a/app/scripts/lib/ppom/security-alerts-api.test.ts b/app/scripts/lib/ppom/security-alerts-api.test.ts new file mode 100644 index 000000000000..9d2d97652d4f --- /dev/null +++ b/app/scripts/lib/ppom/security-alerts-api.test.ts @@ -0,0 +1,117 @@ +import { + BlockaidReason, + BlockaidResultType, +} from '../../../../shared/constants/security-provider'; +import { + getSecurityAlertsAPISupportedChainIds, + isSecurityAlertsAPIEnabled, + validateWithSecurityAlertsAPI, +} from './security-alerts-api'; + +const CHAIN_ID_MOCK = '0x1'; + +const REQUEST_MOCK = { + method: 'eth_sendTransaction', + params: [ + { + from: '0x123', + to: '0x456', + value: '0x123', + }, + ], +}; + +const RESPONSE_MOCK = { + result_type: BlockaidResultType.Errored, + reason: BlockaidReason.maliciousDomain, + description: 'Test Description', +}; + +describe('Security Alerts API', () => { + const fetchMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + + global.fetch = fetchMock; + + fetchMock.mockResolvedValue({ + ok: true, + json: async () => RESPONSE_MOCK, + }); + + process.env.SECURITY_ALERTS_API_URL = 'https://example.com'; + }); + + describe('validateWithSecurityAlertsAPI', () => { + it('sends POST request', async () => { + const response = await validateWithSecurityAlertsAPI( + CHAIN_ID_MOCK, + REQUEST_MOCK, + ); + + expect(response).toEqual(RESPONSE_MOCK); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + `https://example.com/validate/${CHAIN_ID_MOCK}`, + expect.any(Object), + ); + }); + + it('throws an error if response is not ok', async () => { + fetchMock.mockResolvedValue({ ok: false, status: 567 }); + + const responsePromise = validateWithSecurityAlertsAPI( + CHAIN_ID_MOCK, + REQUEST_MOCK, + ); + + await expect(responsePromise).rejects.toThrow( + 'Security alerts API request failed with status: 567', + ); + }); + + it('throws an error if SECURITY_ALERTS_API_URL is not set', async () => { + delete process.env.SECURITY_ALERTS_API_URL; + + await expect( + validateWithSecurityAlertsAPI(CHAIN_ID_MOCK, REQUEST_MOCK), + ).rejects.toThrow('Security alerts API URL is not set'); + }); + + it('throws an error if SECURITY_ALERTS_API_ENABLED is false', () => { + process.env.SECURITY_ALERTS_API_ENABLED = 'false'; + + const isEnabled = isSecurityAlertsAPIEnabled(); + expect(isEnabled).toBe(false); + }); + }); + + describe('getSecurityAlertsAPISupportedChainIds', () => { + it('sends GET request', async () => { + const SUPPORTED_CHAIN_IDS_MOCK = ['0x1', '0x2']; + fetchMock.mockResolvedValue({ + ok: true, + json: async () => SUPPORTED_CHAIN_IDS_MOCK, + }); + const response = await getSecurityAlertsAPISupportedChainIds(); + + expect(response).toEqual(SUPPORTED_CHAIN_IDS_MOCK); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + `https://example.com/supportedChains`, + undefined, + ); + }); + + it('throws an error if response is not ok', async () => { + fetchMock.mockResolvedValue({ ok: false, status: 404 }); + + await expect(getSecurityAlertsAPISupportedChainIds()).rejects.toThrow( + 'Security alerts API request failed with status: 404', + ); + }); + }); +}); diff --git a/app/scripts/lib/ppom/security-alerts-api.ts b/app/scripts/lib/ppom/security-alerts-api.ts new file mode 100644 index 000000000000..258526f1b7c4 --- /dev/null +++ b/app/scripts/lib/ppom/security-alerts-api.ts @@ -0,0 +1,65 @@ +import { Hex, JsonRpcRequest } from '@metamask/utils'; +import { SecurityAlertResponse } from './types'; + +const ENDPOINT_VALIDATE = 'validate'; +const ENDPOINT_SUPPORTED_CHAINS = 'supportedChains'; + +type SecurityAlertsAPIRequestBody = { + method: string; + params: unknown[]; +}; + +export type SecurityAlertsAPIRequest = Omit< + JsonRpcRequest, + 'method' | 'params' +> & + SecurityAlertsAPIRequestBody; + +export function isSecurityAlertsAPIEnabled() { + const isEnabled = process.env.SECURITY_ALERTS_API_ENABLED; + return isEnabled?.toString() === 'true'; +} + +export async function validateWithSecurityAlertsAPI( + chainId: string, + body: + | SecurityAlertsAPIRequestBody + | Pick, +): Promise { + const endpoint = `${ENDPOINT_VALIDATE}/${chainId}`; + return request(endpoint, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +export async function getSecurityAlertsAPISupportedChainIds(): Promise { + return request(ENDPOINT_SUPPORTED_CHAINS); +} + +async function request(endpoint: string, options?: RequestInit) { + const url = getUrl(endpoint); + + const response = await fetch(url, options); + + if (!response.ok) { + throw new Error( + `Security alerts API request failed with status: ${response.status}`, + ); + } + + return response.json(); +} + +function getUrl(endpoint: string) { + const host = process.env.SECURITY_ALERTS_API_URL; + + if (!host) { + throw new Error('Security alerts API URL is not set'); + } + + return `${host}/${endpoint}`; +} diff --git a/app/scripts/lib/ppom/types.ts b/app/scripts/lib/ppom/types.ts index 381233141c8a..6188d644aa12 100644 --- a/app/scripts/lib/ppom/types.ts +++ b/app/scripts/lib/ppom/types.ts @@ -1,3 +1,5 @@ +import { SecurityAlertSource } from '../../../../shared/constants/security-provider'; + export type SecurityAlertResponse = { block?: number; description?: string; @@ -6,4 +8,5 @@ export type SecurityAlertResponse = { reason: string; result_type: string; securityAlertId?: string; + source?: SecurityAlertSource; }; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index c9e39c1b8579..571688688611 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -134,11 +134,11 @@ async function addEthereumChainHandler( } else { networkClientId = existingNetwork.id ?? existingNetwork.type; const currentRpcUrl = getCurrentRpcUrl(); - if ( currentChainIdForDomain === chainId && currentRpcUrl === firstValidRPCUrl ) { + res.result = null; return end(); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js index ee8993ceb3fc..db4b967fa468 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.test.js @@ -73,7 +73,7 @@ describe('addEthereumChainHandler', () => { jest.clearAllMocks(); }); - describe('with permittedChains permissioning inactive', () => { + describe('with `endowment:permitted-chains` permissioning inactive', () => { it('creates a new network configuration for the given chainid and switches to it if none exists', async () => { const mocks = makeMocks({ permissionsFeatureFlagIsActive: false, @@ -253,8 +253,8 @@ describe('addEthereumChainHandler', () => { }); }); - describe('with permittedChains permissioning active', () => { - it('creates a new network configuration for the given chainid, requests permittedChains permission and switches to it if no networkConfigurations with the same chainId exist', async () => { + describe('with `endowment:permitted-chains` permissioning active', () => { + it('creates a new network configuration for the given chainid, requests `endowment:permitted-chains` permission and switches to it if no networkConfigurations with the same chainId exist', async () => { const mocks = makeMocks({ permissionedChainIds: [], permissionsFeatureFlagIsActive: true, @@ -297,7 +297,7 @@ describe('addEthereumChainHandler', () => { describe('if a networkConfiguration for the given chainId already exists', () => { describe('if the proposed networkConfiguration has a different rpcUrl from the one already in state', () => { - it('create a new networkConfiguration and switches to it without requesting permissions, if the requested chainId has permittedChains permission granted for requesting origin', async () => { + it('create a new networkConfiguration and switches to it without requesting permissions, if the requested chainId has `endowment:permitted-chains` permission granted for requesting origin', async () => { const mocks = makeMocks({ permissionedChainIds: [CHAIN_IDS.MAINNET], permissionsFeatureFlagIsActive: true, @@ -541,4 +541,45 @@ describe('addEthereumChainHandler', () => { }), ); }); + + it('should add result set to null to response object if the requested rpcUrl (and chainId) is currently selected', async () => { + const CURRENT_RPC_CONFIG = createMockNonInfuraConfiguration(); + + const mocks = makeMocks({ + permissionsFeatureFlagIsActive: false, + overrides: { + getCurrentChainIdForDomain: jest + .fn() + .mockReturnValue(CURRENT_RPC_CONFIG.chainId), + findNetworkConfigurationBy: jest + .fn() + .mockReturnValue(CURRENT_RPC_CONFIG), + getCurrentRpcUrl: jest.fn().mockReturnValue(CURRENT_RPC_CONFIG.rpcUrl), + }, + }); + const res = {}; + + await addEthereumChainHandler( + { + origin: 'example.com', + params: [ + { + chainId: CURRENT_RPC_CONFIG.chainId, + chainName: 'Custom Network', + rpcUrls: [CURRENT_RPC_CONFIG.rpcUrl], + nativeCurrency: { + symbol: CURRENT_RPC_CONFIG.ticker, + decimals: 18, + }, + blockExplorerUrls: ['https://custom.blockexplorer'], + }, + ], + }, + res, + jest.fn(), + jest.fn(), + mocks, + ); + expect(res.result).toBeNull(); + }); }); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js index 8b10cbb9cd6f..27526ed6cc4e 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/ethereum-chain-utils.js @@ -214,10 +214,7 @@ export async function switchChain( permissionedChainIds === undefined || !permissionedChainIds.includes(chainId) ) { - await requestPermittedChainsPermission([ - ...(permissionedChainIds ?? []), - chainId, - ]); + await requestPermittedChainsPermission([chainId]); } } else { await requestUserApproval({ diff --git a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js index 2677425cce12..e49964314b5c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/switch-ethereum-chain.test.js @@ -167,7 +167,7 @@ describe('switchEthereumChainHandler', () => { describe('with permittedChains permissioning active', () => { const permissionsFeatureFlagIsActive = true; - it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in permittedChains', async () => { + it('should call requestPermittedChainsPermission and setActiveNetwork when chainId is not in `endowment:permitted-chains`', async () => { const mockrequestPermittedChainsPermission = jest .fn() .mockResolvedValue(); @@ -200,7 +200,7 @@ describe('switchEthereumChainHandler', () => { ); }); - it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in permittedChains', async () => { + it('should call setActiveNetwork without calling requestPermittedChainsPermission when requested chainId is in `endowment:permitted-chains`', async () => { const mocks = makeMocks({ permissionsFeatureFlagIsActive, permissionedChainIds: [CHAIN_IDS.MAINNET], diff --git a/app/scripts/lib/setup-initial-state-hooks.js b/app/scripts/lib/setup-initial-state-hooks.js index 3cd13a77e29e..d0b689c9cb30 100644 --- a/app/scripts/lib/setup-initial-state-hooks.js +++ b/app/scripts/lib/setup-initial-state-hooks.js @@ -40,10 +40,14 @@ const persistedStateMask = { globalThis.stateHooks.getSentryState = function () { const sentryState = { browser: window.navigator.userAgent, + // we use the manifest.json version from getVersion and not + // `process.env.METAMASK_VERSION` as they can be different (see `getVersion` + // for more info) version: platform.getVersion(), }; // If `getSentryAppState` is set, it implies that initialization has completed if (globalThis.stateHooks.getSentryAppState) { + sentryLocalStore.cleanUpMostRecentRetrievedState(); return { ...sentryState, state: globalThis.stateHooks.getSentryAppState(), diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 47d96c259727..1206047e62d3 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -3,12 +3,11 @@ import { createModuleLogger, createProjectLogger } from '@metamask/utils'; import { logger } from '@sentry/utils'; import browser from 'webextension-polyfill'; import { isManifestV3 } from '../../../shared/modules/mv3.utils'; -import { filterEvents } from './sentry-filter-events'; import extractEthjsErrorMessage from './extractEthjsErrorMessage'; - -let installType = 'unknown'; +import { filterEvents } from './sentry-filter-events'; const projectLogger = createProjectLogger('sentry'); +let installType = 'unknown'; export const log = createModuleLogger( projectLogger, @@ -48,6 +47,18 @@ export default function setupSentry() { log('Initializing'); + // Normally this would be awaited, but getSelf should be available by the time the report is finalized. + // If it's not, we still get the extensionId, but the installType will default to "unknown" + browser.management + .getSelf() + .then((extensionInfo) => { + if (extensionInfo.installType) { + installType = extensionInfo.installType; + } + }) + .catch((error) => { + log('Error getting extension installType', error); + }); integrateLogging(); setSentryClient(); @@ -65,8 +76,8 @@ function getClientOptions() { beforeBreadcrumb: beforeBreadcrumb(), beforeSend: (report) => rewriteReport(report), debug: METAMASK_DEBUG, - dsn: sentryTarget, dist: isManifestV3 ? 'mv3' : 'mv2', + dsn: sentryTarget, environment, integrations: [ Sentry.dedupeIntegration(), @@ -82,7 +93,7 @@ function getClientOptions() { // we can safely turn them off by setting the `sendClientReports` option to // `false`. sendClientReports: false, - tracesSampleRate: 0.01, + tracesSampleRate: METAMASK_DEBUG ? 1.0 : 0.01, transport: makeTransport, }; } @@ -225,19 +236,6 @@ function setSentryClient() { const clientOptions = getClientOptions(); const { dsn, environment, release } = clientOptions; - // Normally this would be awaited, but getSelf should be available by the time the report is finalized. - // If it's not, we still get the extensionId, but the installType will default to "unknown" - browser.management - .getSelf() - .then((extensionInfo) => { - if (extensionInfo.installType) { - installType = extensionInfo.installType; - } - }) - .catch((error) => { - console.log('Error getting extension installType', error); - }); - /** * Sentry throws on initialization as it wants to avoid polluting the global namespace and * potentially clashing with a website also using Sentry, but this could only happen in the content script. diff --git a/app/scripts/lib/snap-keyring/account-watcher-snap.ts b/app/scripts/lib/snap-keyring/account-watcher-snap.ts new file mode 100644 index 000000000000..3775dcd28405 --- /dev/null +++ b/app/scripts/lib/snap-keyring/account-watcher-snap.ts @@ -0,0 +1,8 @@ +import { SnapId } from '@metamask/snaps-sdk'; +import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap.json'; + +export const ACCOUNT_WATCHER_SNAP_ID: SnapId = + AccountWatcherSnap.snapId as SnapId; + +export const ACCOUNT_WATCHER_NAME: string = + AccountWatcherSnap.manifest.proposedName; diff --git a/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts b/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts new file mode 100644 index 000000000000..1ffca624f759 --- /dev/null +++ b/app/scripts/lib/snap-keyring/bitcoin-wallet-snap.ts @@ -0,0 +1,28 @@ +import { SnapId } from '@metamask/snaps-sdk'; +import { Sender } from '@metamask/keyring-api'; +import { HandlerType } from '@metamask/snaps-utils'; +import { Json, JsonRpcRequest } from '@metamask/utils'; +// This dependency is still installed as part of the `package.json`, however +// the Snap is being pre-installed only for Flask build (for the moment). +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; +import { handleSnapRequest } from '../../../../ui/store/actions'; + +export const BITCOIN_WALLET_SNAP_ID: SnapId = + BitcoinWalletSnap.snapId as SnapId; + +export const BITCOIN_WALLET_NAME: string = + BitcoinWalletSnap.manifest.proposedName; + +export class BitcoinWalletSnapSender implements Sender { + send = async (request: JsonRpcRequest): Promise => { + // We assume the caller of this module is aware of this. If we try to use this module + // without having the pre-installed Snap, this will likely throw an error in + // the `handleSnapRequest` action. + return (await handleSnapRequest({ + origin: 'metamask', + snapId: BITCOIN_WALLET_SNAP_ID, + handler: HandlerType.OnKeyringRequest, + request, + })) as Json; + }; +} diff --git a/app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts index bd48d78cd6bb..a04b443759a1 100644 --- a/app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts +++ b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.test.ts @@ -8,6 +8,12 @@ import { keyringSnapPermissionsBuilder, } from './keyring-snaps-permissions'; +const PORTFOLIO_ORIGINS: string[] = [ + 'https://portfolio.metamask.io', + 'https://dev.portfolio.metamask.io', + 'https://ramps-dev.portfolio.metamask.io', +]; + describe('keyringSnapPermissionsBuilder', () => { const mockController = new SubjectMetadataController({ subjectCacheLimit: 100, @@ -25,6 +31,43 @@ describe('keyringSnapPermissionsBuilder', () => { subjectType: SubjectType.Website, }); + describe('Portfolio origin', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each(PORTFOLIO_ORIGINS)( + 'returns the methods that can be called by %s', + (origin: string) => { + const permissions = keyringSnapPermissionsBuilder( + mockController, + origin, + ); + expect(permissions()).toStrictEqual([ + KeyringRpcMethod.ListAccounts, + KeyringRpcMethod.GetAccount, + KeyringRpcMethod.GetAccountBalances, + KeyringRpcMethod.SubmitRequest, + ]); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(PORTFOLIO_ORIGINS)( + '%s cannot create an account', + (origin: string) => { + const permissions = keyringSnapPermissionsBuilder( + mockController, + origin, + ); + expect(permissions()).not.toContain(KeyringRpcMethod.CreateAccount); + }, + ); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each(PORTFOLIO_ORIGINS)('%s can submit a request', (origin: string) => { + const permissions = keyringSnapPermissionsBuilder(mockController, origin); + expect(permissions()).toContain(KeyringRpcMethod.SubmitRequest); + }); + }); + it('returns the methods metamask can call', () => { const permissions = keyringSnapPermissionsBuilder( mockController, @@ -59,7 +102,6 @@ describe('keyringSnapPermissionsBuilder', () => { KeyringRpcMethod.GetRequest, KeyringRpcMethod.ApproveRequest, KeyringRpcMethod.RejectRequest, - KeyringRpcMethod.SubmitRequest, ]); }); diff --git a/app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts index 9ed74912ffd5..f7d9c829a3cc 100644 --- a/app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts +++ b/app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts @@ -4,6 +4,19 @@ import { } from '@metamask/permission-controller'; import { KeyringRpcMethod } from '@metamask/keyring-api'; +/** + * The origins of the Portfolio dapp. + */ +const PORTFOLIO_ORIGINS: string[] = [ + 'https://portfolio.metamask.io', + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + 'https://dev.portfolio.metamask.io', + 'https://stage.portfolio.metamask.io', + 'https://ramps-dev.portfolio.metamask.io', + 'https://portfolio-builds.metafi-dev.codefi.network', + ///: END:ONLY_INCLUDE_IF +]; + /** * List of keyring methods MetaMask can call. */ @@ -20,6 +33,7 @@ const METAMASK_ALLOWED_METHODS: string[] = [ /** * List of keyring methods a dapp can call. + * !NOTE: DO NOT INCLUDE `KeyringRpcMethod.SubmitRequest` IN THIS LIST. */ const WEBSITE_ALLOWED_METHODS: string[] = [ KeyringRpcMethod.ListAccounts, @@ -33,6 +47,15 @@ const WEBSITE_ALLOWED_METHODS: string[] = [ KeyringRpcMethod.GetRequest, KeyringRpcMethod.ApproveRequest, KeyringRpcMethod.RejectRequest, +]; + +/** + * List of keyring methods that Portfolio can call. + */ +const PORTFOLIO_ALLOWED_METHODS: string[] = [ + KeyringRpcMethod.ListAccounts, + KeyringRpcMethod.GetAccount, + KeyringRpcMethod.GetAccountBalances, KeyringRpcMethod.SubmitRequest, ]; @@ -79,6 +102,10 @@ export function keyringSnapPermissionsBuilder( return METAMASK_ALLOWED_METHODS; } + if (PORTFOLIO_ORIGINS.includes(origin)) { + return PORTFOLIO_ALLOWED_METHODS; + } + const originMetadata = controller.getSubjectMetadata(origin); if (originMetadata?.subjectType === SubjectType.Website) { return isProtocolAllowed(origin) ? WEBSITE_ALLOWED_METHODS : []; diff --git a/app/scripts/lib/snap-keyring/metrics.test.ts b/app/scripts/lib/snap-keyring/metrics.test.ts index fe49b2be73e4..f9c71792f63c 100644 --- a/app/scripts/lib/snap-keyring/metrics.test.ts +++ b/app/scripts/lib/snap-keyring/metrics.test.ts @@ -45,7 +45,6 @@ describe('getSnapAndHardwareInfoForMetrics', () => { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -97,7 +96,6 @@ describe('getSnapAndHardwareInfoForMetrics', () => { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', diff --git a/app/scripts/lib/snap-keyring/snap-keyring.test.ts b/app/scripts/lib/snap-keyring/snap-keyring.test.ts new file mode 100644 index 000000000000..4136fd1fd1fc --- /dev/null +++ b/app/scripts/lib/snap-keyring/snap-keyring.test.ts @@ -0,0 +1,560 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import { EthAccountType, InternalAccount } from '@metamask/keyring-api'; +import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../shared/constants/app'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { + showAccountCreationDialog, + showAccountNameSuggestionDialog, + snapKeyringBuilder, +} from './snap-keyring'; +import { + SnapKeyringBuilderAllowActions, + SnapKeyringBuilderMessenger, +} from './types'; + +const mockAddRequest = jest.fn(); +const mockStartFlow = jest.fn(); +const mockEndFlow = jest.fn(); +const mockShowSuccess = jest.fn(); +const mockShowError = jest.fn(); +const mockGetAccounts = jest.fn(); +const mockSnapId = 'snapId'; +const mockSnapName = 'mock-snap'; +const mockSnapController = jest.fn(); +const mockPersisKeyringHelper = jest.fn(); +const mockSetSelectedAccount = jest.fn(); +const mockSetAccountName = jest.fn(); +const mockRemoveAccountHelper = jest.fn(); +const mockTrackEvent = jest.fn(); +const mockGetAccountByAddress = jest.fn(); + +const mockFlowId = '123'; +const address = '0x2a4d4b667D5f12C3F9Bf8F14a7B9f8D8d9b8c8fA'; +const accountNameSuggestion = 'Suggested Account Name'; +const mockAccount = { + type: EthAccountType.Eoa, + id: '3afa663e-0600-4d93-868a-61c2e553013b', + address, + methods: [], + options: {}, +}; +const mockInternalAccount = { + ...mockAccount, + metadata: { + snap: { + enabled: true, + id: mockSnapId, + name: mockSnapName, + }, + name: accountNameSuggestion, + keyring: { + type: '', + }, + importTime: 0, + }, +}; + +const createControllerMessenger = ({ + account = mockInternalAccount, +}: { + account?: InternalAccount; +} = {}): SnapKeyringBuilderMessenger => { + const messenger = new ControllerMessenger< + SnapKeyringBuilderAllowActions, + never + >().getRestricted({ + name: 'SnapKeyringBuilder', + allowedActions: [ + 'ApprovalController:addRequest', + 'ApprovalController:acceptRequest', + 'ApprovalController:rejectRequest', + 'ApprovalController:startFlow', + 'ApprovalController:endFlow', + 'ApprovalController:showSuccess', + 'ApprovalController:showError', + 'PhishingController:maybeUpdateState', + 'KeyringController:getAccounts', + 'AccountsController:setSelectedAccount', + 'AccountsController:getAccountByAddress', + ], + allowedEvents: [], + }); + + jest.spyOn(messenger, 'call').mockImplementation((...args) => { + // This mock implementation does not have a nice discriminate union where types/parameters can be correctly inferred + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [actionType, ...params]: any[] = args; + + switch (actionType) { + case 'ApprovalController:addRequest': + return mockAddRequest(params); + + case 'ApprovalController:startFlow': + return mockStartFlow.mockReturnValue({ id: mockFlowId })(); + + case 'ApprovalController:endFlow': + return mockEndFlow.mockReturnValue(true)(params); + + case 'ApprovalController:showSuccess': + return mockShowSuccess(); + + case 'ApprovalController:showError': + return mockShowError(); + + case 'KeyringController:getAccounts': + return mockGetAccounts.mockResolvedValue([])(); + + case 'AccountsController:getAccountByAddress': + return mockGetAccountByAddress.mockReturnValue(account)(params); + + case 'AccountsController:setSelectedAccount': + return mockSetSelectedAccount(params); + + case 'AccountsController:setAccountName': + return mockSetAccountName.mockReturnValue(null)(params); + + default: + throw new Error( + `MOCK_FAIL - unsupported messenger call: ${actionType}`, + ); + } + }); + + return messenger; +}; + +const createSnapKeyringBuilder = ({ + snapName = mockSnapName, + isSnapPreinstalled = true, +}: { + snapName?: string; + isSnapPreinstalled?: boolean; +} = {}) => { + return snapKeyringBuilder( + createControllerMessenger(), + mockSnapController, + mockPersisKeyringHelper, + mockRemoveAccountHelper, + mockTrackEvent, + () => snapName, + () => isSnapPreinstalled, + ); +}; + +describe('Snap Keyring Methods', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('helpers', () => { + describe('showAccountCreationDialog', () => { + it('shows account creation dialog and return true on user confirmation', async () => { + const controllerMessenger = createControllerMessenger(); + controllerMessenger.call('ApprovalController:startFlow'); + + await showAccountCreationDialog(mockSnapId, controllerMessenger); + + expect(mockAddRequest).toHaveBeenCalledTimes(1); + expect(mockAddRequest).toHaveBeenCalledWith([ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, + }, + true, + ]); + }); + }); + + describe('showAccountNameSuggestionDialog', () => { + it('shows account name suggestion dialog and return true on user confirmation', async () => { + const controllerMessenger = createControllerMessenger(); + controllerMessenger.call('ApprovalController:startFlow'); + + await showAccountNameSuggestionDialog( + mockSnapId, + controllerMessenger, + accountNameSuggestion, + ); + + expect(mockAddRequest).toHaveBeenCalledTimes(1); + expect(mockAddRequest).toHaveBeenCalledWith([ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: accountNameSuggestion, + }, + }, + true, + ]); + }); + }); + }); + + describe('addAccount', () => { + beforeEach(() => { + mockAddRequest.mockReturnValue(true).mockReturnValue({ success: true }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + + it('handles account creation with confirmations and without a user defined name', async () => { + const builder = createSnapKeyringBuilder(); + await builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: true, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + // First request for show account creation dialog + // Second request for account name suggestion dialog + expect(mockAddRequest).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, + }, + true, + ]); + // First call is from addAccount after user confirmation + // Second call is from within the SnapKeyring after ending the addAccount flow + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(2, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: '', + }, + }, + true, + ]); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockTrackEvent).toHaveBeenCalledTimes(3); + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessViewed, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(2, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessClicked, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(3, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockShowSuccess).toHaveBeenCalledTimes(1); + expect(mockSetAccountName).not.toHaveBeenCalled(); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('handles account creation with skipping confirmation and without user defined name', async () => { + const builder = createSnapKeyringBuilder(); + + await builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: false, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + expect(mockAddRequest).toHaveBeenCalledTimes(1); + // First call is from addAccount after user confirmation + // Second call is from within the SnapKeyring after ending the addAccount flow + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + // No user defined name + snapSuggestedAccountName: '', + }, + }, + true, + ]); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockSetAccountName).not.toHaveBeenCalled(); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('handles account creation with confirmations and with a user defined name', async () => { + const mockNameSuggestion = 'new name'; + mockAddRequest.mockReturnValueOnce(true).mockReturnValueOnce({ + success: true, + name: mockNameSuggestion, + }); + const builder = createSnapKeyringBuilder(); + await builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: true, + accountNameSuggestion: mockNameSuggestion, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + // First request for show account creation dialog + // Second request for account name suggestion second + expect(mockAddRequest).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, + }, + true, + ]); + // First call is from addAccount after user confirmation + // Second call is from within the SnapKeyring + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(2, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: mockNameSuggestion, + }, + }, + true, + ]); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockTrackEvent).toHaveBeenCalledTimes(3); + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessViewed, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(2, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessClicked, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(3, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockSetAccountName).toHaveBeenCalledWith([ + mockAccount.id, + mockNameSuggestion, + ]); + expect(mockShowSuccess).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('handles account creation with skipping confirmation and with user defined name', async () => { + const mockNameSuggestion = 'suggested name'; + mockAddRequest.mockReturnValueOnce({ + success: true, + name: mockNameSuggestion, + }); + const builder = createSnapKeyringBuilder(); + await builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: false, + accountNameSuggestion: mockNameSuggestion, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + expect(mockAddRequest).toHaveBeenCalledTimes(1); + // First call is from addAccount after user confirmation + // Second call is from within the SnapKeyring after ending the addAccount flow + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: mockNameSuggestion, + }, + }, + true, + ]); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockSetAccountName).toHaveBeenCalledTimes(1); + expect(mockSetAccountName).toHaveBeenCalledWith([ + mockAccount.id, + mockNameSuggestion, + ]); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('handles account creation with confirmations and with a user defined name', async () => { + const mockNameSuggestion = 'new name'; + mockAddRequest.mockReturnValueOnce(true).mockReturnValueOnce({ + success: true, + name: mockNameSuggestion, + }); + const builder = createSnapKeyringBuilder(); + await builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: true, + accountNameSuggestion: mockNameSuggestion, + }, + }); + + expect(mockStartFlow).toHaveBeenCalledTimes(1); + // First request for show account creation dialog + // Second request for account name suggestion second + expect(mockAddRequest).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(1, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, + }, + true, + ]); + // First call is from addAccount after user confirmation + // Second call is from within the SnapKeyring + expect(mockPersisKeyringHelper).toHaveBeenCalledTimes(2); + expect(mockAddRequest).toHaveBeenNthCalledWith(2, [ + { + origin: mockSnapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: mockNameSuggestion, + }, + }, + true, + ]); + expect(mockGetAccountByAddress).toHaveBeenCalledTimes(1); + expect(mockGetAccountByAddress).toHaveBeenCalledWith([ + mockAccount.address.toLowerCase(), + ]); + expect(mockTrackEvent).toHaveBeenCalledTimes(3); + expect(mockTrackEvent).toHaveBeenNthCalledWith(1, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessViewed, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(2, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AddSnapAccountSuccessClicked, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockTrackEvent).toHaveBeenNthCalledWith(3, { + category: MetaMetricsEventCategory.Accounts, + event: MetaMetricsEventName.AccountAdded, + properties: { + account_type: 'snap', + snap_id: mockSnapId, + snap_name: mockSnapName, + }, + }); + expect(mockSetAccountName).toHaveBeenCalledTimes(1); + expect(mockSetAccountName).toHaveBeenCalledWith([ + mockAccount.id, + mockNameSuggestion, + ]); + expect(mockShowSuccess).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + + it('ends approval flow on error', async () => { + const errorMessage = 'save error'; + mockPersisKeyringHelper.mockRejectedValue(new Error(errorMessage)); + const builder = createSnapKeyringBuilder(); + await expect( + builder().handleKeyringSnapMessage(mockSnapId, { + method: 'notify:accountCreated', + params: { + account: mockAccount, + displayConfirmation: true, + }, + }), + ).rejects.toThrow( + `Error occurred while creating snap account: ${errorMessage}`, + ); + expect(mockStartFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledTimes(1); + expect(mockEndFlow).toHaveBeenCalledWith([{ id: mockFlowId }]); + }); + }); +}); diff --git a/app/scripts/lib/snap-keyring/snap-keyring.ts b/app/scripts/lib/snap-keyring/snap-keyring.ts index 1ce13ef07fe7..d707b0bb61c4 100644 --- a/app/scripts/lib/snap-keyring/snap-keyring.ts +++ b/app/scripts/lib/snap-keyring/snap-keyring.ts @@ -30,13 +30,74 @@ export const getAccountsBySnapId = async ( return await snapKeyring.getAccountsBySnapId(snapId); }; +/** + * Show the account creation dialog for a given Snap. + * This function will start the approval flow, show the account creation dialog, and end the flow. + * + * @param snapId - Snap ID to show the account creation dialog for. + * @param controllerMessenger - The controller messenger instance. + * @returns The user's confirmation result. + */ +export async function showAccountCreationDialog( + snapId: string, + controllerMessenger: SnapKeyringBuilderMessenger, +) { + try { + const confirmationResult = Boolean( + await controllerMessenger.call( + 'ApprovalController:addRequest', + { + origin: snapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, + }, + true, + ), + ); + return confirmationResult; + } catch (e) { + throw new Error( + `Error occurred while showing account creation dialog.\n${e}`, + ); + } +} + +/** + * Show the account name suggestion confirmation dialog for a given Snap. + * + * @param snapId - Snap ID to show the account name suggestion dialog for. + * @param controllerMessenger - The controller messenger instance. + * @param accountNameSuggestion - Suggested name for the new account. + * @returns The user's confirmation result. + */ +export async function showAccountNameSuggestionDialog( + snapId: string, + controllerMessenger: SnapKeyringBuilderMessenger, + accountNameSuggestion: string, +): Promise<{ success: boolean; name?: string }> { + try { + const confirmationResult = (await controllerMessenger.call( + 'ApprovalController:addRequest', + { + origin: snapId, + type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.showNameSnapAccount, + requestData: { + snapSuggestedAccountName: accountNameSuggestion, + }, + }, + true, + )) as { success: boolean; name?: string }; + return confirmationResult; + } catch (e) { + throw new Error(`Error occurred while showing name account dialog.\n${e}`); + } +} + /** * Constructs a SnapKeyring builder with specified handlers for managing snap accounts. * * @param controllerMessenger - The controller messenger instance. * @param getSnapController - A function that retrieves the Snap Controller instance. * @param persistKeyringHelper - A function that persists all keyrings in the vault. - * @param setSelectedAccountHelper - A function to update current selected account. * @param removeAccountHelper - A function to help remove an account based on its address. * @param trackEvent - A function to track MetaMetrics events. * @param getSnapName - A function to get a snap's localized @@ -51,7 +112,6 @@ export const snapKeyringBuilder = ( controllerMessenger: SnapKeyringBuilderMessenger, getSnapController: () => SnapController, persistKeyringHelper: () => Promise, - setSelectedAccountHelper: (address: string) => void, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any removeAccountHelper: (address: string) => Promise, @@ -122,10 +182,13 @@ export const snapKeyringBuilder = ( address: string, snapId: string, handleUserInput: (accepted: boolean) => Promise, - _accountNameSuggestion?: string, + accountNameSuggestion: string = '', displayConfirmation: boolean = false, ) => { const snapName = getSnapName(snapId); + const { id: addAccountFlowId } = controllerMessenger.call( + 'ApprovalController:startFlow', + ); const trackSnapAccountEvent = (event: MetaMetricsEventName) => { trackEvent({ @@ -139,60 +202,67 @@ export const snapKeyringBuilder = ( }); }; - const learnMoreLink = - 'https://support.metamask.io/managing-my-wallet/accounts-and-addresses/how-to-add-accounts-in-your-wallet/'; - - // If snap is preinstalled and does not request confirmation, skip the confirmation dialog - const skipConfirmation = - isSnapPreinstalled(snapId) && !displayConfirmation; - // If confirmation dialog is skipped, we consider the account creation to be confirmed - let confirmationResult = skipConfirmation; - let confirmationApprovalId = ''; try { - if (!skipConfirmation) { - const { id } = controllerMessenger.call( - 'ApprovalController:startFlow', - ); - confirmationApprovalId = id; - confirmationResult = Boolean( - await controllerMessenger.call( - 'ApprovalController:addRequest', - { - origin: snapId, - type: SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES.confirmAccountCreation, - }, - true, - ), - ); + const learnMoreLink = + 'https://support.metamask.io/managing-my-wallet/accounts-and-addresses/how-to-add-accounts-in-your-wallet/'; + + // If snap is preinstalled and does not request confirmation, skip the confirmation dialog + const skipConfirmation = + isSnapPreinstalled(snapId) && !displayConfirmation; + // If confirmation dialog are skipped, we consider the account creation to be confirmed until the account name dialog is closed + const accountCreationConfirmationResult = + skipConfirmation || + (await showAccountCreationDialog(snapId, controllerMessenger)); + + if (!accountCreationConfirmationResult) { + // User has cancelled account creation + await handleUserInput(accountCreationConfirmationResult); + + throw new Error('User denied account creation'); } - if (confirmationResult) { + const accountNameConfirmationResult = + await showAccountNameSuggestionDialog( + snapId, + controllerMessenger, + accountNameSuggestion, + ); + + if (accountNameConfirmationResult?.success) { try { - await handleUserInput(confirmationResult); + // Persist the account so we can rename it afterward await persistKeyringHelper(); - setSelectedAccountHelper(address); - const internalAccount = controllerMessenger.call( + await handleUserInput(accountNameConfirmationResult.success); + const account = controllerMessenger.call( 'AccountsController:getAccountByAddress', address, ); - if (!internalAccount) { + if (!account) { throw new Error( `Internal account not found for address: ${address}`, ); } + // Set the selected account to the new account controllerMessenger.call( 'AccountsController:setSelectedAccount', - internalAccount.id, + account.id, ); - // TODO: Add events tracking to the dialog itself, so that events are more - // "linked" to UI actions - // User should now see the "Successfuly added account" page - trackSnapAccountEvent( - MetaMetricsEventName.AddSnapAccountSuccessViewed, - ); + if (accountNameConfirmationResult.name) { + controllerMessenger.call( + 'AccountsController:setAccountName', + account.id, + accountNameConfirmationResult.name, + ); + } if (!skipConfirmation) { + // TODO: Add events tracking to the dialog itself, so that events are more + // "linked" to UI actions + // User should now see the "Successfuly added account" page + trackSnapAccountEvent( + MetaMetricsEventName.AddSnapAccountSuccessViewed, + ); await showSuccess( controllerMessenger, snapId, @@ -206,13 +276,15 @@ export const snapKeyringBuilder = ( learnMoreLink, }, ); + // User has clicked on "OK" + trackSnapAccountEvent( + MetaMetricsEventName.AddSnapAccountSuccessClicked, + ); } - // User has clicked on "OK" - trackSnapAccountEvent( - MetaMetricsEventName.AddSnapAccountSuccessClicked, - ); + trackSnapAccountEvent(MetaMetricsEventName.AccountAdded); } catch (e) { + // Error occurred while naming the account const error = (e as Error).message; await showError( @@ -232,30 +304,20 @@ export const snapKeyringBuilder = ( }, ); - trackSnapAccountEvent(MetaMetricsEventName.AccountAddFailed); - throw new Error( `Error occurred while creating snap account: ${error}`, ); } } else { - // User has cancelled account creation - await handleUserInput(confirmationResult); + // User has cancelled account creation so remove the account from the keyring + await handleUserInput(accountNameConfirmationResult?.success); throw new Error('User denied account creation'); } } finally { - // We do not have a `else` clause here, as it's used if the request was - // canceled by the user, thus it's not a "fail" (not an error). - if (confirmationResult) { - trackSnapAccountEvent(MetaMetricsEventName.AccountAdded); - } - // End the approval flow if it was started - if (!skipConfirmation) { - controllerMessenger.call('ApprovalController:endFlow', { - id: confirmationApprovalId, - }); - } + controllerMessenger.call('ApprovalController:endFlow', { + id: addAccountFlowId, + }); } }, removeAccount: async ( diff --git a/app/scripts/lib/snap-keyring/types.ts b/app/scripts/lib/snap-keyring/types.ts index 5f6f210ab945..701198d1d503 100644 --- a/app/scripts/lib/snap-keyring/types.ts +++ b/app/scripts/lib/snap-keyring/types.ts @@ -4,6 +4,7 @@ import type { KeyringControllerGetAccountsAction } from '@metamask/keyring-contr import { GetSubjectMetadata } from '@metamask/permission-controller'; import { AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, AccountsControllerSetSelectedAccountAction, } from '@metamask/accounts-controller'; import type { @@ -16,7 +17,7 @@ import type { StartFlow, } from '@metamask/approval-controller'; -type SnapKeyringBuilderAllowActions = +export type SnapKeyringBuilderAllowActions = | StartFlow | EndFlow | ShowSuccess @@ -29,7 +30,8 @@ type SnapKeyringBuilderAllowActions = | KeyringControllerGetAccountsAction | GetSubjectMetadata | AccountsControllerSetSelectedAccountAction - | AccountsControllerGetAccountByAddressAction; + | AccountsControllerGetAccountByAddressAction + | AccountsControllerSetAccountNameAction; export type SnapKeyringBuilderMessenger = RestrictedControllerMessenger< 'SnapKeyringBuilder', diff --git a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts index b40e52afe170..f2216ff9694a 100644 --- a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts +++ b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts @@ -10,17 +10,24 @@ describe('isBlockedUrl', () => { allowedEvents: [], }); const phishingController = new PhishingController({ + // @ts-expect-error TODO: Resolve/patch mismatch between messenger types messenger: phishingControllerMessenger, state: { phishingLists: [ { - blocklist: ['https://metamask.test'], + blocklist: [ + 'metamask.test', + 'QmYwAPJzv5CZsnAzt8auVTL6aKqgfZY5vHBYdbyz4ySxTm', + 'ipfs://QmXbVAkGZMz6p8nJ3wXBng4JwvBqZWkFwnDMevL7Tz5w8y', + 'QmT78zSuBmuS4z925WJg3vNLRiT4Mj6apb5iS4iykKs4n8', + ], allowlist: [], fuzzylist: [], tolerance: 0, version: 1, lastUpdated: 0, name: ListNames.MetaMask, + c2DomainBlocklist: [], }, ], }, @@ -32,6 +39,16 @@ describe('isBlockedUrl', () => { ['https://metamask.io', false], ['https://metamask.test', true], ['sftp://metamask.io', true], + ['ipfs://QmYwAPJzv5CZsnAzt8auVTL6aKqgfZY5vHBYdbyz4ySxTm', true], + ['ipfs://QmXbVAkGZMz6p8nJ3wXBng4JwvBqZWkFgnDMevL7Tz5w8y', true], + [ + 'https://ipfs.io/ipfs/QmT78zSuBmuS4z925WJg3vNLRiT4Mj6apb5iS4iykKs4n8', + true, + ], + [ + 'https://ipfs.io/ipfs/QmT78zSuBmuS4zdsf925WJsadfsdfg3vNLRiT4Mj6apb5iS4iykKs4n8', + false, + ], ['', true], ['1', true], [undefined, true], diff --git a/app/scripts/lib/transaction/decode/four-byte.test.ts b/app/scripts/lib/transaction/decode/four-byte.test.ts new file mode 100644 index 000000000000..da34598d6cdb --- /dev/null +++ b/app/scripts/lib/transaction/decode/four-byte.test.ts @@ -0,0 +1,300 @@ +import { + FOUR_BYTE_RESPONSE, + FOUR_BYTE_RESPONSE_NESTED, + TRANSACTION_DATA_FOUR_BYTE, + TRANSACTION_DATA_SOURCIFY_NESTED, +} from '../../../../../test/data/confirmations/transaction-decode'; +import { decodeTransactionDataWithFourByte } from './four-byte'; + +describe('Four Byte', () => { + const fetchMock = jest.fn(); + + beforeEach(() => { + jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + + describe('decodeTransactionDataWithFourByte', () => { + it('returns expected data', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => FOUR_BYTE_RESPONSE, + }); + + const result = await decodeTransactionDataWithFourByte( + TRANSACTION_DATA_FOUR_BYTE, + ); + + expect(result).toMatchInlineSnapshot(` + { + "name": "someOtherFunction", + "params": [ + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0xec8507EcF7e946992294F06423A79835a3226846", + }, + { + "children": undefined, + "name": undefined, + "type": "uint256", + "value": { + "hex": "0x64", + "type": "BigNumber", + }, + }, + ], + } + `); + }); + + it('returns expected data with tuples and arrays', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => FOUR_BYTE_RESPONSE_NESTED, + }); + + const result = await decodeTransactionDataWithFourByte( + TRANSACTION_DATA_SOURCIFY_NESTED, + ); + + expect(result).toMatchInlineSnapshot(` + { + "name": "permit", + "params": [ + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0xBe3be93fFAD7d417C08124B43286f4476C006AFe", + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + }, + { + "children": undefined, + "name": undefined, + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 0, + }, + ], + "name": "Item 1", + "type": "tuple", + "value": [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + { + "children": [ + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + }, + { + "children": undefined, + "name": undefined, + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 0, + }, + ], + "name": "Item 2", + "type": "tuple", + "value": [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + { + "children": [ + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + }, + { + "children": undefined, + "name": undefined, + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "name": undefined, + "type": "uint48", + "value": 0, + }, + ], + "name": "Item 3", + "type": "tuple", + "value": [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + ], + "name": undefined, + "type": "tuple[]", + "value": [ + [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + ], + }, + { + "children": undefined, + "name": undefined, + "type": "address", + "value": "0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a", + }, + { + "children": undefined, + "name": undefined, + "type": "uint256", + "value": { + "hex": "0x019747f66fc3", + "type": "BigNumber", + }, + }, + ], + "name": undefined, + "type": "tuple", + "value": [ + [ + [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + ], + "0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a", + { + "hex": "0x019747f66fc3", + "type": "BigNumber", + }, + ], + }, + { + "children": undefined, + "name": undefined, + "type": "bytes", + "value": "0x56e1fabfaf96d309b0039896d1f68b51e27f0e25b4481db8cad059b7e8db95d918bdc43ab1d03a0b6a84c7ea2219bf01133ceab4e7ca4e38055e4ed8af78a63b1b", + }, + ], + } + `); + }); + }); +}); diff --git a/app/scripts/lib/transaction/decode/four-byte.ts b/app/scripts/lib/transaction/decode/four-byte.ts new file mode 100644 index 000000000000..5861172fb60d --- /dev/null +++ b/app/scripts/lib/transaction/decode/four-byte.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { addHexPrefix } from 'ethereumjs-util'; +import { Interface, ParamType } from '@ethersproject/abi'; +import { Hex, createProjectLogger } from '@metamask/utils'; +import { + DecodedTransactionDataMethod, + DecodedTransactionDataParam, +} from '../../../../../shared/types/transaction-decode'; +import { getMethodFrom4Byte } from '../../../../../shared/lib/four-byte'; + +const log = createProjectLogger('four-byte'); + +export async function decodeTransactionDataWithFourByte( + transactionData: string, +): Promise { + const fourBytePrefix = transactionData.slice(0, 10); + + const signature = (await getMethodFrom4Byte(fourBytePrefix)) as Hex; + + if (!signature) { + return undefined; + } + + const name = signature.split('(')[0]; + const inputs = parseSignature(signature); + + log('Generated inputs', inputs); + + const valueData = addHexPrefix(transactionData.slice(10)); + const values = Interface.getAbiCoder().decode(inputs, valueData) as any[]; + + const params = inputs.map((input, index) => + decodeParam(input, index, values), + ); + + return { name, params }; +} + +function decodeParam( + input: ParamType, + index: number, + values: any[], +): DecodedTransactionDataParam { + const value = values[index] as any[]; + const { type, name } = input; + + let children = input.components?.map((child, childIndex) => + decodeParam(child, childIndex, value), + ); + + if (type.endsWith('[]')) { + const childType = type.slice(0, -2); + + children = value.map((_arrayItem, arrayIndex) => { + const childName = `Item ${arrayIndex + 1}`; + + return decodeParam( + { ...input, name: childName, type: childType } as ParamType, + arrayIndex, + value, + ); + }); + } + + return { + name, + type, + value, + children, + }; +} + +function parseSignature(signature: string): ParamType[] { + let typeString = signature.slice(signature.indexOf('(') + 1, -1); + const nested = []; + + while (typeString.includes('(')) { + const nestedBrackets = findFirstNestedBrackets(typeString); + + if (!nestedBrackets) { + break; + } + + nested.push(nestedBrackets.value); + + typeString = `${typeString.slice(0, nestedBrackets.start)}${ + nested.length - 1 + }#${typeString.slice(nestedBrackets.end + 1)}`; + } + + return createInput(typeString, nested); +} + +function createInput(typeString: string, nested: string[]): ParamType[] { + return typeString.split(',').map((value) => { + const parts = value.split('#'); + + const nestedIndex = parts.length > 1 ? parseInt(parts[0], 10) : undefined; + const type = nestedIndex === undefined ? value : `tuple${parts[1] ?? ''}`; + + const components = + nestedIndex === undefined + ? undefined + : createInput(nested[nestedIndex], nested); + + return { + type, + components, + } as ParamType; + }); +} + +function findFirstNestedBrackets( + value: string, +): { start: number; end: number; value: string } | undefined { + let start = -1; + + for (let i = 0; i < value.length; i++) { + if (value[i] === '(') { + start = i; + } else if (value[i] === ')' && start !== -1) { + return { + start, + end: i, + value: value.slice(start + 1, i), + }; + } + } + + return undefined; +} diff --git a/app/scripts/lib/transaction/decode/proxy.test.ts b/app/scripts/lib/transaction/decode/proxy.test.ts new file mode 100644 index 000000000000..d137fe567c73 --- /dev/null +++ b/app/scripts/lib/transaction/decode/proxy.test.ts @@ -0,0 +1,45 @@ +import EthQuery from '@metamask/eth-query'; +import { getContractProxyAddress } from './proxy'; + +const CONTRACT_ADDRESS_MOCK = '0x456'; + +function createEthQueryMock(storageValues: string[]): EthQuery { + const ethQuery = { + eth_getStorageAt: jest.fn(), + }; + + for (const storageValue of storageValues) { + ethQuery.eth_getStorageAt.mockImplementationOnce( + (_contractAddress, _storageSlot, _blockNumber, cb) => + cb(null, storageValue), + ); + } + + return ethQuery as unknown as EthQuery; +} + +describe('Proxy', () => { + describe('getContractProxyAddress', () => { + it('returns undefined if all responses empty', async () => { + const ethQuery = createEthQueryMock([ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ]); + + expect( + await getContractProxyAddress(CONTRACT_ADDRESS_MOCK, ethQuery), + ).toBeUndefined(); + }); + + it('returns first non-empty response', async () => { + const ethQuery = createEthQueryMock([ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '00000000000000000000000000123', + ]); + + expect( + await getContractProxyAddress(CONTRACT_ADDRESS_MOCK, ethQuery), + ).toBe('0x123'); + }); + }); +}); diff --git a/app/scripts/lib/transaction/decode/proxy.ts b/app/scripts/lib/transaction/decode/proxy.ts new file mode 100644 index 000000000000..19d5887acd13 --- /dev/null +++ b/app/scripts/lib/transaction/decode/proxy.ts @@ -0,0 +1,36 @@ +import { query } from '@metamask/controller-utils'; +import EthQuery from '@metamask/eth-query'; +import { Hex } from '@metamask/utils'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; + +const IMPLEMENTATION_STORAGE_SLOTS = [ + // org.zeppelinos.proxy.implementation + '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3', + + // eip1967.proxy.implementation + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc', +]; + +const EMPTY_RESULT = '0'.padEnd(64, '0'); + +export async function getContractProxyAddress( + contractAddress: Hex, + ethQuery: EthQuery, +): Promise { + const responses = await Promise.all( + IMPLEMENTATION_STORAGE_SLOTS.map( + (storageSlot) => + query(ethQuery, 'eth_getStorageAt', [ + contractAddress, + storageSlot, + 'latest', + ]) as Promise, + ), + ); + + const result = responses.find( + (response) => stripHexPrefix(response) !== EMPTY_RESULT, + ); + + return result && (addHexPrefix(result.slice(26)) as Hex | undefined); +} diff --git a/app/scripts/lib/transaction/decode/sourcify.test.ts b/app/scripts/lib/transaction/decode/sourcify.test.ts new file mode 100644 index 000000000000..301e42012047 --- /dev/null +++ b/app/scripts/lib/transaction/decode/sourcify.test.ts @@ -0,0 +1,332 @@ +import { + SOURCIFY_RESPONSE, + SOURCIFY_RESPONSE_NESTED, + TRANSACTION_DATA_SOURCIFY, + TRANSACTION_DATA_SOURCIFY_NESTED, +} from '../../../../../test/data/confirmations/transaction-decode'; +import { decodeTransactionDataWithSourcify } from './sourcify'; + +const CONTRACT_ADDRESS_MOCK = '0x456'; +const CHAIN_ID_MOCK = '0x123'; + +describe('Sourcify', () => { + const fetchMock = jest.fn(); + + beforeEach(() => { + jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + }); + + describe('decodeTransactionDataWithSourcify', () => { + it('returns expected data', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => SOURCIFY_RESPONSE, + }); + + const result = await decodeTransactionDataWithSourcify( + TRANSACTION_DATA_SOURCIFY, + CONTRACT_ADDRESS_MOCK, + CHAIN_ID_MOCK, + ); + + expect(result).toMatchInlineSnapshot(` + { + "description": "Transfer tokens", + "name": "transfer", + "params": [ + { + "children": undefined, + "description": "The address to transfer to", + "name": "to", + "type": "address", + "value": "0xec8507EcF7e946992294F06423A79835a3226846", + }, + { + "children": undefined, + "description": "The amount to transfer", + "name": "value", + "type": "uint256", + "value": { + "hex": "0x64", + "type": "BigNumber", + }, + }, + ], + } + `); + }); + + it('returns expected data with tuples and arrays', async () => { + fetchMock.mockResolvedValue({ + ok: true, + json: async () => SOURCIFY_RESPONSE_NESTED, + }); + + const result = await decodeTransactionDataWithSourcify( + TRANSACTION_DATA_SOURCIFY_NESTED, + CONTRACT_ADDRESS_MOCK, + CHAIN_ID_MOCK, + ); + + expect(result).toMatchInlineSnapshot(` + { + "description": "Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature", + "name": "permit", + "params": [ + { + "children": undefined, + "description": "The owner of the tokens being approved", + "name": "owner", + "type": "address", + "value": "0xBe3be93fFAD7d417C08124B43286f4476C006AFe", + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": undefined, + "description": undefined, + "name": "token", + "type": "address", + "value": "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + }, + { + "children": undefined, + "description": undefined, + "name": "amount", + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "description": undefined, + "name": "expiration", + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "description": undefined, + "name": "nonce", + "type": "uint48", + "value": 0, + }, + ], + "description": undefined, + "name": "Item 1", + "type": "tuple", + "value": [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + { + "children": [ + { + "children": undefined, + "description": undefined, + "name": "token", + "type": "address", + "value": "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + }, + { + "children": undefined, + "description": undefined, + "name": "amount", + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "description": undefined, + "name": "expiration", + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "description": undefined, + "name": "nonce", + "type": "uint48", + "value": 0, + }, + ], + "description": undefined, + "name": "Item 2", + "type": "tuple", + "value": [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + { + "children": [ + { + "children": undefined, + "description": undefined, + "name": "token", + "type": "address", + "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + }, + { + "children": undefined, + "description": undefined, + "name": "amount", + "type": "uint160", + "value": { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + }, + { + "children": undefined, + "description": undefined, + "name": "expiration", + "type": "uint48", + "value": 1749259022275, + }, + { + "children": undefined, + "description": undefined, + "name": "nonce", + "type": "uint48", + "value": 0, + }, + ], + "description": undefined, + "name": "Item 3", + "type": "tuple", + "value": [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + }, + ], + "description": undefined, + "name": "details", + "type": "tuple[]", + "value": [ + [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + ], + }, + { + "children": undefined, + "description": undefined, + "name": "spender", + "type": "address", + "value": "0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a", + }, + { + "children": undefined, + "description": undefined, + "name": "sigDeadline", + "type": "uint256", + "value": { + "hex": "0x019747f66fc3", + "type": "BigNumber", + }, + }, + ], + "description": "Data signed over by the owner specifying the terms of approval", + "name": "permitBatch", + "type": "tuple", + "value": [ + [ + [ + "0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0x4385328cc4D643Ca98DfEA734360C0F596C83449", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + [ + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + { + "hex": "0xffffffffffffffffffffffffffffffffffffffff", + "type": "BigNumber", + }, + 1749259022275, + 0, + ], + ], + "0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a", + { + "hex": "0x019747f66fc3", + "type": "BigNumber", + }, + ], + }, + { + "children": undefined, + "description": "The owner's signature over the permit data", + "name": "signature", + "type": "bytes", + "value": "0x56e1fabfaf96d309b0039896d1f68b51e27f0e25b4481db8cad059b7e8db95d918bdc43ab1d03a0b6a84c7ea2219bf01133ceab4e7ca4e38055e4ed8af78a63b1b", + }, + ], + } + `); + }); + }); +}); diff --git a/app/scripts/lib/transaction/decode/sourcify.ts b/app/scripts/lib/transaction/decode/sourcify.ts new file mode 100644 index 000000000000..b7939d379c75 --- /dev/null +++ b/app/scripts/lib/transaction/decode/sourcify.ts @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { FunctionFragment, Interface, ParamType } from '@ethersproject/abi'; +import { Hex, createProjectLogger } from '@metamask/utils'; +import { + DecodedTransactionDataMethod, + DecodedTransactionDataParam, +} from '../../../../../shared/types/transaction-decode'; + +const log = createProjectLogger('sourcify'); + +export type SourcifyResponse = { + files: { + name: string; + content: string; + }[]; +}; + +export type SourcifyMetadata = { + output: { + abi: { + inputs: { name: string; type: string }[]; + }[]; + devdoc?: { + methods: { + [signature: string]: { + details?: string; + params?: { [name: string]: string }; + }; + }; + }; + userdoc?: { + methods: { + [signature: string]: { + notice?: string; + params?: { [name: string]: string }; + }; + }; + }; + }; +}; + +export async function decodeTransactionDataWithSourcify( + transactionData: Hex, + contractAddress: Hex, + chainId: Hex, +): Promise { + const metadata = await fetchSourcifyMetadata(contractAddress, chainId); + + log('Retrieved Sourcify metadata', { + contractAddress, + chainId, + metadata, + }); + + const { abi } = metadata.output; + const contractInterface = new Interface(abi); + const functionSignature = transactionData.slice(0, 10); + + let functionData: FunctionFragment | undefined; + + try { + functionData = contractInterface.getFunction(functionSignature); + } catch (e) { + // Ignore + } + + if (!functionData) { + log('Failed to find function in ABI', functionSignature, abi); + return undefined; + } + + const { name, inputs } = functionData; + const signature = buildSignature(name, inputs); + const userDoc = metadata.output.userdoc?.methods[signature]; + const devDoc = metadata.output.devdoc?.methods[signature]; + const description = userDoc?.notice ?? devDoc?.details; + + log('Extracted NatSpec', { signature, userDoc, devDoc }); + + const values = contractInterface.decodeFunctionData( + functionSignature, + transactionData, + ) as any[]; + + const params = inputs.map((input, index) => + decodeParam(input, index, values, userDoc, devDoc), + ); + + return { + name, + description, + params, + }; +} + +function decodeParam( + input: ParamType, + index: number, + values: any[], + userDoc: any, + devDoc: any, +): DecodedTransactionDataParam { + const { name: paramName, type, components } = input; + + const paramDescription = + userDoc?.params?.[paramName] ?? devDoc?.params?.[paramName]; + + const value = values[index]; + + let children = components?.map((child, childIndex) => + decodeParam(child, childIndex, value, {}, {}), + ); + + if (type.endsWith('[]')) { + const childType = type.slice(0, -2); + + children = (value as any[]).map((_arrayItem, arrayIndex) => { + const childName = `Item ${arrayIndex + 1}`; + + return decodeParam( + { ...input, name: childName, type: childType } as ParamType, + arrayIndex, + value, + {}, + {}, + ); + }); + } + + return { + name: paramName, + description: paramDescription, + type, + value, + children, + }; +} + +async function fetchSourcifyMetadata(address: Hex, chainId: Hex) { + const response = await fetchSourcifyFiles(address, chainId); + + const metadata = response.files?.find((file) => + file.name.includes('metadata.json'), + ); + + if (!metadata) { + throw new Error('Metadata not found'); + } + + return JSON.parse(metadata.content) as SourcifyMetadata; +} + +async function fetchSourcifyFiles( + address: Hex, + chainId: Hex, +): Promise { + const chainIdDecimal = parseInt(chainId, 16); + + const respose = await fetch( + `https://sourcify.dev/server/files/any/${chainIdDecimal}/${address}`, + ); + + if (!respose.ok) { + throw new Error('Failed to fetch Sourcify files'); + } + + return respose.json(); +} + +function buildSignature(name: string | undefined, inputs: ParamType[]): string { + const types = inputs.map((input) => + input.components?.length + ? `${buildSignature(undefined, input.components)}${ + input.type.endsWith('[]') ? '[]' : '' + }` + : input.type, + ); + + return `${name ?? ''}(${types.join(',')})`; +} diff --git a/app/scripts/lib/transaction/decode/uniswap-commands.ts b/app/scripts/lib/transaction/decode/uniswap-commands.ts new file mode 100644 index 000000000000..b73f22c780cd --- /dev/null +++ b/app/scripts/lib/transaction/decode/uniswap-commands.ts @@ -0,0 +1,610 @@ +export const UNISWAP_ROUTER_COMMANDS = { + '0': { + name: 'V3_SWAP_EXACT_IN', + params: [ + { + type: 'address', + description: 'The recipient of the output of the trade', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of input tokens for the trade', + name: 'amountIn', + }, + { + type: 'uint256', + description: 'The minimum amount of output tokens the user wants', + name: 'amountOutMin', + }, + { + type: 'bytes', + description: 'The UniswapV3 encoded path to trade along', + name: 'path', + }, + { + type: 'bool', + description: + 'A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter', + name: 'payerIsUser', + }, + ], + }, + '1': { + name: 'V3_SWAP_EXACT_OUT', + params: [ + { + type: 'address', + description: 'The recipient of the output of the trade', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of output tokens to receive', + name: 'amountOut', + }, + { + type: 'uint256', + description: 'The maximum number of input tokens that should be spent', + name: 'amountInMax', + }, + { + type: 'bytes', + description: 'The UniswapV3 encoded path to trade along', + name: 'path', + }, + { + type: 'bool', + description: + 'A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter', + name: 'payerIsUser', + }, + ], + }, + '2': { + name: 'PERMIT2_TRANSFER_FROM', + params: [ + { + type: 'address', + description: 'The token to fetch from Permit2', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the tokens fetched', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of token to fetch', + name: 'amount', + }, + ], + }, + '3': { + name: 'PERMIT2_PERMIT_BATCH', + params: [ + { + type: 'bytes', + description: + 'A PermitBatch struct outlining all of the Permit2 permits to execute.', + name: 'batch', + }, + { + type: 'bytes', + description: 'The signature to provide to Permit2', + name: 'data', + }, + ], + }, + '4': { + name: 'SWEEP', + params: [ + { + type: 'address', + description: 'The ERC20 token to sweep (or Constants.ETH for ETH)', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the sweep', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The minimum required tokens to receive from the sweep', + name: 'amountMin', + }, + ], + }, + '5': { + name: 'TRANSFER', + params: [ + { + type: 'address', + description: 'The ERC20 token to transfer (or Constants.ETH for ETH)', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the transfer', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount to transfer', + name: 'value', + }, + ], + }, + '6': { + name: 'PAY_PORTION', + params: [ + { + type: 'address', + description: 'The ERC20 token to transfer (or Constants.ETH for ETH)', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the transfer', + name: 'recipient', + }, + { + type: 'uint256', + description: + 'In basis points, the percentage of the contract’s balance to transfer', + name: 'bips', + }, + ], + }, + '8': { + name: 'V2_SWAP_EXACT_IN', + params: [ + { + type: 'address', + description: 'The recipient of the output of the trade', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of input tokens for the trade', + name: 'amountIn', + }, + { + type: 'uint256', + description: 'The minimum amount of output tokens the user wants', + name: 'amountOutMin', + }, + { + type: 'address[]', + description: 'The UniswapV2 token path to trade along', + name: 'path', + }, + { + type: 'bool', + description: + 'A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter', + name: 'payerIsUser', + }, + ], + }, + '9': { + name: 'V2_SWAP_EXACT_OUT', + params: [ + { + type: 'address', + description: 'The recipient of the output of the trade', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of output tokens to receive', + name: 'amountOut', + }, + { + type: 'uint256', + description: 'The maximum number of input tokens that should be spent', + name: 'amountInMax', + }, + { + type: 'address[]', + description: 'The UniswapV2 token path to trade along', + name: 'path', + }, + { + type: 'bool', + description: + 'A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter', + name: 'payerIsUser', + }, + ], + }, + '10': { + name: 'PERMIT2_PERMIT', + params: [ + { + type: 'bytes', + description: + 'A PermitSingle struct outlining the Permit2 permit to execute', + name: 'permitSingle', + }, + { + type: 'bytes', + description: 'The signature to provide to Permit2', + name: 'signature', + }, + ], + }, + '11': { + name: 'WRAP_ETH', + params: [ + { + type: 'address', + description: 'The recipient of the WETH', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The amount of ETH to wrap', + name: 'amountMin', + }, + ], + }, + '12': { + name: 'UNWRAP_WETH', + params: [ + { + type: 'address', + description: 'The recipient of the ETH', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The minimum required ETH to receive from the unwrapping', + name: 'amountMin', + }, + ], + }, + '13': { + name: 'PERMIT2_TRANSFER_FROM_BATCH', + params: [ + { + type: 'bytes', + description: + 'An array of AllowanceTransferDetails structs that each describe a Permit2 transfer to perform', + name: 'batchDetails', + }, + ], + }, + '16': { + name: 'SEAPORT', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the Seaport contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the Seaport contract', + name: 'data', + }, + ], + }, + '17': { + name: 'LOOKS_RARE_721', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the LooksRare contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the LooksRare contract', + name: 'data', + }, + { + type: 'address', + description: 'The recipient of the ERC721', + name: 'recipient', + }, + { + type: 'address', + description: 'The ERC721 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC721', + name: 'id', + }, + ], + }, + '18': { + name: 'NFTX', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the NFTX contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the NFTX contract', + name: 'data', + }, + ], + }, + '19': { + name: 'CRYPTOPUNKS', + params: [ + { + type: 'uint256', + description: 'The PunkID to purchase', + name: 'punkId', + }, + { + type: 'address', + description: 'The recipient for the cryptopunk', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The ETH value to forward to the Cryptopunks contract', + name: 'value', + }, + ], + }, + '20': { + name: 'LOOKS_RARE_1155', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the LooksRare contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the LooksRare contract', + name: 'data', + }, + { + type: 'address', + description: 'The recipient of the ERC1155', + name: 'recipient', + }, + { + type: 'address', + description: 'The ERC1155 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC1155', + name: 'id', + }, + { + type: 'uint256', + description: 'The amount of the ERC1155 to transfer', + name: 'amount', + }, + ], + }, + '21': { + name: 'OWNER_CHECK_721', + params: [ + { + type: 'address', + description: 'The required owner of the ERC721', + name: 'owner', + }, + { + type: 'address', + description: 'The ERC721 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC721', + name: 'id', + }, + ], + }, + '22': { + name: 'OWNER_CHECK_1155', + params: [ + { + type: 'address', + description: 'The required owner of the ERC1155', + name: 'owner', + }, + { + type: 'address', + description: 'The ERC721 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC1155', + name: 'id', + }, + { + type: 'uint256', + description: 'The minimum required amount of the ERC1155', + name: 'minBalance', + }, + ], + }, + '23': { + name: 'SWEEP_ERC721', + params: [ + { + type: 'address', + description: 'The ERC721 token address to transfer', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the transfer', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The token ID to transfer', + name: 'id', + }, + ], + }, + '24': { + name: 'X2Y2_721', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the X2Y2 contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the X2Y2 contract', + name: 'data', + }, + { + type: 'address', + description: 'The recipient of the ERC721', + name: 'recipient', + }, + { + type: 'address', + description: 'The ERC721 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC721', + name: 'id', + }, + ], + }, + '25': { + name: 'SUDOSWAP', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the Sudoswap contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the Sudoswap contract', + name: 'data', + }, + ], + }, + '26': { + name: 'NFT20', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the NFT20 contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the NFT20 contract', + name: 'data', + }, + ], + }, + '27': { + name: 'X2Y2_1155', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the X2Y2 contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the X2Y2 contract', + name: 'data', + }, + { + type: 'address', + description: 'The recipient of the ERC1155', + name: 'recipient', + }, + { + type: 'address', + description: 'The ERC1155 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC1155', + name: 'id', + }, + { + type: 'uint256', + description: 'The amount of the ERC1155 to transfer', + name: 'amount', + }, + ], + }, + '28': { + name: 'FOUNDATION', + params: [ + { + type: 'uint256', + description: 'The ETH value to forward to the Foundation contract', + name: 'value', + }, + { + type: 'bytes', + description: 'The calldata to use to call the Foundation contract', + name: 'data', + }, + { + type: 'address', + description: 'The recipient of the ERC721', + name: 'recipient', + }, + { + type: 'address', + description: 'The ERC721 token address', + name: 'token', + }, + { + type: 'uint256', + description: 'The ID of the ERC721', + name: 'id', + }, + ], + }, + '29': { + name: 'SWEEP_ERC1155', + params: [ + { + type: 'address', + description: 'The ERC1155 token address to sweep', + name: 'token', + }, + { + type: 'address', + description: 'The recipient of the sweep', + name: 'recipient', + }, + { + type: 'uint256', + description: 'The token ID to sweep', + name: 'id', + }, + { + type: 'uint256', + description: 'The minimum required tokens to receive from the sweep', + name: 'amount', + }, + ], + }, +}; diff --git a/app/scripts/lib/transaction/decode/uniswap.test.ts b/app/scripts/lib/transaction/decode/uniswap.test.ts new file mode 100644 index 000000000000..86455b8c9dc0 --- /dev/null +++ b/app/scripts/lib/transaction/decode/uniswap.test.ts @@ -0,0 +1,164 @@ +import { CHAIN_IDS } from '../../../../../shared/constants/network'; +import { TRANSACTION_DATA_UNISWAP } from '../../../../../test/data/confirmations/transaction-decode'; +import { + UNISWAP_UNIVERSAL_ROUTER_ADDRESSES, + decodeUniswapRouterTransactionData, +} from './uniswap'; + +describe('Uniswap', () => { + describe('decodeUniswapRouterTransactionData', () => { + it('returns undefined if invalid data', () => { + expect( + decodeUniswapRouterTransactionData({ + transactionData: '0x123', + contractAddress: + UNISWAP_UNIVERSAL_ROUTER_ADDRESSES[CHAIN_IDS.MAINNET][0], + chainId: CHAIN_IDS.MAINNET, + }), + ).toBeUndefined(); + }); + + it('returns undefined if contract address does not match chain', () => { + expect( + decodeUniswapRouterTransactionData({ + transactionData: TRANSACTION_DATA_UNISWAP, + contractAddress: '0x123', + chainId: CHAIN_IDS.MAINNET, + }), + ).toBeUndefined(); + }); + + it('returns expected commands', () => { + expect( + decodeUniswapRouterTransactionData({ + transactionData: TRANSACTION_DATA_UNISWAP, + contractAddress: + UNISWAP_UNIVERSAL_ROUTER_ADDRESSES[CHAIN_IDS.MAINNET][0], + chainId: CHAIN_IDS.MAINNET, + }), + ).toMatchInlineSnapshot(` + [ + { + "name": "WRAP_ETH", + "params": [ + { + "description": "The recipient of the WETH", + "name": "recipient", + "type": "address", + "value": "0x0000000000000000000000000000000000000002", + }, + { + "description": "The amount of ETH to wrap", + "name": "amountMin", + "type": "uint256", + "value": { + "hex": "0x5af3107a4000", + "type": "BigNumber", + }, + }, + ], + }, + { + "name": "V3_SWAP_EXACT_IN", + "params": [ + { + "description": "The recipient of the output of the trade", + "name": "recipient", + "type": "address", + "value": "0x0000000000000000000000000000000000000002", + }, + { + "description": "The amount of input tokens for the trade", + "name": "amountIn", + "type": "uint256", + "value": { + "hex": "0x5af3107a4000", + "type": "BigNumber", + }, + }, + { + "description": "The minimum amount of output tokens the user wants", + "name": "amountOutMin", + "type": "uint256", + "value": { + "hex": "0x04c418", + "type": "BigNumber", + }, + }, + { + "description": "The UniswapV3 encoded path to trade along", + "name": "path", + "type": "bytes", + "value": [ + { + "firstAddress": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "secondAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "tickSpacing": 500, + }, + ], + }, + { + "description": "A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter", + "name": "payerIsUser", + "type": "bool", + "value": false, + }, + ], + }, + { + "name": "PAY_PORTION", + "params": [ + { + "description": "The ERC20 token to transfer (or Constants.ETH for ETH)", + "name": "token", + "type": "address", + "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }, + { + "description": "The recipient of the transfer", + "name": "recipient", + "type": "address", + "value": "0x27213E28D7fDA5c57Fe9e5dD923818DBCcf71c47", + }, + { + "description": "In basis points, the percentage of the contract’s balance to transfer", + "name": "bips", + "type": "uint256", + "value": { + "hex": "0x19", + "type": "BigNumber", + }, + }, + ], + }, + { + "name": "SWEEP", + "params": [ + { + "description": "The ERC20 token to sweep (or Constants.ETH for ETH)", + "name": "token", + "type": "address", + "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }, + { + "description": "The recipient of the sweep", + "name": "recipient", + "type": "address", + "value": "0x0000000000000000000000000000000000000001", + }, + { + "description": "The minimum required tokens to receive from the sweep", + "name": "amountMin", + "type": "uint256", + "value": { + "hex": "0x04c418", + "type": "BigNumber", + }, + }, + ], + }, + ] + `); + }); + }); +}); diff --git a/app/scripts/lib/transaction/decode/uniswap.ts b/app/scripts/lib/transaction/decode/uniswap.ts new file mode 100644 index 000000000000..c38e204180fd --- /dev/null +++ b/app/scripts/lib/transaction/decode/uniswap.ts @@ -0,0 +1,214 @@ +import { Interface, TransactionDescription } from '@ethersproject/abi'; +import { Hex } from '@metamask/utils'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; +import { CHAIN_IDS } from '../../../../../shared/constants/network'; +import { UNISWAP_ROUTER_COMMANDS } from './uniswap-commands'; + +export type UniswapRouterCommand = { + name: string; + params: { + name: string; + type: string; + description: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; + }[]; +}; + +export type UniswapPathPool = { + firstAddress: Hex; + tickSpacing: number; + secondAddress: Hex; +}; + +const ADDRESS_LENGTH = 40; +const TICK_SPACING_LENGTH = 6; + +export const UNISWAP_UNIVERSAL_ROUTER_ADDRESSES = { + [CHAIN_IDS.ARBITRUM]: [ + '0x4C60051384bd2d3C01bfc845Cf5F4b44bcbE9de5', + '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4', + '0x5E325eDA8064b456f4781070C0738d849c824258', + ], + [CHAIN_IDS.AVALANCHE]: [ + '0x82635AF6146972cD6601161c4472ffe97237D292', + '0x4Dae2f939ACf50408e13d58534Ff8c2776d45265', + ], + [CHAIN_IDS.BASE]: [ + '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4', + '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', + ], + [CHAIN_IDS.BSC]: [ + '0x5Dc88340E1c5c6366864Ee415d6034cadd1A9897', + '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4', + '0x4Dae2f939ACf50408e13d58534Ff8c2776d45265', + ], + [CHAIN_IDS.MAINNET]: [ + '0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B', + '0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD', + ], + [CHAIN_IDS.OPTIMISM]: [ + '0xb555edF5dcF85f42cEeF1f3630a52A108E55A654', + '0xeC8B0F7Ffe3ae75d7FfAb09429e3675bb63503e4', + '0xCb1355ff08Ab38bBCE60111F1bb2B784bE25D7e8', + ], + [CHAIN_IDS.POLYGON]: [ + '0x4C60051384bd2d3C01bfc845Cf5F4b44bcbE9de5', + '0x643770E279d5D0733F21d6DC03A8efbABf3255B4', + '0xec7BE89e9d109e7e3Fec59c222CF297125FEFda2', + ], + [CHAIN_IDS.SEPOLIA]: ['0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD'], +} as Record; + +const ABI = [ + { + constant: true, + inputs: [ + { + name: 'commands', + type: 'bytes', + }, + { + name: 'inputs', + type: 'bytes[]', + }, + { + name: 'deadline', + type: 'uint256', + }, + ], + name: 'execute', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: 'commands', + type: 'bytes', + }, + { + name: 'inputs', + type: 'bytes[]', + }, + ], + name: 'execute', + type: 'function', + }, +]; + +export function decodeUniswapRouterTransactionData({ + transactionData, + contractAddress, + chainId, +}: { + transactionData: string; + contractAddress: string; + chainId: string; +}): UniswapRouterCommand[] | undefined { + const supportedAddresses = UNISWAP_UNIVERSAL_ROUTER_ADDRESSES[chainId]; + + if ( + !supportedAddresses + ?.map((address) => address.toLowerCase()) + .includes(contractAddress.toLowerCase()) + ) { + return undefined; + } + + const contractInterface = new Interface(ABI); + + let parsedTransactionData: TransactionDescription; + + try { + parsedTransactionData = contractInterface.parseTransaction({ + data: transactionData, + }); + } catch (error) { + return undefined; + } + + const commands = parsedTransactionData.args.commands as string; + const inputs = parsedTransactionData.args.inputs as string[]; + const commandBytes = commands.slice(2).match(/.{1,2}/gu) as string[]; + + return commandBytes + .map((commandByte, i) => decodeUniswapCommand(commandByte, inputs[i])) + .filter((command) => command !== undefined) as UniswapRouterCommand[]; +} + +function decodeUniswapCommand( + commandByte: string, + input: string, +): UniswapRouterCommand | undefined { + const commandValue = parseInt(commandByte, 16); + // eslint-disable-next-line no-bitwise + const commandIndex = commandValue & 0b11111; + + const data = + UNISWAP_ROUTER_COMMANDS[ + String(commandIndex) as keyof typeof UNISWAP_ROUTER_COMMANDS + ]; + + if (!data) { + return undefined; + } + + const types = data.params.map((param) => param.type); + const abiDecoder = Interface.getAbiCoder(); + const values = abiDecoder.decode(types, input); + const { name } = data; + + const params = data.params.map((param, index) => { + const { name: paramName, type, description } = param; + const rawData = values[index]; + const value = paramName === 'path' ? decodeUniswapPath(rawData) : rawData; + + return { name: paramName, type, value, description }; + }); + + return { + name, + params, + }; +} + +function decodeUniswapPath(rawPath: string): UniswapPathPool[] { + const pools: UniswapPathPool[] = []; + let remainingData = stripHexPrefix(rawPath); + let currentPool = {} as UniswapPathPool; + let isParsingAddress = true; + + while (remainingData.length) { + if (isParsingAddress) { + const address = addHexPrefix( + remainingData.slice(0, ADDRESS_LENGTH), + ) as Hex; + + if (currentPool.firstAddress) { + currentPool.secondAddress = address; + + pools.push(currentPool); + + currentPool = { + firstAddress: address, + } as UniswapPathPool; + } else { + currentPool.firstAddress = address; + } + + remainingData = remainingData.slice(ADDRESS_LENGTH); + } else { + currentPool.tickSpacing = parseInt( + remainingData.slice(0, TICK_SPACING_LENGTH), + 16, + ); + + remainingData = remainingData.slice(TICK_SPACING_LENGTH); + } + + isParsingAddress = !isParsingAddress; + } + + return pools; +} diff --git a/app/scripts/lib/transaction/decode/util.test.ts b/app/scripts/lib/transaction/decode/util.test.ts new file mode 100644 index 000000000000..66984b29cfe4 --- /dev/null +++ b/app/scripts/lib/transaction/decode/util.test.ts @@ -0,0 +1,106 @@ +import EthQuery from '@metamask/eth-query'; +import { + TRANSACTION_DATA_FOUR_BYTE, + TRANSACTION_DATA_SOURCIFY, + TRANSACTION_DATA_UNISWAP, + TRANSACTION_DECODE_FOUR_BYTE, + TRANSACTION_DECODE_SOURCIFY, + TRANSACTION_DECODE_UNISWAP, +} from '../../../../../test/data/confirmations/transaction-decode'; +import { decodeUniswapRouterTransactionData } from './uniswap'; +import { decodeTransactionData } from './util'; +import { decodeTransactionDataWithSourcify } from './sourcify'; +import { decodeTransactionDataWithFourByte } from './four-byte'; +import { getContractProxyAddress } from './proxy'; + +jest.mock('./uniswap'); +jest.mock('./sourcify'); +jest.mock('./four-byte'); +jest.mock('./proxy'); + +const CONTRACT_ADDRESS_MOCK = '0x456'; +const CHAIN_ID_MOCK = '0x123'; +const ETH_QUERY_MOCK = {} as EthQuery; + +describe('Transaction Decode Utils', () => { + const decodeUniswapRouterTransactionDataMock = jest.mocked( + decodeUniswapRouterTransactionData, + ); + + const decodeTransactionDataWithSourcifyMock = jest.mocked( + decodeTransactionDataWithSourcify, + ); + + const decodeTransactionDataWithFourByteMock = jest.mocked( + decodeTransactionDataWithFourByte, + ); + + const getContractProxyAddressMock = jest.mocked(getContractProxyAddress); + + beforeEach(() => { + jest.resetAllMocks(); + + decodeUniswapRouterTransactionDataMock.mockReturnValue(undefined); + decodeTransactionDataWithSourcifyMock.mockResolvedValue(undefined); + decodeTransactionDataWithFourByteMock.mockResolvedValue(undefined); + getContractProxyAddressMock.mockResolvedValue(undefined); + }); + + describe('decodeTransactionData', () => { + it('returns uniswap data', async () => { + decodeUniswapRouterTransactionDataMock.mockReturnValue( + TRANSACTION_DECODE_UNISWAP.data, + ); + + const result = await decodeTransactionData({ + transactionData: TRANSACTION_DATA_UNISWAP, + contractAddress: CONTRACT_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + ethQuery: ETH_QUERY_MOCK, + }); + + expect(result).toStrictEqual(TRANSACTION_DECODE_UNISWAP); + }); + + it('returns sourcify data', async () => { + decodeTransactionDataWithSourcifyMock.mockResolvedValue( + TRANSACTION_DECODE_SOURCIFY.data[0], + ); + + const result = await decodeTransactionData({ + transactionData: TRANSACTION_DATA_SOURCIFY, + contractAddress: CONTRACT_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + ethQuery: ETH_QUERY_MOCK, + }); + + expect(result).toStrictEqual(TRANSACTION_DECODE_SOURCIFY); + }); + + it('returns four byte data', async () => { + decodeTransactionDataWithFourByteMock.mockResolvedValue( + TRANSACTION_DECODE_FOUR_BYTE.data[0], + ); + + const result = await decodeTransactionData({ + transactionData: TRANSACTION_DATA_FOUR_BYTE, + contractAddress: CONTRACT_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + ethQuery: ETH_QUERY_MOCK, + }); + + expect(result).toStrictEqual(TRANSACTION_DECODE_FOUR_BYTE); + }); + + it('returns undefined if no data', async () => { + const result = await decodeTransactionData({ + transactionData: TRANSACTION_DATA_FOUR_BYTE, + contractAddress: CONTRACT_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + ethQuery: ETH_QUERY_MOCK, + }); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/app/scripts/lib/transaction/decode/util.ts b/app/scripts/lib/transaction/decode/util.ts new file mode 100644 index 000000000000..d8bca103ff73 --- /dev/null +++ b/app/scripts/lib/transaction/decode/util.ts @@ -0,0 +1,128 @@ +import { Hex, createProjectLogger } from '@metamask/utils'; +import EthQuery from '@metamask/eth-query'; +import { + DecodedTransactionDataMethod, + DecodedTransactionDataParam, + DecodedTransactionDataResponse, + DecodedTransactionDataSource, +} from '../../../../../shared/types/transaction-decode'; +import { decodeUniswapRouterTransactionData } from './uniswap'; +import { decodeTransactionDataWithSourcify } from './sourcify'; +import { getContractProxyAddress } from './proxy'; +import { decodeTransactionDataWithFourByte } from './four-byte'; + +const log = createProjectLogger('transaction-decode'); + +export async function decodeTransactionData({ + transactionData, + contractAddress, + chainId, + ethQuery, +}: { + transactionData: Hex; + contractAddress: Hex; + chainId: Hex; + ethQuery: EthQuery; +}): Promise { + log('Decoding transaction data', { + transactionData, + contractAddress, + chainId, + }); + + const uniswapData = decodeUniswapRouterTransactionData({ + transactionData, + contractAddress, + chainId, + }); + + if (uniswapData) { + log('Decoded with Uniswap commands', uniswapData); + + return { + data: normalizeDecodedMethods(uniswapData), + source: DecodedTransactionDataSource.Uniswap, + }; + } + + const proxyAddress = await getContractProxyAddress(contractAddress, ethQuery); + + if (proxyAddress) { + log('Retrieved proxy implementation address', proxyAddress); + } + + const address = proxyAddress ?? contractAddress; + + const sourcifyData = decodeTransactionDataWithSourcify( + transactionData, + address, + chainId, + ); + + const fourByteData = decodeTransactionDataWithFourByte(transactionData); + + const [sourcifyResult, fourByteResult] = await Promise.allSettled([ + sourcifyData, + fourByteData, + ]); + + if (sourcifyResult.status === 'fulfilled' && sourcifyResult.value) { + log('Decoded data with Sourcify', sourcifyResult.value); + + return { + data: normalizeDecodedMethods([sourcifyResult.value]), + source: DecodedTransactionDataSource.Sourcify, + }; + } + + log('Failed to decode data with Sourcify', sourcifyResult); + + if (fourByteResult.status === 'fulfilled' && fourByteResult.value) { + log('Decoded data with 4Byte', fourByteResult.value); + + return { + data: normalizeDecodedMethods([fourByteResult.value]), + source: DecodedTransactionDataSource.FourByte, + }; + } + + log('Failed to decode data with 4Byte', fourByteResult); + + return undefined; +} + +function normalizeDecodedMethods( + methods: DecodedTransactionDataMethod[], +): DecodedTransactionDataMethod[] { + return methods.map((method) => normalizeDecodedMethod(method)); +} + +function normalizeDecodedMethod( + method: DecodedTransactionDataMethod, +): DecodedTransactionDataMethod { + return { + ...method, + params: method.params.map((param) => normalizeDecodedParam(param)), + }; +} + +function normalizeDecodedParam( + param: DecodedTransactionDataParam, +): DecodedTransactionDataParam { + return { + ...param, + value: normalizeDecodedParamValue(param.value), + children: param.children?.map((child) => normalizeDecodedParam(child)), + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function normalizeDecodedParamValue(value: any): any { + const hexValue = value._hex; + + if (hexValue) { + return parseInt(hexValue, 16); + } + + return value; +} diff --git a/app/scripts/lib/transaction/metrics.test.ts b/app/scripts/lib/transaction/metrics.test.ts index 3fed7842378b..75ea5c4b84c0 100644 --- a/app/scripts/lib/transaction/metrics.test.ts +++ b/app/scripts/lib/transaction/metrics.test.ts @@ -72,6 +72,10 @@ const mockTransactionMetricsRequest = { trackEvent: jest.fn(), getIsSmartTransaction: jest.fn(), getSmartTransactionByMinedTxHash: jest.fn(), + getRedesignedTransactionsEnabled: jest.fn(), + getMethodData: jest.fn(), + getIsRedesignedConfirmationsDeveloperEnabled: jest.fn(), + getIsConfirmationAdvancedDetailsOpen: jest.fn(), } as TransactionMetricsRequest; describe('Transaction metrics', () => { @@ -152,6 +156,7 @@ describe('Transaction metrics', () => { transaction_speed_up: false, transaction_type: TransactionType.simpleSend, ui_customizations: null, + transaction_advanced_view: null, }; expectedSensitiveProperties = { diff --git a/app/scripts/lib/transaction/metrics.ts b/app/scripts/lib/transaction/metrics.ts index 96ca86f8e7d3..3177e2d74b26 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -1,27 +1,15 @@ -import { isHexString } from 'ethereumjs-util'; import EthQuery, { Provider } from '@metamask/eth-query'; -import { BigNumber } from 'bignumber.js'; import { FetchGasFeeEstimateOptions } from '@metamask/gas-fee-controller'; +import { BigNumber } from 'bignumber.js'; +import { isHexString } from 'ethereumjs-util'; +import { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types'; import { TransactionMeta, TransactionType, } from '@metamask/transaction-controller'; -import { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; -import { - determineTransactionAssetType, - isEIP1559Transaction, -} from '../../../../shared/modules/transaction.utils'; -import { - hexWEIToDecETH, - hexWEIToDecGWEI, -} from '../../../../shared/modules/conversion.utils'; -import { - TokenStandard, - TransactionApprovalAmountType, - TransactionMetaMetricsEvent, -} from '../../../../shared/constants/transaction'; +import { GasRecommendations } from '../../../../shared/constants/gas'; import { MetaMetricsEventCategory, MetaMetricsEventFragment, @@ -30,17 +18,33 @@ import { MetaMetricsPageObject, MetaMetricsReferrerObject, } from '../../../../shared/constants/metametrics'; -import { GasRecommendations } from '../../../../shared/constants/gas'; +import { + TokenStandard, + TransactionApprovalAmountType, + TransactionMetaMetricsEvent, +} from '../../../../shared/constants/transaction'; import { calcGasTotal, getSwapsTokensReceivedFromTxMeta, TRANSACTION_ENVELOPE_TYPE_NAMES, } from '../../../../shared/lib/transactions-controller-utils'; +import { + hexWEIToDecETH, + hexWEIToDecGWEI, +} from '../../../../shared/modules/conversion.utils'; +import { getSmartTransactionMetricsProperties } from '../../../../shared/modules/metametrics'; +import { + determineTransactionAssetType, + isEIP1559Transaction, +} from '../../../../shared/modules/transaction.utils'; import { getBlockaidMetricsProps, getSwapAndSendMetricsProps, } from '../../../../ui/helpers/utils/metrics'; -import { getSmartTransactionMetricsProperties } from '../../../../shared/modules/metametrics'; +import { + REDESIGN_DEV_TRANSACTION_TYPES, + REDESIGN_USER_TRANSACTION_TYPES, +} from '../../../../ui/pages/confirmations/utils'; import { getSnapAndHardwareInfoForMetrics, type SnapAndHardwareMessenger, @@ -93,6 +97,10 @@ export type TransactionMetricsRequest = { getSmartTransactionByMinedTxHash: ( txhash: string | undefined, ) => SmartTransaction; + getRedesignedTransactionsEnabled: () => boolean; + getMethodData: (data: string) => Promise<{ name: string }>; + getIsRedesignedConfirmationsDeveloperEnabled: () => boolean; + getIsConfirmationAdvancedDetailsOpen: () => boolean; }; export const METRICS_STATUS_FAILED = 'failed on-chain'; @@ -790,7 +798,6 @@ async function buildEventFragmentProperties({ currentTokenBalance, originalApprovalAmount, finalApprovalAmount, - contractMethodName, securityProviderResponse, simulationFails, } = transactionMeta; @@ -803,6 +810,14 @@ async function buildEventFragmentProperties({ transactionMetricsRequest.getTokenStandardAndDetails, ); + let contractMethodName; + if (transactionMeta.txParams.data) { + const { name } = await transactionMetricsRequest.getMethodData( + transactionMeta.txParams.data, + ); + contractMethodName = name; + } + // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const gasParams = {} as Record; @@ -960,6 +975,7 @@ async function buildEventFragmentProperties({ } const uiCustomizations = []; + let isAdvancedDetailsOpen = null; /** securityProviderResponse is used by the OpenSea <> Blockaid provider */ // eslint-disable-next-line no-lonely-if @@ -982,7 +998,30 @@ async function buildEventFragmentProperties({ if (simulationFails) { uiCustomizations.push(MetaMetricsEventUiCustomization.GasEstimationFailed); } + const isRedesignedConfirmationsDeveloperSettingEnabled = + transactionMetricsRequest.getIsRedesignedConfirmationsDeveloperEnabled() || + Boolean(process.env.ENABLE_CONFIRMATION_REDESIGN); + + const isRedesignedTransactionsUserSettingEnabled = + transactionMetricsRequest.getRedesignedTransactionsEnabled(); + if ( + (isRedesignedConfirmationsDeveloperSettingEnabled && + REDESIGN_DEV_TRANSACTION_TYPES.includes( + transactionMeta.type as TransactionType, + )) || + (isRedesignedTransactionsUserSettingEnabled && + REDESIGN_USER_TRANSACTION_TYPES.includes( + transactionMeta.type as TransactionType, + )) + ) { + uiCustomizations.push( + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ); + + isAdvancedDetailsOpen = + transactionMetricsRequest.getIsConfirmationAdvancedDetailsOpen(); + } const smartTransactionMetricsProperties = getSmartTransactionMetricsProperties( transactionMetricsRequest, @@ -1016,6 +1055,7 @@ async function buildEventFragmentProperties({ ...blockaidProperties, // ui_customizations must come after ...blockaidProperties ui_customizations: uiCustomizations.length > 0 ? uiCustomizations : null, + transaction_advanced_view: isAdvancedDetailsOpen, ...smartTransactionMetricsProperties, ...swapAndSendMetricsProperties, // TODO: Replace `any` with type diff --git a/app/scripts/lib/transaction/smart-transactions.test.ts b/app/scripts/lib/transaction/smart-transactions.test.ts index eed52a909f67..b3cea1b12c88 100644 --- a/app/scripts/lib/transaction/smart-transactions.test.ts +++ b/app/scripts/lib/transaction/smart-transactions.test.ts @@ -110,6 +110,8 @@ describe('submitSmartTransactionHook', () => { smartTransactionsController: createSmartTransactionsControllerMock(), transactionController: createTransactionControllerMock(), isSmartTransaction: true, + signedTransactionInHex: + '0x02f8b104058504a817c8008504a817c80082b427949ba60bbf4ba1de43f3b4983a539feebfbd5fd97680b844095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000011170c080a0fdd2cb46203b5e7bba99cc56a37da3e5e3f36163a5bd9c51cddfd8d7028f5dd0a054c35cfa10b3350a3fd3a0e7b4aeb0b603d528c07a8cfdf4a78505d9864edef4', controllerMessenger: createSmartTransactionsControllerMessengerMock(), featureFlags: { extensionActive: true, @@ -203,7 +205,7 @@ describe('submitSmartTransactionHook', () => { ); }); - it('submits a smart transaction', async () => { + it('submits a smart transaction with an already signed transaction', async () => { const request: SubmitSmartTransactionRequestMocked = createRequest(); setImmediate(() => { request.smartTransactionsController.eventEmitter.emit( @@ -227,6 +229,89 @@ describe('submitSmartTransactionHook', () => { }); const result = await submitSmartTransactionHook(request); expect(result).toEqual({ transactionHash: txHash }); + const { txParams } = request.transactionMeta; + expect( + request.smartTransactionsController.submitSignedTransactions, + ).toHaveBeenCalledWith({ + signedTransactions: [request.signedTransactionInHex], + signedCanceledTransactions: [], + txParams, + transactionMeta: request.transactionMeta, + }); + addRequestCallback(); + expect(request.controllerMessenger.call).toHaveBeenCalledTimes(4); + expect(request.controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:startFlow', + ); + expect(request.controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:addRequest', + { + id: 'approvalId', + origin: 'http://localhost', + type: 'smartTransaction:showSmartTransactionStatusPage', + requestState: { + smartTransaction: { + status: 'pending', + uuid, + creationTime: expect.any(Number), + }, + isDapp: true, + txId, + }, + }, + true, + ); + expect(request.controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:updateRequestState', + { + id: 'approvalId', + requestState: { + smartTransaction: { + status: 'success', + statusMetadata: { + minedHash: + '0x0302b75dfb9fd9eb34056af031efcaee2a8cbd799ea054a85966165cd82a7356', + }, + }, + isDapp: true, + txId, + }, + }, + ); + + expect(request.controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:endFlow', + { + id: 'approvalId', + }, + ); + }); + + it('signs and submits a smart transaction', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + request.signedTransactionInHex = undefined; + setImmediate(() => { + request.smartTransactionsController.eventEmitter.emit( + `uuid:smartTransaction`, + { + status: 'pending', + statusMetadata: { + minedHash: '', + }, + }, + ); + request.smartTransactionsController.eventEmitter.emit( + `uuid:smartTransaction`, + { + status: 'success', + statusMetadata: { + minedHash: txHash, + }, + }, + ); + }); + const result = await submitSmartTransactionHook(request); + expect(result).toEqual({ transactionHash: txHash }); const { txParams, chainId } = request.transactionMeta; expect( request.transactionController.approveTransactionsWithSameNonce, @@ -325,24 +410,14 @@ describe('submitSmartTransactionHook', () => { }); const result = await submitSmartTransactionHook(request); expect(result).toEqual({ transactionHash: txHash }); - const { txParams, chainId } = request.transactionMeta; + const { txParams } = request.transactionMeta; expect( request.transactionController.approveTransactionsWithSameNonce, - ).toHaveBeenCalledWith( - [ - { - ...txParams, - maxFeePerGas: '0x2fd8a58d7', - maxPriorityFeePerGas: '0xaa0f8a94', - chainId, - }, - ], - { hasNonce: true }, - ); + ).not.toHaveBeenCalled(); expect( request.smartTransactionsController.submitSignedTransactions, ).toHaveBeenCalledWith({ - signedTransactions: [createSignedTransaction()], + signedTransactions: [request.signedTransactionInHex], signedCanceledTransactions: [], txParams, transactionMeta: request.transactionMeta, diff --git a/app/scripts/lib/transaction/smart-transactions.ts b/app/scripts/lib/transaction/smart-transactions.ts index 21a95e94494d..9226f003232b 100644 --- a/app/scripts/lib/transaction/smart-transactions.ts +++ b/app/scripts/lib/transaction/smart-transactions.ts @@ -60,6 +60,7 @@ export type FeatureFlags = { export type SubmitSmartTransactionRequest = { transactionMeta: TransactionMeta; + signedTransactionInHex?: string; smartTransactionsController: SmartTransactionsController; transactionController: TransactionController; isSmartTransaction: boolean; @@ -96,11 +97,14 @@ class SmartTransactionHook { #transactionMeta: TransactionMeta; + #signedTransactionInHex?: string; + #txParams: TransactionParams; constructor(request: SubmitSmartTransactionRequest) { const { transactionMeta, + signedTransactionInHex, smartTransactionsController, transactionController, isSmartTransaction, @@ -110,6 +114,7 @@ class SmartTransactionHook { this.#approvalFlowId = ''; this.#approvalFlowEnded = false; this.#transactionMeta = transactionMeta; + this.#signedTransactionInHex = signedTransactionInHex; this.#smartTransactionsController = smartTransactionsController; this.#transactionController = transactionController; this.#isSmartTransaction = isSmartTransaction; @@ -291,17 +296,18 @@ class SmartTransactionHook { }: { getFeesResponse: Fees; }) { - const signedTransactions = await this.#createSignedTransactions( - getFeesResponse.tradeTxFees?.fees ?? [], - false, - ); - const signedCanceledTransactions = await this.#createSignedTransactions( - getFeesResponse.tradeTxFees?.cancelFees || [], - true, - ); + let signedTransactions; + if (this.#signedTransactionInHex) { + signedTransactions = [this.#signedTransactionInHex]; + } else { + signedTransactions = await this.#createSignedTransactions( + getFeesResponse.tradeTxFees?.fees ?? [], + false, + ); + } return await this.#smartTransactionsController.submitSignedTransactions({ signedTransactions, - signedCanceledTransactions, + signedCanceledTransactions: [], txParams: this.#txParams, transactionMeta: this.#transactionMeta, }); diff --git a/app/scripts/lib/transaction/util.test.ts b/app/scripts/lib/transaction/util.test.ts index f8b07cc7b17c..16077e0f08ed 100644 --- a/app/scripts/lib/transaction/util.test.ts +++ b/app/scripts/lib/transaction/util.test.ts @@ -9,6 +9,7 @@ import { UserOperationController } from '@metamask/user-operation-controller'; import { cloneDeep } from 'lodash'; import { generateSecurityAlertId, + isChainSupported, validateRequestWithPPOM, } from '../ppom/ppom-util'; import { @@ -17,6 +18,7 @@ import { } from '../../../../shared/constants/security-provider'; import { SecurityAlertResponse } from '../ppom/types'; import { flushPromises } from '../../../../test/lib/timer-helpers'; +import { createMockInternalAccount } from '../../../../test/jest/mocks'; import { AddDappTransactionRequest, AddTransactionOptions, @@ -38,6 +40,11 @@ jest.mock('uuid', () => { const SECURITY_ALERT_ID_MOCK = '123'; +const INTERNAL_ACCOUNT_ADDRESS = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b'; +const INTERNAL_ACCOUNT = createMockInternalAccount({ + address: INTERNAL_ACCOUNT_ADDRESS, +}); + const TRANSACTION_PARAMS_MOCK: TransactionParams = { from: '0x1', }; @@ -69,7 +76,8 @@ const TRANSACTION_REQUEST_MOCK: AddTransactionRequest = { transactionParams: TRANSACTION_PARAMS_MOCK, transactionOptions: TRANSACTION_OPTIONS_MOCK, waitForSubmit: false, -} as AddTransactionRequest; + internalAccounts: [], +} as unknown as AddTransactionRequest; const SECURITY_ALERT_RESPONSE_MOCK: SecurityAlertResponse = { result_type: BlockaidResultType.Malicious, @@ -97,6 +105,7 @@ describe('Transaction Utils', () => { let userOperationController: jest.Mocked; const validateRequestWithPPOMMock = jest.mocked(validateRequestWithPPOM); const generateSecurityAlertIdMock = jest.mocked(generateSecurityAlertId); + const isChainSupportedMock = jest.mocked(isChainSupported); beforeEach(() => { jest.resetAllMocks(); @@ -121,6 +130,7 @@ describe('Transaction Utils', () => { }); generateSecurityAlertIdMock.mockReturnValue(SECURITY_ALERT_ID_MOCK); + isChainSupportedMock.mockResolvedValue(true); request.transactionController = transactionController; request.userOperationController = userOperationController; @@ -466,7 +476,38 @@ describe('Transaction Utils', () => { expect(validateRequestWithPPOMMock).toHaveBeenCalledTimes(0); }); + it('send to users own account', async () => { + const sendRequest = { + ...request, + transactionParams: { + ...request.transactionParams, + to: INTERNAL_ACCOUNT_ADDRESS, + }, + }; + await addTransaction({ + ...sendRequest, + securityAlertsEnabled: false, + chainId: '0x1', + internalAccounts: [INTERNAL_ACCOUNT], + }); + + expect( + request.transactionController.addTransaction, + ).toHaveBeenCalledTimes(1); + + expect( + request.transactionController.addTransaction, + ).toHaveBeenCalledWith( + sendRequest.transactionParams, + TRANSACTION_OPTIONS_MOCK, + ); + + expect(validateRequestWithPPOMMock).toHaveBeenCalledTimes(0); + }); + it('unless chain is not supported', async () => { + isChainSupportedMock.mockResolvedValue(false); + await addTransaction({ ...request, securityAlertsEnabled: true, diff --git a/app/scripts/lib/transaction/util.ts b/app/scripts/lib/transaction/util.ts index d62ea2af02f3..8b71e33119f8 100644 --- a/app/scripts/lib/transaction/util.ts +++ b/app/scripts/lib/transaction/util.ts @@ -16,14 +16,15 @@ import { PPOMController } from '@metamask/ppom-validator'; import { generateSecurityAlertId, handlePPOMError, + isChainSupported, validateRequestWithPPOM, } from '../ppom/ppom-util'; import { SecurityAlertResponse } from '../ppom/types'; import { LOADING_SECURITY_ALERT_RESPONSE, SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES, - SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS, } from '../../../../shared/constants/security-provider'; +import { endTrace, TraceName } from '../../../../shared/lib/trace'; export type AddTransactionOptions = NonNullable< Parameters[1] @@ -43,6 +44,7 @@ type BaseAddTransactionRequest = { securityAlertResponse: SecurityAlertResponse, ) => void; userOperationController: UserOperationController; + internalAccounts: InternalAccount[]; }; type FinalAddTransactionRequest = BaseAddTransactionRequest & { @@ -64,7 +66,7 @@ export async function addDappTransaction( ): Promise { const { dappRequest } = request; const { id: actionId, method, origin } = dappRequest; - const { securityAlertResponse } = dappRequest; + const { securityAlertResponse, traceContext } = dappRequest; const transactionOptions: AddTransactionOptions = { actionId, @@ -75,18 +77,27 @@ export async function addDappTransaction( securityAlertResponse, }; + endTrace({ name: TraceName.Middleware, id: actionId }); + const { waitForHash } = await addTransactionOrUserOperation({ ...request, - transactionOptions, + transactionOptions: { + ...transactionOptions, + traceContext, + }, }); - return (await waitForHash()) as string; + const hash = (await waitForHash()) as string; + + endTrace({ name: TraceName.Transaction, id: actionId }); + + return hash; } export async function addTransaction( request: AddTransactionRequest, ): Promise { - validateSecurity(request); + await validateSecurity(request); const { transactionMeta, waitForHash } = await addTransactionOrUserOperation( request, @@ -215,7 +226,7 @@ function getTransactionByHash( ); } -function validateSecurity(request: AddTransactionRequest) { +async function validateSecurity(request: AddTransactionRequest) { const { chainId, ppomController, @@ -223,10 +234,13 @@ function validateSecurity(request: AddTransactionRequest) { transactionOptions, transactionParams, updateSecurityAlertResponse, + internalAccounts, } = request; const { type } = transactionOptions; + const isCurrentChainSupported = await isChainSupported(chainId); + const typeIsExcludedFromPPOM = SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES.includes( type as TransactionType, @@ -234,12 +248,21 @@ function validateSecurity(request: AddTransactionRequest) { if ( !securityAlertsEnabled || - !SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS.includes(chainId) || + !isCurrentChainSupported || typeIsExcludedFromPPOM ) { return; } + if ( + internalAccounts.some( + ({ address }) => + address.toLowerCase() === transactionParams.to?.toLowerCase(), + ) + ) { + return; + } + try { const { from, to, value, data } = transactionParams; const { actionId, origin } = transactionOptions; @@ -251,19 +274,22 @@ function validateSecurity(request: AddTransactionRequest) { params: [ { from, - to, - value, - data, + to: to ?? '', + value: value ?? '', + data: data ?? '', }, ], + jsonrpc: '2.0' as const, }; const securityAlertId = generateSecurityAlertId(); + // Intentionally not awaited to avoid blocking the confirmation process while the validation occurs. validateRequestWithPPOM({ ppomController, request: ppomRequest, securityAlertId, + chainId, }).then((securityAlertResponse) => { updateSecurityAlertResponse( ppomRequest.method, diff --git a/app/scripts/lib/tx-verification/tx-verification-middleware.test.ts b/app/scripts/lib/tx-verification/tx-verification-middleware.test.ts index 110a2dc3040e..ea0aea6216d0 100644 --- a/app/scripts/lib/tx-verification/tx-verification-middleware.test.ts +++ b/app/scripts/lib/tx-verification/tx-verification-middleware.test.ts @@ -4,13 +4,14 @@ import { EXPERIENCES_TYPE, FIRST_PARTY_CONTRACT_NAMES, } from '../../../../shared/constants/first-party-contracts'; +import { mockNetworkState } from '../../../../test/stub/networks'; import { createTxVerificationMiddleware, TxParams, } from './tx-verification-middleware'; const getMockNetworkController = (chainId: `0x${string}` = '0x1') => - ({ state: { providerConfig: { chainId } } } as unknown as NetworkController); + ({ state: mockNetworkState({ chainId }) } as NetworkController); const mockTrustedSigners: Partial> = { [EXPERIENCES_TYPE.METAMASK_BRIDGE]: diff --git a/app/scripts/lib/tx-verification/tx-verification-middleware.ts b/app/scripts/lib/tx-verification/tx-verification-middleware.ts index 30782a98721b..322e22902c93 100644 --- a/app/scripts/lib/tx-verification/tx-verification-middleware.ts +++ b/app/scripts/lib/tx-verification/tx-verification-middleware.ts @@ -22,6 +22,7 @@ import { TRUSTED_SIGNERS, } from '../../../../shared/constants/verification'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { getCurrentChainId } from '../../../../ui/selectors'; export type TxParams = { chainId?: `0x${string}`; @@ -62,7 +63,7 @@ export function createTxVerificationMiddleware( const chainId = typeof params.chainId === 'string' ? (params.chainId.toLowerCase() as Hex) - : networkController.state.providerConfig.chainId; + : getCurrentChainId({ metamask: networkController.state }); const experienceType = getExperience( params.to.toLowerCase() as Hex, diff --git a/app/scripts/lib/util.test.js b/app/scripts/lib/util.test.js index 1868007f7684..d428c3cd7ae1 100644 --- a/app/scripts/lib/util.test.js +++ b/app/scripts/lib/util.test.js @@ -14,6 +14,7 @@ import { PLATFORM_OPERA, } from '../../../shared/constants/app'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; +import * as FourBiteUtils from '../../../shared/lib/four-byte'; import { shouldEmitDappViewedEvent, addUrlProtocolPrefix, @@ -23,6 +24,7 @@ import { getPlatform, getValidUrl, isWebUrl, + getMethodDataName, } from './util'; describe('app utils', () => { @@ -357,4 +359,59 @@ describe('app utils', () => { expect(result).toStrictEqual(expectedResult); }); }); + + describe('getMethodDataName', () => { + const knownMethodData = { + '0x60806040': { + name: 'Approve Tokens', + }, + '0x095ea7b3': { + name: 'Approve Tokens', + }, + }; + it('return null if use4ByteResolution is not true', async () => { + expect( + await getMethodDataName(knownMethodData, false, '0x60806040'), + ).toStrictEqual(null); + }); + it('return null if prefixedData is not defined', async () => { + expect( + await getMethodDataName(knownMethodData, true, undefined), + ).toStrictEqual(null); + }); + it('return details from knownMethodData if defined', async () => { + expect( + await getMethodDataName(knownMethodData, true, '0x60806040'), + ).toStrictEqual(knownMethodData['0x60806040']); + }); + it('invoke getMethodDataAsync if details not available in knownMethodData', async () => { + const DUMMY_METHOD_NAME = { + name: 'Dummy Method Name', + }; + jest + .spyOn(FourBiteUtils, 'getMethodDataAsync') + .mockResolvedValue(DUMMY_METHOD_NAME); + expect( + await getMethodDataName(knownMethodData, true, '0x123'), + ).toStrictEqual(DUMMY_METHOD_NAME); + }); + it('invoke addKnownMethodData if details not available in knownMethodData', async () => { + const DUMMY_METHOD_NAME = { + name: 'Dummy Method Name', + }; + const addKnownMethodData = jest.fn(); + jest + .spyOn(FourBiteUtils, 'getMethodDataAsync') + .mockResolvedValue(DUMMY_METHOD_NAME); + expect( + await getMethodDataName( + knownMethodData, + true, + '0x123', + addKnownMethodData, + ), + ).toStrictEqual(DUMMY_METHOD_NAME); + expect(addKnownMethodData).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/app/scripts/lib/util.ts b/app/scripts/lib/util.ts index f44dc48628fa..2caade79b83e 100644 --- a/app/scripts/lib/util.ts +++ b/app/scripts/lib/util.ts @@ -19,6 +19,7 @@ import { } from '../../../shared/constants/app'; import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network'; import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; +import { getMethodDataAsync } from '../../../shared/lib/four-byte'; /** * @see {@link getEnvironmentType} @@ -382,3 +383,37 @@ export function formatValue( return includeParentheses ? `(${formattedNumber})` : formattedNumber; } + +type MethodData = { + name: string; + params: { type: string }[]; +}; + +export const getMethodDataName = async ( + knownMethodData: Record, + use4ByteResolution: boolean, + prefixedData: string, + addKnownMethodData: (fourBytePrefix: string, methodData: MethodData) => void, + provider: object, +) => { + if (!prefixedData || !use4ByteResolution) { + return null; + } + const fourBytePrefix = prefixedData.slice(0, 10); + + if (knownMethodData?.[fourBytePrefix]) { + return knownMethodData?.[fourBytePrefix]; + } + + const methodData = await getMethodDataAsync( + fourBytePrefix, + use4ByteResolution, + provider, + ); + + if (addKnownMethodData) { + addKnownMethodData(fourBytePrefix, methodData as MethodData); + } + + return methodData; +}; diff --git a/app/scripts/load-app.js b/app/scripts/load-app.js deleted file mode 100644 index 16dd34383c2a..000000000000 --- a/app/scripts/load-app.js +++ /dev/null @@ -1,28 +0,0 @@ -// eslint-disable-next-line import/unambiguous -'use strict'; - -setTimeout(() => { - // eslint-disable-next-line spaced-comment - const scriptsToLoad = [ - /* SCRIPTS */ - ]; - - const loadScript = (src) => { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.async = true; - script.onload = loadNext; - script.src = src; - document.body.appendChild(script); - }; - - loadNext(); - - function loadNext() { - if (scriptsToLoad.length) { - loadScript(scriptsToLoad.shift()); - } else { - document.documentElement.classList.add('metamask-loaded'); - } - } -}, 10); diff --git a/app/scripts/load/_initialize.ts b/app/scripts/load/_initialize.ts new file mode 100644 index 000000000000..5e0537876b73 --- /dev/null +++ b/app/scripts/load/_initialize.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +// currently only used in webpack build. + +// The root compartment will populate this with hooks +global.stateHooks = {} as typeof stateHooks; + +if (process.env.ENABLE_LAVAMOAT === 'true') { + // TODO: lavamoat support + throw new Error('LAVAMOAT not supported in webpack build yet'); +} else { + if (process.env.ENABLE_SENTRY === 'true') { + require('../sentry-install'); + } + if (process.env.ENABLE_SNOW === 'true') { + require('@lavamoat/snow/snow.prod'); + require('../use-snow'); + } + if (process.env.ENABLE_LOCKDOWN === 'true') { + require('../lockdown-install'); + require('../lockdown-run'); + require('../lockdown-more'); + } + + require('../init-globals'); + require('../runtime-cjs'); +} + +export {}; diff --git a/app/scripts/load/background.ts b/app/scripts/load/background.ts new file mode 100644 index 000000000000..1017d516e361 --- /dev/null +++ b/app/scripts/load/background.ts @@ -0,0 +1,9 @@ +// currently only used in webpack build. + +import './_initialize'; +import '../background'; + +if (process.env.IN_TEST) { + // only used for testing + document.documentElement.classList.add('metamask-loaded'); +} diff --git a/app/scripts/load/ui.ts b/app/scripts/load/ui.ts new file mode 100644 index 000000000000..471489bf948f --- /dev/null +++ b/app/scripts/load/ui.ts @@ -0,0 +1,9 @@ +// currently only used in webpack build. + +import './_initialize'; +import '../ui'; + +if (process.env.IN_TEST) { + // only used for testing + document.documentElement.classList.add('metamask-loaded'); +} diff --git a/app/scripts/lockdown-install.js b/app/scripts/lockdown-install.js new file mode 100644 index 000000000000..add418dea917 --- /dev/null +++ b/app/scripts/lockdown-install.js @@ -0,0 +1,6 @@ +// currently only used in webpack build. + +import 'ses'; +// lockdown() is called in lockdown-run.js + +export {}; diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index 33fa8c9baa8a..b5c4ef72dc63 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -89,10 +89,6 @@ describe('MetaMaskController', function () { blocklist: ['127.0.0.1'], name: ListNames.MetaMask, }, - phishfort_hotlist: { - blocklist: [], - name: ListNames.Phishfort, - }, }), ) .get(METAMASK_HOTLIST_DIFF_FILE) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 109a79eab6ff..b38574c10b96 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -82,6 +82,7 @@ import { SnapController, IframeExecutionService, SnapInterfaceController, + SnapInsightsController, OffscreenExecutionService, } from '@metamask/snaps-controllers'; import { @@ -142,6 +143,14 @@ import { import { Interface } from '@ethersproject/abi'; import { abiERC1155, abiERC721 } from '@metamask/metamask-eth-abis'; import { isEvmAccountType } from '@metamask/keyring-api'; +import { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; +import { + NotificationsServicesPushController, + NotificationServicesController, +} from '@metamask/notification-services-controller'; import { methodsRequiringNetworkSwitch, methodsWithConfirmation, @@ -160,7 +169,6 @@ import { import { CHAIN_IDS, NETWORK_TYPES, - TEST_NETWORK_TICKER_MAP, NetworkStatus, } from '../../shared/constants/network'; import { getAllowedSmartTransactionsChainIds } from '../../shared/constants/smartTransactions'; @@ -181,12 +189,12 @@ import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { MILLISECOND, SECOND } from '../../shared/constants/time'; import { ORIGIN_METAMASK, - SNAP_DIALOG_TYPES, POLLING_TOKEN_ENVIRONMENT_TYPES, } from '../../shared/constants/app'; import { MetaMetricsEventCategory, MetaMetricsEventName, + MetaMetricsUserTrait, } from '../../shared/constants/metametrics'; import { LOG_EVENT } from '../../shared/constants/logs'; @@ -203,9 +211,11 @@ import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { convertNetworkId } from '../../shared/modules/network.utils'; import { getIsSmartTransaction, + isHardwareWallet, getFeatureFlagsByChainId, getSmartTransactionsOptInStatus, getCurrentChainSupportsSmartTransactions, + getHardwareWalletType, } from '../../shared/modules/selectors'; import { createCaipStream } from '../../shared/modules/caip-stream'; import { BaseUrl } from '../../shared/constants/urls'; @@ -213,6 +223,9 @@ import { TOKEN_TRANSFER_LOG_TOPIC_HASH, TRANSFER_SINFLE_LOG_TOPIC_HASH, } from '../../shared/lib/transactions-controller-utils'; +import { getCurrentChainId } from '../../ui/selectors'; +import { getProviderConfig } from '../../ui/ducks/metamask/metamask'; +import { endTrace, trace } from '../../shared/lib/trace'; import { BalancesController as MultichainBalancesController } from './lib/accounts/BalancesController'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -280,7 +293,11 @@ import SwapsController from './controllers/swaps'; import MetaMetricsController from './controllers/metametrics'; import { segment } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; -import { previousValueComparator } from './lib/util'; +import { + addHexPrefix, + getMethodDataName, + previousValueComparator, +} from './lib/util'; import createMetamaskMiddleware from './lib/createMetamaskMiddleware'; import { hardwareKeyringBuilderFactory } from './lib/hardware-keyring-builder-factory'; import EncryptionPublicKeyController from './controllers/encryption-public-key'; @@ -313,14 +330,19 @@ import PREINSTALLED_SNAPS from './snaps/preinstalled-snaps'; import { WeakRefObjectMap } from './lib/WeakRefObjectMap'; // Notification controllers -import AuthenticationController from './controllers/authentication/authentication-controller'; -import UserStorageController from './controllers/user-storage/user-storage-controller'; -import { PushPlatformNotificationsController } from './controllers/push-platform-notifications/push-platform-notifications'; -import { MetamaskNotificationsController } from './controllers/metamask-notifications/metamask-notifications'; import { createTxVerificationMiddleware } from './lib/tx-verification/tx-verification-middleware'; import { updateSecurityAlertResponse } from './lib/ppom/ppom-util'; import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmMethodsToNonEvmAccountReqFilterMiddleware'; import { isEthAddress } from './lib/multichain/address'; +import { decodeTransactionData } from './lib/transaction/decode/util'; +import { BridgeBackgroundAction } from './controllers/bridge/types'; +import BridgeController from './controllers/bridge/bridge-controller'; +import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; +import { + onPushNotificationClicked, + onPushNotificationReceived, +} from './controllers/push-notifications'; +import createTracingMiddleware from './lib/createTracingMiddleware'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -330,9 +352,9 @@ export const METAMASK_CONTROLLER_EVENTS = { APPROVAL_STATE_CHANGE: 'ApprovalController:stateChange', QUEUED_REQUEST_STATE_CHANGE: 'QueuedRequestController:stateChange', METAMASK_NOTIFICATIONS_LIST_UPDATED: - 'MetamaskNotificationsController:notificationsListUpdated', + 'NotificationServicesController:notificationsListUpdated', METAMASK_NOTIFICATIONS_MARK_AS_READ: - 'MetamaskNotificationsController:markNotificationsAsRead', + 'NotificationServicesController:markNotificationsAsRead', NOTIFICATIONS_STATE_CHANGE: 'NotificationController:stateChange', }; @@ -359,7 +381,7 @@ export default class MetamaskController extends EventEmitter { this.platform = opts.platform; this.notificationManager = opts.notificationManager; const initState = opts.initState || {}; - const version = this.platform.getVersion(); + const version = process.env.METAMASK_VERSION; this.recordFirstTimeInfo(initState); this.featureFlags = opts.featureFlags; @@ -453,7 +475,6 @@ export default class MetamaskController extends EventEmitter { }), showApprovalRequest: opts.showUserConfirmation, typesExcludedFromRateLimiting: [ - ApprovalType.EthSign, ApprovalType.PersonalSign, ApprovalType.EthSignTypedData, ApprovalType.Transaction, @@ -487,14 +508,9 @@ export default class MetamaskController extends EventEmitter { id: 'networkConfigurationId', }; initialNetworkControllerState = { - providerConfig: { - ...networkConfig, - type: 'rpc', - }, + selectedNetworkClientId: networkConfig.id, networkConfigurations: { - networkConfigurationId: { - ...networkConfig, - }, + [networkConfig.id]: networkConfig, }, }; } else if ( @@ -502,11 +518,7 @@ export default class MetamaskController extends EventEmitter { process.env.METAMASK_ENVIRONMENT === 'test' ) { initialNetworkControllerState = { - providerConfig: { - type: NETWORK_TYPES.SEPOLIA, - chainId: CHAIN_IDS.SEPOLIA, - ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.SEPOLIA], - }, + selectedNetworkClientId: NETWORK_TYPES.SEPOLIA, }; } this.networkController = new NetworkController({ @@ -523,12 +535,6 @@ export default class MetamaskController extends EventEmitter { this.networkController.getProviderAndBlockTracker().blockTracker; this.deprecatedNetworkVersions = {}; - const tokenListMessenger = this.controllerMessenger.getRestricted({ - name: 'TokenListController', - allowedEvents: ['NetworkController:stateChange'], - allowedActions: ['NetworkController:getNetworkClientById'], - }); - const accountsControllerMessenger = this.controllerMessenger.getRestricted({ name: 'AccountsController', allowedEvents: [ @@ -566,8 +572,14 @@ export default class MetamaskController extends EventEmitter { networkConfigurations: this.networkController.state.networkConfigurations, }); + const tokenListMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenListController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: ['NetworkController:stateChange'], + }); + this.tokenListController = new TokenListController({ - chainId: this.networkController.state.providerConfig.chainId, + chainId: getCurrentChainId({ metamask: this.networkController.state }), preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( this.preferencesController.store.getState(), ), @@ -577,7 +589,7 @@ export default class MetamaskController extends EventEmitter { this.assetsContractController = new AssetsContractController( { - chainId: this.networkController.state.providerConfig.chainId, + chainId: getCurrentChainId({ metamask: this.networkController.state }), onPreferencesStateChange: (listener) => this.preferencesController.store.subscribe(listener), onNetworkDidChange: (cb) => @@ -603,49 +615,21 @@ export default class MetamaskController extends EventEmitter { allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getNetworkClientById', + 'AccountsController:getSelectedAccount', + 'AccountsController:getAccount', ], allowedEvents: [ 'NetworkController:networkDidChange', - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'PreferencesController:stateChange', 'TokenListController:stateChange', ], }); this.tokensController = new TokensController({ - messenger: tokensControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - // TODO: The tokens controller currently does not support internalAccounts. This is done to match the behavior of the previous tokens controller subscription. - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ selectedAddress: newlySelectedInternalAccount.address }); - }, - ), - onNetworkDidChange: (cb) => - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - () => { - const networkState = this.networkController.state; - return cb(networkState); - }, - ), - onTokenListStateChange: (listener) => - this.controllerMessenger.subscribe( - `${this.tokenListController.name}:stateChange`, - listener, - ), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - config: { - provider: this.provider, - selectedAddress: - initState.AccountsController?.internalAccounts?.accounts[ - initState.AccountsController?.internalAccounts?.selectedAccount - ]?.address ?? '', - }, state: initState.TokensController, + provider: this.provider, + messenger: tokensControllerMessenger, + chainId: getCurrentChainId({ metamask: this.networkController.state }), }); const nftControllerMessenger = this.controllerMessenger.getRestricted({ @@ -663,15 +647,9 @@ export default class MetamaskController extends EventEmitter { ], }); this.nftController = new NftController({ + state: initState.NftController, messenger: nftControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), + chainId: getCurrentChainId({ metamask: this.networkController.state }), getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( this.assetsContractController, ), @@ -705,10 +683,6 @@ export default class MetamaskController extends EventEmitter { source, }, }), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - state: initState.NftController, }); this.nftController.setApiKey(process.env.OPENSEA_KEY); @@ -717,36 +691,26 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'NftDetectionController', allowedEvents: [ - 'PreferencesController:stateChange', 'NetworkController:stateChange', + 'PreferencesController:stateChange', ], allowedActions: [ 'ApprovalController:addRequest', 'NetworkController:getState', 'NetworkController:getNetworkClientById', - 'PreferencesController:getState', 'AccountsController:getSelectedAccount', ], }); this.nftDetectionController = new NftDetectionController({ messenger: nftDetectionControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - onNftsStateChange: (listener) => this.nftController.subscribe(listener), - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), + chainId: getCurrentChainId({ metamask: this.networkController.state }), getOpenSeaApiKey: () => this.nftController.openSeaApiKey, getBalancesInSingleCall: this.assetsContractController.getBalancesInSingleCall.bind( this.assetsContractController, ), addNft: this.nftController.addNft.bind(this.nftController), - getNftApi: this.nftController.getNftApi.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] disabled: @@ -754,11 +718,6 @@ export default class MetamaskController extends EventEmitter { undefined ? false // the detection is enabled by default : !this.preferencesController.store.getState().useNftDetection, - selectedAddress: - this.preferencesController.store.getState().selectedAddress, - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), }); this.metaMetricsController = new MetaMetricsController({ @@ -769,12 +728,14 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:networkDidChange', ), getNetworkIdentifier: () => { - const { type, rpcUrl } = this.networkController.state.providerConfig; + const { type, rpcUrl } = getProviderConfig({ + metamask: this.networkController.state, + }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, - version: this.platform.getVersion(), + getCurrentChainId({ metamask: this.networkController.state }), + version: process.env.METAMASK_VERSION, environment: process.env.METAMASK_ENVIRONMENT, extension: this.extension, initState: initState.MetaMetricsController, @@ -821,10 +782,13 @@ export default class MetamaskController extends EventEmitter { legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, getCurrentNetworkLegacyGasAPICompatibility: () => { - const { chainId } = this.networkController.state.providerConfig; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); return chainId === CHAIN_IDS.BSC; }, - getChainId: () => this.networkController.state.providerConfig.chainId, + getChainId: () => + getCurrentChainId({ metamask: this.networkController.state }), }); this.appStateController = new AppStateController({ @@ -882,12 +846,16 @@ export default class MetamaskController extends EventEmitter { messenger: this.controllerMessenger.getRestricted({ name: 'PPOMController', allowedEvents: ['NetworkController:stateChange'], + allowedActions: ['NetworkController:getNetworkClientById'], }), storageBackend: new IndexedDBPPOMStorage('PPOMDB', 1), provider: this.provider, - ppomProvider: { PPOM: PPOMModule.PPOM, ppomInit: PPOMModule.default }, + ppomProvider: { + PPOM: PPOMModule.PPOM, + ppomInit: () => PPOMModule.default(process.env.PPOM_URI), + }, state: initState.PPOMController, - chainId: this.networkController.state.providerConfig.chainId, + chainId: getCurrentChainId({ metamask: this.networkController.state }), securityAlertsEnabled: this.preferencesController.store.getState().securityAlertsEnabled, onPreferencesChange: this.preferencesController.store.subscribe.bind( @@ -927,18 +895,19 @@ export default class MetamaskController extends EventEmitter { const multichainBalancesControllerMessenger = this.controllerMessenger.getRestricted({ name: 'BalancesController', - allowedEvents: ['AccountsController:stateChange'], - allowedActions: ['SnapController:handleRequest'], + allowedEvents: [ + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], + allowedActions: [ + 'AccountsController:listMultichainAccounts', + 'SnapController:handleRequest', + ], }); this.multichainBalancesController = new MultichainBalancesController({ messenger: multichainBalancesControllerMessenger, - state: {}, - // TODO: remove when listMultichainAccounts action is available - listMultichainAccounts: - this.accountsController.listMultichainAccounts.bind( - this.accountsController, - ), + state: initState.MultichainBalancesController, }); const multichainRatesControllerMessenger = @@ -952,55 +921,29 @@ export default class MetamaskController extends EventEmitter { fetchMultiExchangeRate, }); - const tokenRatesControllerMessenger = - this.controllerMessenger.getRestricted({ - name: 'TokenRatesController', - allowedEvents: [ - 'PreferencesController:stateChange', - 'TokensController:stateChange', - 'NetworkController:stateChange', - ], - allowedActions: [ - 'TokensController:getState', - 'NetworkController:getNetworkClientById', - 'NetworkController:getState', - 'PreferencesController:getState', - ], - }); + const tokenRatesMessenger = this.controllerMessenger.getRestricted({ + name: 'TokenRatesController', + allowedActions: [ + 'TokensController:getState', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + 'AccountsController:getAccount', + 'AccountsController:getSelectedAccount', + ], + allowedEvents: [ + 'NetworkController:stateChange', + 'AccountsController:selectedEvmAccountChange', + 'PreferencesController:stateChange', + 'TokensController:stateChange', + ], + }); // token exchange rate tracker - this.tokenRatesController = new TokenRatesController( - { - messenger: tokenRatesControllerMessenger, - chainId: this.networkController.state.providerConfig.chainId, - ticker: this.networkController.state.providerConfig.ticker, - selectedAddress: this.accountsController.getSelectedAccount().address, - onTokensStateChange: (listener) => - this.tokensController.subscribe(listener), - onNetworkStateChange: networkControllerMessenger.subscribe.bind( - networkControllerMessenger, - 'NetworkController:stateChange', - ), - onPreferencesStateChange: (listener) => - this.controllerMessenger.subscribe( - `AccountsController:selectedAccountChange`, - (newlySelectedInternalAccount) => { - listener({ - selectedAddress: newlySelectedInternalAccount.address, - }); - }, - ), - tokenPricesService: new CodefiTokenPricesServiceV2(), - getNetworkClientById: this.networkController.getNetworkClientById.bind( - this.networkController, - ), - }, - { - allTokens: this.tokensController.state.allTokens, - allDetectedTokens: this.tokensController.state.allDetectedTokens, - }, - initState.TokenRatesController, - ); + this.tokenRatesController = new TokenRatesController({ + state: initState.TokenRatesController, + messenger: tokenRatesMessenger, + tokenPricesService: new CodefiTokenPricesServiceV2(), + }); this.preferencesController.store.subscribe( previousValueComparator((prevState, currState) => { @@ -1017,6 +960,8 @@ export default class MetamaskController extends EventEmitter { this.ensController = new EnsController({ messenger: this.controllerMessenger.getRestricted({ name: 'EnsController', + allowedActions: ['NetworkController:getNetworkClientById'], + allowedEvents: [], }), provider: this.provider, onNetworkDidChange: networkControllerMessenger.subscribe.bind( @@ -1031,9 +976,9 @@ export default class MetamaskController extends EventEmitter { let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)]; - if (isManifestV3 === false) { - const keyringOverrides = this.opts.overrides?.keyrings; + const keyringOverrides = this.opts.overrides?.keyrings; + if (isManifestV3 === false) { const additionalKeyringTypes = [ keyringOverrides?.lattice || LatticeKeyring, QRHardwareKeyring, @@ -1064,8 +1009,14 @@ export default class MetamaskController extends EventEmitter { ); } else { additionalKeyrings.push( - hardwareKeyringBuilderFactory(TrezorKeyring, TrezorOffscreenBridge), - hardwareKeyringBuilderFactory(LedgerKeyring, LedgerOffscreenBridge), + hardwareKeyringBuilderFactory( + TrezorKeyring, + keyringOverrides?.trezorBridge || TrezorOffscreenBridge, + ), + hardwareKeyringBuilderFactory( + LedgerKeyring, + keyringOverrides?.ledgerBridge || LedgerOffscreenBridge, + ), keyringBuilderFactory(LatticeKeyringOffscreen), ); } @@ -1097,6 +1048,7 @@ export default class MetamaskController extends EventEmitter { 'KeyringController:getAccounts', 'AccountsController:setSelectedAccount', 'AccountsController:getAccountByAddress', + 'AccountsController:setAccountName', ], }); @@ -1142,7 +1094,6 @@ export default class MetamaskController extends EventEmitter { snapKeyringBuildMessenger, getSnapController, persistAndUpdateAccounts, - (address) => this.preferencesController.setSelectedAddress(address), (address) => this.removeAccount(address), this.metaMetricsController.trackEvent.bind(this.metaMetricsController), getSnapName, @@ -1466,6 +1417,8 @@ export default class MetamaskController extends EventEmitter { allowedActions: [ `${this.phishingController.name}:maybeUpdateState`, `${this.phishingController.name}:testOrigin`, + `${this.approvalController.name}:hasRequest`, + `${this.approvalController.name}:acceptRequest`, ], }); @@ -1474,26 +1427,47 @@ export default class MetamaskController extends EventEmitter { messenger: snapInterfaceControllerMessenger, }); + const snapInsightsControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'SnapInsightsController', + allowedActions: [ + `${this.snapController.name}:handleRequest`, + `${this.snapController.name}:getAll`, + `${this.permissionController.name}:getPermissions`, + `${this.snapInterfaceController.name}:deleteInterface`, + ], + allowedEvents: [ + `TransactionController:unapprovedTransactionAdded`, + `TransactionController:transactionStatusUpdated`, + `SignatureController:stateChange`, + ], + }); + + this.snapInsightsController = new SnapInsightsController({ + state: initState.SnapInsightsController, + messenger: snapInsightsControllerMessenger, + }); + // Notification Controllers - this.authenticationController = new AuthenticationController({ + this.authenticationController = new AuthenticationController.Controller({ state: initState.AuthenticationController, messenger: this.controllerMessenger.getRestricted({ name: 'AuthenticationController', allowedActions: [ 'KeyringController:getState', 'SnapController:handleRequest', - 'UserStorageController:disableProfileSyncing', ], allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], }), metametrics: { getMetaMetricsId: () => this.metaMetricsController.getMetaMetricsId(), + agent: 'extension', }, }); - this.userStorageController = new UserStorageController({ + this.userStorageController = new UserStorageController.Controller({ getMetaMetricsState: () => - this.metaMetricsController.state.participateInMetaMetrics, + this.metaMetricsController.state.participateInMetaMetrics ?? false, state: initState.UserStorageController, messenger: this.controllerMessenger.getRestricted({ name: 'UserStorageController', @@ -1505,25 +1479,42 @@ export default class MetamaskController extends EventEmitter { 'AuthenticationController:isSignedIn', 'AuthenticationController:performSignOut', 'AuthenticationController:performSignIn', - 'MetamaskNotificationsController:disableMetamaskNotifications', - 'MetamaskNotificationsController:selectIsMetamaskNotificationsEnabled', + 'NotificationServicesController:disableNotificationServices', + 'NotificationServicesController:selectIsNotificationServicesEnabled', ], allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'], }), }); - const pushPlatformNotificationsControllerMessenger = + const notificationServicesPushControllerMessenger = this.controllerMessenger.getRestricted({ - name: 'PushPlatformNotificationsController', + name: 'NotificationServicesPushController', allowedActions: ['AuthenticationController:getBearerToken'], + allowedEvents: [], }); - this.pushPlatformNotificationsController = - new PushPlatformNotificationsController({ - state: initState.PushPlatformNotificationsController, - messenger: pushPlatformNotificationsControllerMessenger, + this.notificationServicesPushController = + new NotificationsServicesPushController.Controller({ + messenger: notificationServicesPushControllerMessenger, + state: initState.NotificationsServicesPushController, + env: { + apiKey: process.env.FIREBASE_API_KEY ?? '', + authDomain: process.env.FIREBASE_AUTH_DOMAIN ?? '', + storageBucket: process.env.FIREBASE_STORAGE_BUCKET ?? '', + projectId: process.env.FIREBASE_PROJECT_ID ?? '', + messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID ?? '', + appId: process.env.FIREBASE_APP_ID ?? '', + measurementId: process.env.FIREBASE_MEASUREMENT_ID ?? '', + vapidKey: process.env.VAPID_KEY ?? '', + }, + config: { + isPushEnabled: isManifestV3, + platform: 'extension', + onPushNotificationReceived, + onPushNotificationClicked, + }, }); - pushPlatformNotificationsControllerMessenger.subscribe( - 'PushPlatformNotificationsController:onNewNotifications', + notificationServicesPushControllerMessenger.subscribe( + 'NotificationServicesPushController:onNewNotifications', (notification) => { this.metaMetricsController.trackEvent({ category: MetaMetricsEventCategory.PushNotifications, @@ -1536,8 +1527,8 @@ export default class MetamaskController extends EventEmitter { }); }, ); - pushPlatformNotificationsControllerMessenger.subscribe( - 'PushPlatformNotificationsController:pushNotificationClicked', + notificationServicesPushControllerMessenger.subscribe( + 'NotificationServicesPushController:pushNotificationClicked', (notification) => { this.metaMetricsController.trackEvent({ category: MetaMetricsEventCategory.PushNotifications, @@ -1553,41 +1544,53 @@ export default class MetamaskController extends EventEmitter { }, ); - this.metamaskNotificationsController = new MetamaskNotificationsController({ - messenger: this.controllerMessenger.getRestricted({ - name: 'MetamaskNotificationsController', - allowedActions: [ - 'KeyringController:getAccounts', - 'KeyringController:getState', - 'AuthenticationController:getBearerToken', - 'AuthenticationController:isSignedIn', - 'UserStorageController:enableProfileSyncing', - 'UserStorageController:getStorageKey', - 'UserStorageController:performGetStorage', - 'UserStorageController:performSetStorage', - 'PushPlatformNotificationsController:enablePushNotifications', - 'PushPlatformNotificationsController:disablePushNotifications', - 'PushPlatformNotificationsController:updateTriggerPushNotifications', - ], - allowedEvents: [ - 'KeyringController:stateChange', - 'KeyringController:lock', - 'KeyringController:unlock', - 'PushPlatformNotificationsController:onNewNotifications', - ], - }), - state: initState.MetamaskNotificationsController, - }); + this.notificationServicesController = + new NotificationServicesController.Controller({ + messenger: this.controllerMessenger.getRestricted({ + name: 'NotificationServicesController', + allowedActions: [ + 'KeyringController:getAccounts', + 'KeyringController:getState', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'UserStorageController:enableProfileSyncing', + 'UserStorageController:getStorageKey', + 'UserStorageController:performGetStorage', + 'UserStorageController:performSetStorage', + 'NotificationServicesPushController:enablePushNotifications', + 'NotificationServicesPushController:disablePushNotifications', + 'NotificationServicesPushController:updateTriggerPushNotifications', + ], + allowedEvents: [ + 'KeyringController:stateChange', + 'KeyringController:lock', + 'KeyringController:unlock', + 'NotificationServicesPushController:onNewNotifications', + ], + }), + state: initState.NotificationServicesController, + env: { + isPushIntegrated: isManifestV3, + featureAnnouncements: { + platform: 'extension', + spaceId: process.env.CONTENTFUL_ACCESS_SPACE_ID ?? '', + accessToken: process.env.CONTENTFUL_ACCESS_TOKEN ?? '', + }, + }, + }); // account tracker watches balances, nonces, and any code at their address this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, + getCurrentChainId({ metamask: this.networkController.state }), getNetworkIdentifier: (providerConfig) => { const { type, rpcUrl } = - providerConfig ?? this.networkController.state.providerConfig; + providerConfig ?? + getProviderConfig({ + metamask: this.networkController.state, + }); return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, preferencesController: this.preferencesController, @@ -1638,6 +1641,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.getRestricted({ name: 'TokenDetectionController', allowedActions: [ + 'AccountsController:getAccount', 'AccountsController:getSelectedAccount', 'KeyringController:getState', 'NetworkController:getNetworkClientById', @@ -1649,7 +1653,7 @@ export default class MetamaskController extends EventEmitter { 'TokensController:addDetectedTokens', ], allowedEvents: [ - 'AccountsController:selectedAccountChange', + 'AccountsController:selectedEvmAccountChange', 'KeyringController:lock', 'KeyringController:unlock', 'NetworkController:networkDidChange', @@ -1735,6 +1739,7 @@ export default class MetamaskController extends EventEmitter { `${this.approvalController.name}:addRequest`, 'NetworkController:findNetworkClientIdByChainId', 'NetworkController:getNetworkClientById', + 'AccountsController:getSelectedAccount', ], allowedEvents: [`NetworkController:stateChange`], }); @@ -1759,17 +1764,15 @@ export default class MetamaskController extends EventEmitter { getPermittedAccounts: this.getPermittedAccounts.bind(this), getSavedGasFees: () => this.preferencesController.store.getState().advancedGasFee[ - this.networkController.state.providerConfig.chainId + getCurrentChainId({ metamask: this.networkController.state }) ], - getSelectedAddress: () => - this.accountsController.getSelectedAccount().address, incomingTransactions: { includeTokenTransfers: false, isEnabled: () => Boolean( this.preferencesController.store.getState() .incomingTransactionsPreferences?.[ - this.networkController.state.providerConfig.chainId + getCurrentChainId({ metamask: this.networkController.state }) ] && this.onboardingController.store.getState().completedOnboarding, ), queryEntireHistory: false, @@ -1796,6 +1799,7 @@ export default class MetamaskController extends EventEmitter { }, provider: this.provider, testGasFeeFlows: process.env.TEST_GAS_FEE_FLOWS, + trace, hooks: { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) afterSign: (txMeta, signedEthTx) => @@ -1869,12 +1873,9 @@ export default class MetamaskController extends EventEmitter { `${this.loggingController.name}:add`, ], }), - isEthSignEnabled: () => - this.preferencesController.store.getState() - ?.disabledRpcMethodPreferences?.eth_sign, getAllState: this.getState.bind(this), getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, + getCurrentChainId({ metamask: this.networkController.state }), }); this.signatureController.hub.on( @@ -1953,8 +1954,27 @@ export default class MetamaskController extends EventEmitter { }); ///: END:ONLY_INCLUDE_IF + const swapsControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'SwapsController', + // TODO: allow these internal calls once GasFeeController and TransactionController + // export these action types and register its action handlers + // allowedActions: [ + // 'GasFeeController:getEIP1559GasFeeEstimates', + // 'TransactionController:getLayer1GasFee', + // ], + allowedActions: [ + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', + 'TokenRatesController:getState', + ], + allowedEvents: [], + }); + this.swapsController = new SwapsController( { + messenger: swapsControllerMessenger, + provider: this.provider, + // TODO: Remove once TransactionController exports this action type getBufferedGasLimit: async (txMeta, multiplier) => { const { gas: gasLimit, simulationFails } = await this.txController.estimateGasBuffered( @@ -1964,26 +1984,31 @@ export default class MetamaskController extends EventEmitter { return { gasLimit, simulationFails }; }, - provider: this.provider, - getProviderConfig: () => this.networkController.state.providerConfig, - getTokenRatesState: () => this.tokenRatesController.state, - getCurrentChainId: () => - this.networkController.state.providerConfig.chainId, + // TODO: Remove once GasFeeController exports this action type getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind( this.gasFeeController, ), + // TODO: Remove once TransactionController exports this action type getLayer1GasFee: this.txController.getLayer1GasFee.bind( this.txController, ), - getNetworkClientId: () => - this.networkController.state.selectedNetworkClientId, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }, initState.SwapsController, ); + + const bridgeControllerMessenger = this.controllerMessenger.getRestricted({ + name: BRIDGE_CONTROLLER_NAME, + allowedActions: [], + allowedEvents: [], + }); + this.bridgeController = new BridgeController({ + messenger: bridgeControllerMessenger, + }); + this.smartTransactionsController = new SmartTransactionsController( { getNetworkClientById: this.networkController.getNetworkClientById.bind( @@ -2003,6 +2028,20 @@ export default class MetamaskController extends EventEmitter { trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), + getMetaMetricsProps: async () => { + const selectedAddress = + this.accountsController.getSelectedAccount().address; + const accountHardwareType = await getHardwareWalletType( + this._getMetaMaskState(), + ); + const accountType = await this.getAccountType(selectedAddress); + const deviceModel = await this.getDeviceModel(selectedAddress); + return { + accountHardwareType, + accountType, + deviceModel, + }; + }, }, { supportedChainIds: getAllowedSmartTransactionsChainIds(), @@ -2133,9 +2172,6 @@ export default class MetamaskController extends EventEmitter { ), // msg signing ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - processEthSignMessage: this.signatureController.newUnsignedMessage.bind( - this.signatureController, - ), processTypedMessage: this.signatureController.newUnsignedTypedMessage.bind( this.signatureController, @@ -2156,9 +2192,6 @@ export default class MetamaskController extends EventEmitter { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) /* eslint-disable no-dupe-keys */ - processEthSignMessage: this.mmiController.newUnsignedMessage.bind( - this.mmiController, - ), processTypedMessage: this.mmiController.newUnsignedMessage.bind( this.mmiController, ), @@ -2212,7 +2245,8 @@ export default class MetamaskController extends EventEmitter { DecryptMessageController: this.decryptMessageController, EncryptionPublicKeyController: this.encryptionPublicKeyController, SignatureController: this.signatureController, - SwapsController: this.swapsController.store, + SwapsController: this.swapsController, + BridgeController: this.bridgeController, EnsController: this.ensController, ApprovalController: this.approvalController, PPOMController: this.ppomController, @@ -2252,6 +2286,7 @@ export default class MetamaskController extends EventEmitter { SnapsRegistry: this.snapsRegistry, NotificationController: this.notificationController, SnapInterfaceController: this.snapInterfaceController, + SnapInsightsController: this.snapInsightsController, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) CustodyController: this.custodyController.store, InstitutionalFeaturesController: @@ -2264,9 +2299,9 @@ export default class MetamaskController extends EventEmitter { // Notification Controllers AuthenticationController: this.authenticationController, UserStorageController: this.userStorageController, - MetamaskNotificationsController: this.metamaskNotificationsController, - PushPlatformNotificationsController: - this.pushPlatformNotificationsController, + NotificationServicesController: this.notificationServicesController, + NotificationServicesPushController: + this.notificationServicesPushController, ...resetOnRestartStore, }); @@ -2304,6 +2339,7 @@ export default class MetamaskController extends EventEmitter { SnapsRegistry: this.snapsRegistry, NotificationController: this.notificationController, SnapInterfaceController: this.snapInterfaceController, + SnapInsightsController: this.snapInsightsController, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) CustodyController: this.custodyController.store, InstitutionalFeaturesController: @@ -2315,10 +2351,10 @@ export default class MetamaskController extends EventEmitter { // Notification Controllers AuthenticationController: this.authenticationController, UserStorageController: this.userStorageController, - MetamaskNotificationsController: this.metamaskNotificationsController, + NotificationServicesController: this.notificationServicesController, QueuedRequestController: this.queuedRequestController, - PushPlatformNotificationsController: - this.pushPlatformNotificationsController, + NotificationServicesPushController: + this.notificationServicesPushController, ...resetOnRestartStore, }, controllerMessenger: this.controllerMessenger, @@ -2334,7 +2370,8 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController, ), this.signatureController.resetState.bind(this.signatureController), - this.swapsController.resetState, + this.swapsController.resetState.bind(this.swapsController), + this.bridgeController.resetState.bind(this.bridgeController), this.ensController.resetState.bind(this.ensController), this.approvalController.clear.bind(this.approvalController), // WE SHOULD ADD TokenListController.resetState here too. But it's not implemented yet. @@ -2402,6 +2439,26 @@ export default class MetamaskController extends EventEmitter { if (usePhishDetect) { this.phishingController.maybeUpdateState(); } + + // post onboarding emit detectTokens event + const preferencesControllerState = + this.preferencesController.store.getState(); + const { useTokenDetection, useNftDetection } = + preferencesControllerState ?? {}; + this.metaMetricsController.trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsUserTrait.TokenDetectionEnabled, + properties: { + [MetaMetricsUserTrait.TokenDetectionEnabled]: useTokenDetection, + }, + }); + this.metaMetricsController.trackEvent({ + category: MetaMetricsEventCategory.Onboarding, + event: MetaMetricsUserTrait.NftAutodetectionEnabled, + properties: { + [MetaMetricsUserTrait.NftAutodetectionEnabled]: useNftDetection, + }, + }); } triggerNetworkrequests() { @@ -2567,7 +2624,11 @@ export default class MetamaskController extends EventEmitter { ...buildSnapRestrictedMethodSpecifications( Object.keys(ExcludedSnapPermissions), { - getLocale: this.getLocale.bind(this), + getPreferences: () => { + const locale = this.getLocale(); + const currency = this.currencyRateController.state.currentCurrency; + return { locale, currency }; + }, clearSnapState: this.controllerMessenger.call.bind( this.controllerMessenger, 'SnapController:clearSnapState', @@ -2585,12 +2646,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getSnapState', ), - showDialog: (origin, type, id, placeholder) => - this.approvalController.addAndShowApprovalRequest({ - origin, - type: SNAP_DIALOG_TYPES[type], - requestData: { id, placeholder }, - }), + requestUserApproval: + this.approvalController.addAndShowApprovalRequest.bind( + this.approvalController, + ), showNativeNotification: (origin, args) => this.controllerMessenger.call( 'RateLimitController:call', @@ -2623,7 +2682,7 @@ export default class MetamaskController extends EventEmitter { 'PhishingController:maybeUpdateState', ); }, - isOnPhishingList: (origin) => { + isOnPhishingList: (sender) => { const { usePhishDetect } = this.preferencesController.store.getState(); @@ -2633,7 +2692,7 @@ export default class MetamaskController extends EventEmitter { return this.controllerMessenger.call( 'PhishingController:testOrigin', - origin, + sender.url, ).result; }, createInterface: this.controllerMessenger.call.bind( @@ -3030,10 +3089,8 @@ export default class MetamaskController extends EventEmitter { networkController, announcementController, onboardingController, - appMetadataController, permissionController, preferencesController, - swapsController, tokensController, smartTransactionsController, txController, @@ -3044,8 +3101,8 @@ export default class MetamaskController extends EventEmitter { // Notification Controllers authenticationController, userStorageController, - metamaskNotificationsController, - pushPlatformNotificationsController, + notificationServicesController, + notificationServicesPushController, } = this; return { @@ -3067,10 +3124,6 @@ export default class MetamaskController extends EventEmitter { preferencesController.setUseMultiAccountBalanceChecker.bind( preferencesController, ), - dismissOpenSeaToBlockaidBanner: - preferencesController.dismissOpenSeaToBlockaidBanner.bind( - preferencesController, - ), setUseSafeChainsListValidation: preferencesController.setUseSafeChainsListValidation.bind( preferencesController, @@ -3094,7 +3147,10 @@ export default class MetamaskController extends EventEmitter { getUseRequestQueue: this.preferencesController.getUseRequestQueue.bind( this.preferencesController, ), - getProviderConfig: () => this.networkController.state.providerConfig, + getProviderConfig: () => + getProviderConfig({ + metamask: this.networkController.state, + }), setSecurityAlertsEnabled: preferencesController.setSecurityAlertsEnabled.bind( preferencesController, @@ -3105,6 +3161,18 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), ///: END:ONLY_INCLUDE_IF + setWatchEthereumAccountEnabled: + preferencesController.setWatchEthereumAccountEnabled.bind( + preferencesController, + ), + setBitcoinSupportEnabled: + preferencesController.setBitcoinSupportEnabled.bind( + preferencesController, + ), + setBitcoinTestnetSupportEnabled: + preferencesController.setBitcoinTestnetSupportEnabled.bind( + preferencesController, + ), setUseExternalNameSources: preferencesController.setUseExternalNameSources.bind( preferencesController, @@ -3192,9 +3260,6 @@ export default class MetamaskController extends EventEmitter { verifyPassword: this.verifyPassword.bind(this), // network management - setProviderType: (type) => { - return this.networkController.setProviderType(type); - }, setActiveNetwork: (networkConfigurationId) => { return this.networkController.setActiveNetwork(networkConfigurationId); }, @@ -3250,14 +3315,6 @@ export default class MetamaskController extends EventEmitter { preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), - setDisabledRpcMethodPreference: - preferencesController.setDisabledRpcMethodPreference.bind( - preferencesController, - ), - getRpcMethodPreferences: - preferencesController.getRpcMethodPreferences.bind( - preferencesController, - ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), @@ -3281,7 +3338,6 @@ export default class MetamaskController extends EventEmitter { accountsController.setAccountName.bind(accountsController), setAccountLabel: (address, label) => { - this.preferencesController.setAccountLabel(address, label); const account = this.accountsController.getAccountByAddress(address); if (account === undefined) { throw new Error(`No account found for address: ${address}`); @@ -3386,6 +3442,14 @@ export default class MetamaskController extends EventEmitter { appStateController.setSwitchedNetworkNeverShowMessage.bind( appStateController, ), + getLastInteractedConfirmationInfo: + appStateController.getLastInteractedConfirmationInfo.bind( + appStateController, + ), + setLastInteractedConfirmationInfo: + appStateController.setLastInteractedConfirmationInfo.bind( + appStateController, + ), // EnsController tryReverseResolveAddress: @@ -3468,12 +3532,6 @@ export default class MetamaskController extends EventEmitter { this.encryptionPublicKeyController, ), - // AppMetadataController - setShowTokenAutodetectModalOnUpgrade: - appMetadataController.setShowTokenAutodetectModalOnUpgrade.bind( - appMetadataController, - ), - // onboarding controller setSeedPhraseBackedUp: onboardingController.setSeedPhraseBackedUp.bind(onboardingController), @@ -3503,10 +3561,6 @@ export default class MetamaskController extends EventEmitter { getCustodianAccounts: this.mmiController.getCustodianAccounts.bind( this.mmiController, ), - getCustodianAccountsByAddress: - this.mmiController.getCustodianAccountsByAddress.bind( - this.mmiController, - ), getCustodianTransactionDeepLink: this.mmiController.getCustodianTransactionDeepLink.bind( this.mmiController, @@ -3605,43 +3659,101 @@ export default class MetamaskController extends EventEmitter { ), // swaps - fetchAndSetQuotes: - swapsController.fetchAndSetQuotes.bind(swapsController), - setSelectedQuoteAggId: - swapsController.setSelectedQuoteAggId.bind(swapsController), - resetSwapsState: swapsController.resetSwapsState.bind(swapsController), - setSwapsTokens: swapsController.setSwapsTokens.bind(swapsController), - clearSwapsQuotes: swapsController.clearSwapsQuotes.bind(swapsController), - setApproveTxId: swapsController.setApproveTxId.bind(swapsController), - setTradeTxId: swapsController.setTradeTxId.bind(swapsController), - setSwapsTxGasPrice: - swapsController.setSwapsTxGasPrice.bind(swapsController), - setSwapsTxGasLimit: - swapsController.setSwapsTxGasLimit.bind(swapsController), - setSwapsTxMaxFeePerGas: - swapsController.setSwapsTxMaxFeePerGas.bind(swapsController), - setSwapsTxMaxFeePriorityPerGas: - swapsController.setSwapsTxMaxFeePriorityPerGas.bind(swapsController), - safeRefetchQuotes: - swapsController.safeRefetchQuotes.bind(swapsController), - stopPollingForQuotes: - swapsController.stopPollingForQuotes.bind(swapsController), - setBackgroundSwapRouteState: - swapsController.setBackgroundSwapRouteState.bind(swapsController), - resetPostFetchState: - swapsController.resetPostFetchState.bind(swapsController), - setSwapsErrorKey: swapsController.setSwapsErrorKey.bind(swapsController), - setInitialGasEstimate: - swapsController.setInitialGasEstimate.bind(swapsController), - setCustomApproveTxData: - swapsController.setCustomApproveTxData.bind(swapsController), - setSwapsLiveness: swapsController.setSwapsLiveness.bind(swapsController), - setSwapsFeatureFlags: - swapsController.setSwapsFeatureFlags.bind(swapsController), - setSwapsUserFeeLevel: - swapsController.setSwapsUserFeeLevel.bind(swapsController), - setSwapsQuotesPollingLimitEnabled: - swapsController.setSwapsQuotesPollingLimitEnabled.bind(swapsController), + fetchAndSetQuotes: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:fetchAndSetQuotes', + ), + setSelectedQuoteAggId: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSelectedQuoteAggId', + ), + resetSwapsState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:resetSwapsState', + ), + setSwapsTokens: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsTokens', + ), + clearSwapsQuotes: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:clearSwapsQuotes', + ), + setApproveTxId: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setApproveTxId', + ), + setTradeTxId: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setTradeTxId', + ), + setSwapsTxGasPrice: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsTxGasPrice', + ), + setSwapsTxGasLimit: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsTxGasLimit', + ), + setSwapsTxMaxFeePerGas: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsTxMaxFeePerGas', + ), + setSwapsTxMaxFeePriorityPerGas: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsTxMaxFeePriorityPerGas', + ), + safeRefetchQuotes: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:safeRefetchQuotes', + ), + stopPollingForQuotes: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:stopPollingForQuotes', + ), + setBackgroundSwapRouteState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setBackgroundSwapRouteState', + ), + resetPostFetchState: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:resetPostFetchState', + ), + setSwapsErrorKey: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsErrorKey', + ), + setInitialGasEstimate: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setInitialGasEstimate', + ), + setCustomApproveTxData: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setCustomApproveTxData', + ), + setSwapsLiveness: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsLiveness', + ), + setSwapsFeatureFlags: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsFeatureFlags', + ), + setSwapsUserFeeLevel: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsUserFeeLevel', + ), + setSwapsQuotesPollingLimitEnabled: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SwapsController:setSwapsQuotesPollingLimitEnabled', + ), + + // Bridge + [BridgeBackgroundAction.SET_FEATURE_FLAGS]: + this.controllerMessenger.call.bind( + this.controllerMessenger, + `${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.SET_FEATURE_FLAGS}`, + ), // Smart Transactions fetchSmartTransactionFees: smartTransactionsController.getFees.bind( @@ -3770,63 +3882,81 @@ export default class MetamaskController extends EventEmitter { userStorageController, ), - // MetamaskNotificationsController + // NotificationServicesController checkAccountsPresence: - metamaskNotificationsController.checkAccountsPresence.bind( - metamaskNotificationsController, + notificationServicesController.checkAccountsPresence.bind( + notificationServicesController, ), createOnChainTriggers: - metamaskNotificationsController.createOnChainTriggers.bind( - metamaskNotificationsController, + notificationServicesController.createOnChainTriggers.bind( + notificationServicesController, ), deleteOnChainTriggersByAccount: - metamaskNotificationsController.deleteOnChainTriggersByAccount.bind( - metamaskNotificationsController, + notificationServicesController.deleteOnChainTriggersByAccount.bind( + notificationServicesController, ), updateOnChainTriggersByAccount: - metamaskNotificationsController.updateOnChainTriggersByAccount.bind( - metamaskNotificationsController, + notificationServicesController.updateOnChainTriggersByAccount.bind( + notificationServicesController, ), fetchAndUpdateMetamaskNotifications: - metamaskNotificationsController.fetchAndUpdateMetamaskNotifications.bind( - metamaskNotificationsController, + notificationServicesController.fetchAndUpdateMetamaskNotifications.bind( + notificationServicesController, ), markMetamaskNotificationsAsRead: - metamaskNotificationsController.markMetamaskNotificationsAsRead.bind( - metamaskNotificationsController, + notificationServicesController.markMetamaskNotificationsAsRead.bind( + notificationServicesController, ), setFeatureAnnouncementsEnabled: - metamaskNotificationsController.setFeatureAnnouncementsEnabled.bind( - metamaskNotificationsController, + notificationServicesController.setFeatureAnnouncementsEnabled.bind( + notificationServicesController, ), enablePushNotifications: - pushPlatformNotificationsController.enablePushNotifications.bind( - pushPlatformNotificationsController, + notificationServicesPushController.enablePushNotifications.bind( + notificationServicesPushController, ), disablePushNotifications: - pushPlatformNotificationsController.disablePushNotifications.bind( - pushPlatformNotificationsController, + notificationServicesPushController.disablePushNotifications.bind( + notificationServicesPushController, ), updateTriggerPushNotifications: - pushPlatformNotificationsController.updateTriggerPushNotifications.bind( - pushPlatformNotificationsController, + notificationServicesPushController.updateTriggerPushNotifications.bind( + notificationServicesPushController, ), enableMetamaskNotifications: - metamaskNotificationsController.enableMetamaskNotifications.bind( - metamaskNotificationsController, + notificationServicesController.enableMetamaskNotifications.bind( + notificationServicesController, ), disableMetamaskNotifications: - metamaskNotificationsController.disableMetamaskNotifications.bind( - metamaskNotificationsController, + notificationServicesController.disableNotificationServices.bind( + notificationServicesController, ), // E2E testing throwTestError: this.throwTestError.bind(this), + // NameController updateProposedNames: this.nameController.updateProposedNames.bind( this.nameController, ), setName: this.nameController.setName.bind(this.nameController), + + // MultichainBalancesController + multichainUpdateBalance: (accountId) => + this.multichainBalancesController.updateBalance(accountId), + + multichainUpdateBalances: () => + this.multichainBalancesController.updateBalances(), + + // Transaction Decode + decodeTransactionData: (request) => + decodeTransactionData({ + ...request, + ethQuery: new EthQuery(this.provider), + }), + + // Trace + endTrace, }; } @@ -4028,7 +4158,9 @@ export default class MetamaskController extends EventEmitter { async _addAccountsWithBalance() { // Scan accounts until we find an empty one - const { chainId } = this.networkController.state.providerConfig; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); const ethQuery = new EthQuery(this.provider); const accounts = await this.keyringController.getAccounts(); let address = accounts[accounts.length - 1]; @@ -4289,7 +4421,9 @@ export default class MetamaskController extends EventEmitter { this.appStateController.setTrezorModel(model); } - keyring.network = this.networkController.state.providerConfig.type; + keyring.network = getProviderConfig({ + metamask: this.networkController.state, + }).type; return keyring; } @@ -4566,10 +4700,11 @@ export default class MetamaskController extends EventEmitter { /** * Stops exposing the specified chain ID to all third parties. - * Exposed chain IDs are stored in caveats of the permittedChains permission. This - * method uses `PermissionController.updatePermissionsByCaveat` to - * remove the specified chain ID from every permittedChains permission. If a - * permission only included this chain ID, the permission is revoked entirely. + * Exposed chain IDs are stored in caveats of the `endowment:permitted-chains` + * permission. This method uses `PermissionController.updatePermissionsByCaveat` + * to remove the specified chain ID from every `endowment:permitted-chains` + * permission. If a permission only included this chain ID, the permission is + * revoked entirely. * * @param {string} targetChainId - The chain ID to stop exposing * to third parties. @@ -4674,6 +4809,7 @@ export default class MetamaskController extends EventEmitter { dappRequest, }) { return { + internalAccounts: this.accountsController.listAccounts(), dappRequest, networkClientId: dappRequest?.networkClientId ?? @@ -4685,7 +4821,7 @@ export default class MetamaskController extends EventEmitter { transactionOptions, transactionParams, userOperationController: this.userOperationController, - chainId: this.networkController.state.providerConfig.chainId, + chainId: getCurrentChainId({ metamask: this.networkController.state }), ppomController: this.ppomController, securityAlertsEnabled: this.preferencesController.store.getState()?.securityAlertsEnabled, @@ -4857,8 +4993,27 @@ export default class MetamaskController extends EventEmitter { sender, subjectType, }) { - const { completedOnboarding } = this.onboardingController.store.getState(); - const { usePhishDetect } = this.preferencesController.store.getState(); + if (sender.url) { + if (this.onboardingController.store.getState().completedOnboarding) { + if (this.preferencesController.store.getState().usePhishDetect) { + const { hostname } = new URL(sender.url); + this.phishingController.maybeUpdateState(); + // Check if new connection is blocked if phishing detection is on + const phishingTestResponse = this.phishingController.test(sender.url); + if (phishingTestResponse?.result) { + this.sendPhishingWarning(connectionStream, hostname); + this.metaMetricsController.trackEvent({ + event: MetaMetricsEventName.PhishingPageDisplayed, + category: MetaMetricsEventCategory.Phishing, + properties: { + url: hostname, + }, + }); + return; + } + } + } + } let inputSubjectType; if (subjectType) { @@ -4869,24 +5024,6 @@ export default class MetamaskController extends EventEmitter { inputSubjectType = SubjectType.Website; } - if (usePhishDetect && completedOnboarding && sender.url) { - const { hostname } = new URL(sender.url); - this.phishingController.maybeUpdateState(); - // Check if new connection is blocked if phishing detection is on - const phishingTestResponse = this.phishingController.test(hostname); - if (phishingTestResponse?.result) { - this.sendPhishingWarning(connectionStream, hostname); - this.metaMetricsController.trackEvent({ - event: MetaMetricsEventName.PhishingPageDisplayed, - category: MetaMetricsEventCategory.Phishing, - properties: { - url: hostname, - }, - }); - return; - } - } - // setup multiplexing const mux = setupMultiplex(connectionStream); @@ -5294,12 +5431,15 @@ export default class MetamaskController extends EventEmitter { engine.push(createTxVerificationMiddleware(this.networkController)); } + engine.push(createTracingMiddleware()); + engine.push( createPPOMMiddleware( this.ppomController, this.preferencesController, this.networkController, this.appStateController, + this.accountsController, this.updateSecurityAlertResponse.bind(this), ), ); @@ -5419,7 +5559,7 @@ export default class MetamaskController extends EventEmitter { { eth_accounts: {} }, ), requestPermittedChainsPermission: (chainIds) => - this.permissionController.requestPermissions( + this.permissionController.requestPermissionsIncremental( { origin }, { [PermissionNames.permittedChains]: { @@ -5471,7 +5611,9 @@ export default class MetamaskController extends EventEmitter { getChainPermissionsFeatureFlag: () => Boolean(process.env.CHAIN_PERMISSIONS), getCurrentRpcUrl: () => - this.networkController.state.providerConfig.rpcUrl, + getProviderConfig({ + metamask: this.networkController.state, + }).rpcUrl, // network configuration-related upsertNetworkConfiguration: this.networkController.upsertNetworkConfiguration.bind( @@ -5572,9 +5714,25 @@ export default class MetamaskController extends EventEmitter { getIsLocked: () => { return !this.appStateController.isUnlocked(); }, - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) - hasPermission: this.permissionController.hasPermission.bind( - this.permissionController, + getInterfaceState: (...args) => + this.controllerMessenger.call( + 'SnapInterfaceController:getInterface', + origin, + ...args, + ).state, + createInterface: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapInterfaceController:createInterface', + origin, + ), + updateInterface: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapInterfaceController:updateInterface', + origin, + ), + resolveInterface: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapInterfaceController:resolveInterface', origin, ), getSnap: this.controllerMessenger.call.bind( @@ -5585,28 +5743,17 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:getAll', ), + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + hasPermission: this.permissionController.hasPermission.bind( + this.permissionController, + origin, + ), handleSnapRpcRequest: (args) => this.handleSnapRequest({ ...args, origin }), getAllowedKeyringMethods: keyringSnapPermissionsBuilder( this.subjectMetadataController, origin, ), - createInterface: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapInterfaceController:createInterface', - origin, - ), - getInterfaceState: (...args) => - this.controllerMessenger.call( - 'SnapInterfaceController:getInterface', - origin, - ...args, - ).state, - updateInterface: this.controllerMessenger.call.bind( - this.controllerMessenger, - 'SnapInterfaceController:updateInterface', - origin, - ), ///: END:ONLY_INCLUDE_IF }), ); @@ -6001,13 +6148,23 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.subscribe( 'TransactionController:transactionNewSwap', ({ transactionMeta }) => - this.swapsController.setTradeTxId(transactionMeta.id), + // TODO: This can be called internally by the TransactionController + // since Swaps Controller registers this action handler + this.controllerMessenger.call( + 'SwapsController:setTradeTxId', + transactionMeta.id, + ), ); this.controllerMessenger.subscribe( 'TransactionController:transactionNewSwapApproval', ({ transactionMeta }) => - this.swapsController.setApproveTxId(transactionMeta.id), + // TODO: This can be called internally by the TransactionController + // since Swaps Controller registers this action handler + this.controllerMessenger.call( + 'SwapsController:setApproveTxId', + transactionMeta.id, + ), ); this.controllerMessenger.subscribe( @@ -6068,6 +6225,37 @@ export default class MetamaskController extends EventEmitter { txHash, ); }, + getRedesignedConfirmationsEnabled: () => { + return this.preferencesController.getRedesignedConfirmationsEnabled; + }, + getRedesignedTransactionsEnabled: () => { + return this.preferencesController.getRedesignedTransactionsEnabled; + }, + getMethodData: (data) => { + if (!data) { + return null; + } + const { knownMethodData, use4ByteResolution } = + this.preferencesController.store.getState(); + const prefixedData = addHexPrefix(data); + return getMethodDataName( + knownMethodData, + use4ByteResolution, + prefixedData, + this.preferencesController.addKnownMethodData.bind( + this.preferencesController, + ), + this.provider, + ); + }, + getIsRedesignedConfirmationsDeveloperEnabled: () => { + return this.preferencesController.store.getState().preferences + .isRedesignedConfirmationsDeveloperEnabled; + }, + getIsConfirmationAdvancedDetailsOpen: () => { + return this.preferencesController.store.getState().preferences + .showConfirmationAdvancedDetails; + }, }; return { ...controllerActions, @@ -6149,7 +6337,7 @@ export default class MetamaskController extends EventEmitter { */ recordFirstTimeInfo(initState) { if (!('firstTimeInfo' in initState)) { - const version = this.platform.getVersion(); + const version = process.env.METAMASK_VERSION; initState.firstTimeInfo = { version, date: Date.now(), @@ -6653,7 +6841,7 @@ export default class MetamaskController extends EventEmitter { ); } - _publishSmartTransactionHook(transactionMeta) { + _publishSmartTransactionHook(transactionMeta, signedTransactionInHex) { const state = this._getMetaMaskState(); const isSmartTransaction = getIsSmartTransaction(state); if (!isSmartTransaction) { @@ -6663,10 +6851,12 @@ export default class MetamaskController extends EventEmitter { const featureFlags = getFeatureFlagsByChainId(state); return submitSmartTransactionHook({ transactionMeta, + signedTransactionInHex, transactionController: this.txController, smartTransactionsController: this.smartTransactionsController, controllerMessenger: this.controllerMessenger, isSmartTransaction, + isHardwareWallet: isHardwareWallet(state), featureFlags, }); } @@ -6679,7 +6869,9 @@ export default class MetamaskController extends EventEmitter { async #onPreferencesControllerStateChange(currentState, previousState) { const { currentLocale } = currentState; - const { chainId } = this.networkController.state.providerConfig; + const chainId = getCurrentChainId({ + metamask: this.networkController.state, + }); await updateCurrentLocale(currentLocale); diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index e23489cd41cf..bae372187f14 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -19,7 +19,6 @@ import { BtcMethod, EthAccountType, } from '@metamask/keyring-api'; -import { NetworkType } from '@metamask/controller-utils'; import { ControllerMessenger } from '@metamask/base-controller'; import { LoggingController, LogType } from '@metamask/logging-controller'; import { TransactionController } from '@metamask/transaction-controller'; @@ -27,10 +26,9 @@ import { RatesController, TokenListController, } from '@metamask/assets-controllers'; +import ObjectMultiplex from '@metamask/object-multiplex'; import { TrezorKeyring } from '@metamask/eth-trezor-keyring'; import { LedgerKeyring } from '@metamask/eth-ledger-bridge-keyring'; -import ObjectMultiplex from '@metamask/object-multiplex'; -import { NETWORK_TYPES } from '../../shared/constants/network'; import { createTestProviderTools } from '../../test/stub/provider'; import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; import { KeyringType } from '../../shared/constants/keyring'; @@ -40,10 +38,12 @@ import * as tokenUtils from '../../shared/lib/token-util'; import { flushPromises } from '../../test/lib/timer-helpers'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { createMockInternalAccount } from '../../test/jest/mocks'; +import { mockNetworkState } from '../../test/stub/networks'; import { BalancesController as MultichainBalancesController, - BTC_AVG_BLOCK_TIME, + BALANCES_UPDATE_TIME as MULTICHAIN_BALANCES_UPDATE_TIME, } from './lib/accounts/BalancesController'; +import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker'; import { deferredPromise } from './lib/util'; import MetaMaskController from './metamask-controller'; @@ -244,49 +244,32 @@ const firstTimeState = { }, }, NetworkController: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: ALT_MAINNET_RPC_URL, - chainId: MAINNET_CHAIN_ID, - ticker: ETH, - nickname: 'Alt Mainnet', - id: NETWORK_CONFIGURATION_ID_1, - }, - networkConfigurations: { - [NETWORK_CONFIGURATION_ID_1]: { + ...mockNetworkState( + { rpcUrl: ALT_MAINNET_RPC_URL, - type: NETWORK_TYPES.RPC, chainId: MAINNET_CHAIN_ID, ticker: ETH, nickname: 'Alt Mainnet', id: NETWORK_CONFIGURATION_ID_1, + blockExplorerUrl: undefined, }, - [NETWORK_CONFIGURATION_ID_2]: { + { rpcUrl: POLYGON_RPC_URL, - type: NETWORK_TYPES.RPC, chainId: POLYGON_CHAIN_ID, ticker: MATIC, nickname: 'Polygon', id: NETWORK_CONFIGURATION_ID_2, + blockExplorerUrl: undefined, }, - [NETWORK_CONFIGURATION_ID_3]: { + { rpcUrl: POLYGON_RPC_URL_2, - type: NETWORK_TYPES.RPC, chainId: POLYGON_CHAIN_ID, ticker: MATIC, nickname: 'Alt Polygon', - id: NETWORK_CONFIGURATION_ID_1, - }, - }, - selectedNetworkClientId: NetworkType.mainnet, - networksMetadata: { - [NetworkType.mainnet]: { - EIPS: { - 1559: false, - }, - status: 'available', + id: NETWORK_CONFIGURATION_ID_3, + blockExplorerUrl: undefined, }, - }, + ), }, NotificationController: { notifications: { @@ -340,10 +323,6 @@ describe('MetaMaskController', () => { blocklist: ['test.metamask-phishing.io'], name: ListNames.MetaMask, }, - phishfort_hotlist: { - blocklist: [], - name: ListNames.Phishfort, - }, }), ) .get(METAMASK_HOTLIST_DIFF_FILE) @@ -459,17 +438,20 @@ describe('MetaMaskController', () => { }, ); + const metamaskVersion = process.env.METAMASK_VERSION; + afterEach(() => { + // reset `METAMASK_VERSION` env var + process.env.METAMASK_VERSION = metamaskVersion; + }); + it('should details with LoggingController', async () => { const mockVersion = '1.3.7'; - const mockGetVersionInfo = jest.fn().mockReturnValue(mockVersion); + process.env.METAMASK_VERSION = mockVersion; jest.spyOn(LoggingController.prototype, 'add'); const localController = new MetaMaskController({ initLangCode: 'en_US', - platform: { - getVersion: mockGetVersionInfo, - }, browser: browserPolyfillMock, infuraProjectId: 'foo', }); @@ -487,7 +469,7 @@ describe('MetaMaskController', () => { it('should openExtensionInBrowser if version is 8.1.0', () => { const mockVersion = '8.1.0'; - const mockGetVersionInfo = jest.fn().mockReturnValue(mockVersion); + process.env.METAMASK_VERSION = mockVersion; const openExtensionInBrowserMock = jest.fn(); @@ -495,7 +477,6 @@ describe('MetaMaskController', () => { new MetaMaskController({ initLangCode: 'en_US', platform: { - getVersion: mockGetVersionInfo, openExtensionInBrowser: openExtensionInBrowserMock, }, browser: browserPolyfillMock, @@ -663,59 +644,46 @@ describe('MetaMaskController', () => { it('should clear previous identities after vault restoration', async () => { jest.spyOn(metamaskController, 'getBalance').mockResolvedValue('0x0'); - let startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); - let endTime = Date.now(); - const firstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const firstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - expect( - firstVaultIdentities[TEST_ADDRESS].lastSelected >= startTime && - firstVaultIdentities[TEST_ADDRESS].lastSelected <= endTime, - ).toStrictEqual(true); - delete firstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(firstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - }); + expect(firstVaultAccounts).toHaveLength(1); + expect(firstVaultAccounts[0].address).toBe(TEST_ADDRESS); - await metamaskController.preferencesController.setAccountLabel( - TEST_ADDRESS, + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + metamaskController.accountsController.setAccountName( + selectedAccount.id, 'Account Foo', ); - const labelledFirstVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const labelledFirstVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); - delete labelledFirstVaultIdentities[TEST_ADDRESS].lastSelected; - expect(labelledFirstVaultIdentities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, - }); - startTime = Date.now(); + expect(labelledFirstVaultAccounts[0].address).toBe(TEST_ADDRESS); + expect(labelledFirstVaultAccounts[0].metadata.name).toBe('Account Foo'); + await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED_ALT, ); - endTime = Date.now(); - const secondVaultIdentities = cloneDeep( - metamaskController.getState().identities, + const secondVaultAccounts = cloneDeep( + metamaskController.accountsController.listAccounts(), ); + + expect(secondVaultAccounts).toHaveLength(1); expect( - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected >= startTime && - secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected <= endTime, - ).toStrictEqual(true); - delete secondVaultIdentities[TEST_ADDRESS_ALT].lastSelected; - expect(secondVaultIdentities).toStrictEqual({ - [TEST_ADDRESS_ALT]: { - address: TEST_ADDRESS_ALT, - name: DEFAULT_LABEL, - }, - }); + metamaskController.accountsController.getSelectedAccount().address, + ).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].address).toBe(TEST_ADDRESS_ALT); + expect(secondVaultAccounts[0].metadata.name).toBe(DEFAULT_LABEL); }); it('should restore any consecutive accounts with balances without extra zero balance accounts', async () => { @@ -749,29 +717,29 @@ describe('MetaMaskController', () => { allDetectedTokens: { '0x1': { [TEST_ADDRESS_2]: [{}] } }, }); - const startTime = Date.now(); await metamaskController.createNewVaultAndRestore( 'foobar1337', TEST_SEED, ); // Expect first account to be selected - const identities = cloneDeep(metamaskController.getState().identities); - expect( - identities[TEST_ADDRESS].lastSelected >= startTime && - identities[TEST_ADDRESS].lastSelected <= Date.now(), - ).toStrictEqual(true); - - // Expect first 2 accounts to be restored - delete identities[TEST_ADDRESS].lastSelected; - expect(identities).toStrictEqual({ - [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - [TEST_ADDRESS_2]: { - address: TEST_ADDRESS_2, - name: 'Account 2', - lastSelected: expect.any(Number), - }, - }); + const accounts = cloneDeep( + metamaskController.accountsController.listAccounts(), + ); + + const selectedAccount = + metamaskController.accountsController.getSelectedAccount(); + + expect(selectedAccount.address).toBe(TEST_ADDRESS); + expect(accounts).toHaveLength(2); + expect(accounts[0].address).toBe(TEST_ADDRESS); + expect(accounts[0].metadata.name).toBe(DEFAULT_LABEL); + expect(accounts[1].address).toBe(TEST_ADDRESS_2); + expect(accounts[1].metadata.name).toBe('Account 2'); + // TODO: Handle last selected in the update of the next accounts controller. + // expect(accounts[1].metadata.lastSelected).toBeGreaterThan( + // accounts[0].metadata.lastSelected, + // ); }); }); @@ -1876,14 +1844,12 @@ describe('MetaMaskController', () => { symbol: 'FOO', }; - metamaskController.tokensController.update((state) => { - state.tokens = [ - { - address: '0x6b175474e89094c44da98b954eedeac495271d0f', - ...tokenData, - }, - ]; - }); + await metamaskController.tokensController.addTokens([ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + ...tokenData, + }, + ]); metamaskController.provider = provider; const tokenDetails = @@ -2105,7 +2071,6 @@ describe('MetaMaskController', () => { id: NETWORK_CONFIGURATION_ID_1, rpcUrl: ALT_MAINNET_RPC_URL, ticker: ETH, - type: NETWORK_TYPES.RPC, }); }); @@ -2116,7 +2081,6 @@ describe('MetaMaskController', () => { }), ).toStrictEqual({ rpcUrl: POLYGON_RPC_URL, - type: NETWORK_TYPES.RPC, chainId: POLYGON_CHAIN_ID, ticker: MATIC, nickname: 'Polygon', @@ -2135,7 +2099,6 @@ describe('MetaMaskController', () => { id: NETWORK_CONFIGURATION_ID_1, rpcUrl: ALT_MAINNET_RPC_URL, ticker: ETH, - type: NETWORK_TYPES.RPC, }); }); @@ -2154,7 +2117,6 @@ describe('MetaMaskController', () => { }), ).toStrictEqual({ rpcUrl: POLYGON_RPC_URL, - type: NETWORK_TYPES.RPC, chainId: POLYGON_CHAIN_ID, ticker: MATIC, nickname: 'Polygon', @@ -2535,12 +2497,31 @@ describe('MetaMaskController', () => { type: BtcAccountType.P2wpkh, methods: [BtcMethod.SendMany], address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + // We need to have a "Snap account" account here, since the MultichainBalancesController will + // filter it out otherwise! + metadata: { + name: 'Bitcoin Account', + importTime: Date.now(), + keyring: { + type: KeyringType.snap, + }, + snap: { + id: 'npm:@metamask/bitcoin-wallet-snap', + }, + }, }; let localMetamaskController; + let spyBalancesTrackerUpdateBalance; beforeEach(() => { jest.useFakeTimers(); jest.spyOn(MultichainBalancesController.prototype, 'updateBalances'); + jest + .spyOn(MultichainBalancesController.prototype, 'updateBalance') + .mockResolvedValue(); + spyBalancesTrackerUpdateBalance = jest + .spyOn(MultichainBalancesTracker.prototype, 'updateBalance') + .mockResolvedValue(); localMetamaskController = new MetaMaskController({ showUserConfirmation: noop, encryptor: mockEncryptor, @@ -2579,11 +2560,30 @@ describe('MetaMaskController', () => { }); it('calls updateBalances after the interval has passed', async () => { - jest.advanceTimersByTime(BTC_AVG_BLOCK_TIME); - // 2 calls because 1 is during startup + // 1st call is during startup: + // updatesBalances is going to call updateBalance for the only non-EVM + // account that we have expect( localMetamaskController.multichainBalancesController.updateBalances, - ).toHaveBeenCalledTimes(2); + ).toHaveBeenCalledTimes(1); + expect(spyBalancesTrackerUpdateBalance).toHaveBeenCalledTimes(1); + expect(spyBalancesTrackerUpdateBalance).toHaveBeenCalledWith( + mockNonEvmAccount.id, + ); + + // Wait for "block time", so balances will have to be refreshed + jest.advanceTimersByTime(MULTICHAIN_BALANCES_UPDATE_TIME); + + // Check that we tried to fetch the balances more than once + // NOTE: For now, this method might be called a lot more than just twice, but this + // method has some internal logic to prevent fetching the balance too often if we + // consider the balance to be "up-to-date" + expect( + spyBalancesTrackerUpdateBalance.mock.calls.length, + ).toBeGreaterThan(1); + expect(spyBalancesTrackerUpdateBalance).toHaveBeenLastCalledWith( + mockNonEvmAccount.id, + ); }); }); }); diff --git a/app/scripts/migrations/105.test.ts b/app/scripts/migrations/105.test.ts index 944606043075..168fe8dd0916 100644 --- a/app/scripts/migrations/105.test.ts +++ b/app/scripts/migrations/105.test.ts @@ -72,6 +72,7 @@ function expectedInternalAccount( type: 'HD Key Tree', }, lastSelected: lastSelected ? expect.any(Number) : undefined, + importTime: 0, }, options: {}, methods: ETH_EOA_METHODS, diff --git a/app/scripts/migrations/105.ts b/app/scripts/migrations/105.ts index d4f5f2985215..a54b3e6457a7 100644 --- a/app/scripts/migrations/105.ts +++ b/app/scripts/migrations/105.ts @@ -97,6 +97,7 @@ function createInternalAccountsForAccountsController( metadata: { name: identity.name, lastSelected: identity.lastSelected ?? undefined, + importTime: 0, keyring: { // This is default HD Key Tree type because the keyring is encrypted // during migration, the type will get updated when the during the diff --git a/app/scripts/migrations/119.ts b/app/scripts/migrations/119.ts index 6e0f66687350..8cb0d2c04b97 100644 --- a/app/scripts/migrations/119.ts +++ b/app/scripts/migrations/119.ts @@ -46,13 +46,13 @@ function transformState(state: Record) { .accounts, ).length > 0 ) { - Object.values(accountsController.internalAccounts.accounts).forEach( - (internalAccount: InternalAccount) => { - if (!internalAccount.metadata?.importTime) { - internalAccount.metadata.importTime = Date.now(); - } - }, - ); + Object.values( + accountsController.internalAccounts.accounts, + ).forEach((internalAccount) => { + if (!internalAccount.metadata?.importTime) { + internalAccount.metadata.importTime = Date.now(); + } + }); } return { diff --git a/app/scripts/migrations/120.2.test.ts b/app/scripts/migrations/120.2.test.ts index 9cd64275580b..a1b98acde2bd 100644 --- a/app/scripts/migrations/120.2.test.ts +++ b/app/scripts/migrations/120.2.test.ts @@ -573,4 +573,58 @@ describe('migration #120.2', () => { }); }); }); + + it('migrates state from all controllers', async () => { + const oldState = { + NetworkController: { + networkDetails: {}, + networkId: 'example', + networkStatus: 'example', + previousProviderStore: 'example', + provider: 'example', + providerConfig: { + id: 'some-id', + }, + selectedNetworkClientId: 'example', + }, + PhishingController: { + listState: {}, + phishingLists: [], + }, + SelectedNetworkController: { + domains: { + 'https://metamask.io': { + network: 'mainnet', + }, + }, + perDomainNetwork: true, + }, + SnapController: { + snapErrors: {}, + snapStates: {}, + unencryptedSnapStates: {}, + snaps: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toEqual({ + NetworkController: { + providerConfig: {}, + selectedNetworkClientId: 'example', + }, + PhishingController: { + phishingLists: [], + }, + SnapController: { + snapStates: {}, + unencryptedSnapStates: {}, + snaps: {}, + }, + }); + }); }); diff --git a/app/scripts/migrations/120.2.ts b/app/scripts/migrations/120.2.ts index 06e6cc091b9e..ce79c30e2fac 100644 --- a/app/scripts/migrations/120.2.ts +++ b/app/scripts/migrations/120.2.ts @@ -10,7 +10,8 @@ type VersionedData = { export const version = 120.2; /** - * This migration removes any dangling instances of SelectedNetworkController.perDomainNetwork and SnapController.snapErrors + * This migration removes obsolete state from various controllers. In all cases, this was done to + * address Sentry errors. * * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. * @param originalVersionedData.meta - State metadata. diff --git a/app/scripts/migrations/121.1.test.ts b/app/scripts/migrations/121.1.test.ts new file mode 100644 index 000000000000..1d2d6f9ab70e --- /dev/null +++ b/app/scripts/migrations/121.1.test.ts @@ -0,0 +1,238 @@ +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { cloneDeep } from 'lodash'; +import { createMockInternalAccount } from '../../../test/jest/mocks'; +import { migrate, version } from './121.1'; + +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + +const oldVersion = 121; + +const mockInternalAccount = createMockInternalAccount(); +const mockAccountsControllerState: AccountsControllerState = { + internalAccounts: { + accounts: { + [mockInternalAccount.id]: mockInternalAccount, + }, + selectedAccount: mockInternalAccount.id, + }, +}; + +describe('migration #121.1', () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('updates selected account if it is not found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: 'unknown id', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data.AccountsController).toStrictEqual({ + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: mockInternalAccount.id, + }, + }); + }); + + it('does nothing if the selectedAccount is found in the list of accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: mockAccountsControllerState, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if AccountsController state is missing', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + OtherController: {}, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if there are no accounts', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + accounts: {}, + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + it('does nothing if selectedAccount is unset', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + AccountsController: { + ...mockAccountsControllerState, + internalAccounts: { + ...mockAccountsControllerState.internalAccounts, + selectedAccount: '', + }, + }, + }, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(newStorage.data).toStrictEqual(oldStorage.data); + }); + + const invalidState = [ + { + errorMessage: `Migration ${version}: Invalid AccountsController state of type 'string'`, + label: 'AccountsController type', + state: { AccountsController: 'invalid' }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + label: 'Missing internalAccounts', + state: { AccountsController: {} }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state of type 'string'`, + label: 'Invalid internalAccounts', + state: { AccountsController: { internalAccounts: 'invalid' } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + label: 'Missing selectedAccount', + state: { AccountsController: { internalAccounts: { accounts: {} } } }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type 'object'`, + label: 'Invalid selectedAccount', + state: { + AccountsController: { + internalAccounts: { accounts: {}, selectedAccount: {} }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + label: 'Missing accounts', + state: { + AccountsController: { + internalAccounts: { selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type 'string'`, + label: 'Missing accounts', + state: { + AccountsController: { + internalAccounts: { accounts: 'invalid', selectedAccount: '' }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type 'string'`, + label: 'Account entry type', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: 'invalid' }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + label: 'Account entry missing ID', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: {} }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + { + errorMessage: `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type 'object'`, + label: 'Account entry missing ID', + state: { + AccountsController: { + internalAccounts: { + accounts: { [mockInternalAccount.id]: { id: {} } }, + selectedAccount: 'unknown id', + }, + }, + }, + }, + ]; + + // @ts-expect-error 'each' function missing from type definitions, but it does exist + it.each(invalidState)( + 'captures error when state is invalid due to: $label', + async ({ + errorMessage, + state, + }: { + errorMessage: string; + state: Record; + }) => { + const oldStorage = { + meta: { version: oldVersion }, + data: state, + }; + + const newStorage = await migrate(cloneDeep(oldStorage)); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error(errorMessage), + ); + expect(newStorage.data).toStrictEqual(oldStorage.data); + }, + ); +}); diff --git a/app/scripts/migrations/121.1.ts b/app/scripts/migrations/121.1.ts new file mode 100644 index 000000000000..f81893bb00e8 --- /dev/null +++ b/app/scripts/migrations/121.1.ts @@ -0,0 +1,143 @@ +import { hasProperty } from '@metamask/utils'; +import { cloneDeep, isObject } from 'lodash'; +import log from 'loglevel'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 121.1; + +/** + * Fix AccountsController state corruption, where the `selectedAccount` state is set to an invalid + * ID. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record): void { + if (!hasProperty(state, 'AccountsController')) { + return; + } + + const accountsControllerState = state.AccountsController; + + if (!isObject(accountsControllerState)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state of type '${typeof accountsControllerState}'`, + ), + ); + return; + } else if (!hasProperty(accountsControllerState, 'internalAccounts')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController state, missing internalAccounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state of type '${typeof accountsControllerState.internalAccounts}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'selectedAccount') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing selectedAccount`, + ), + ); + return; + } else if ( + typeof accountsControllerState.internalAccounts.selectedAccount !== 'string' + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.selectedAccount state of type '${typeof accountsControllerState + .internalAccounts.selectedAccount}'`, + ), + ); + return; + } else if ( + !hasProperty(accountsControllerState.internalAccounts, 'accounts') + ) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts state, missing accounts`, + ), + ); + return; + } else if (!isObject(accountsControllerState.internalAccounts.accounts)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state of type '${typeof accountsControllerState + .internalAccounts.accounts}'`, + ), + ); + return; + } + + if ( + Object.keys(accountsControllerState.internalAccounts.accounts).length === 0 + ) { + log.warn(`Migration ${version}: Skipping, no accounts found`); + return; + } else if (accountsControllerState.internalAccounts.selectedAccount === '') { + log.warn(`Migration ${version}: Skipping, no selected account set`); + return; + } + + const firstAccount = Object.values( + accountsControllerState.internalAccounts.accounts, + )[0]; + if (!isObject(firstAccount)) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found of type '${typeof firstAccount}'`, + ), + ); + return; + } else if (!hasProperty(firstAccount, 'id')) { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found that is missing an id`, + ), + ); + return; + } else if (typeof firstAccount.id !== 'string') { + global.sentry?.captureException( + new Error( + `Migration ${version}: Invalid AccountsController internalAccounts.accounts state, entry found with an id of type '${typeof firstAccount.id}'`, + ), + ); + return; + } + + if ( + !hasProperty( + accountsControllerState.internalAccounts.accounts, + accountsControllerState.internalAccounts.selectedAccount, + ) + ) { + accountsControllerState.internalAccounts.selectedAccount = firstAccount.id; + } +} diff --git a/app/scripts/migrations/121.2.test.ts b/app/scripts/migrations/121.2.test.ts new file mode 100644 index 000000000000..24e510babe00 --- /dev/null +++ b/app/scripts/migrations/121.2.test.ts @@ -0,0 +1,274 @@ +import { cloneDeep } from 'lodash'; +import { migrate, version } from './121.2'; + +const oldVersion = 121.1; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('Does nothing if `networkConfigurations` or `providerConfig` are not in the network controller state', async () => { + const oldState = { + NetworkController: { + selectedNetworkClientId: 'mainnet', + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('Updates MATIC ticker to POL and updates imageURL in networkConfigurations', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'MATIC', + rpcPrefs: { + imageUrl: './images/matic-token.svg', + }, + }, + }, + }, + }; + + const expectedState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'POL', + rpcPrefs: { + imageUrl: './images/pol-token.svg', + }, + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(expectedState); + }); + + it('Does not update ticker to POL if ticker is not MATIC, but still updates imageURL in networkConfigurations', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'NOT_MATIC', + rpcPrefs: { + imageUrl: './images/matic-token.svg', + }, + }, + }, + }, + }; + + const expectedState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'NOT_MATIC', + rpcPrefs: { + imageUrl: './images/pol-token.svg', + }, + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(expectedState); + }); + + it('Does not update tickers for other network configurations, updates only ticker and imageURL for chain 0x89', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'MATIC', + rpcPrefs: { + imageUrl: './images/matic-token.svg', + }, + }, + '0x1': { + chainId: '0x1', + ticker: 'ETH', + rpcPrefs: { + imageUrl: './images/eth-token.svg', + }, + }, + }, + }, + }; + + const expectedState = { + NetworkController: { + networkConfigurations: { + '0x89': { + chainId: '0x89', + ticker: 'POL', + rpcPrefs: { + imageUrl: './images/pol-token.svg', + }, + }, + '0x1': { + chainId: '0x1', + ticker: 'ETH', + rpcPrefs: { + imageUrl: './images/eth-token.svg', + }, + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(expectedState); + }); + + it('Does nothing if Polygon ChainId (0x89) is not in networkConfigurations', async () => { + const oldState = { + NetworkController: { + networkConfigurations: { + '0x1': { + chainId: '0x1', + ticker: 'ETH', + rpcPrefs: { + imageUrl: './images/eth-token.svg', + }, + }, + '0x2a': { + chainId: '0x2a', + ticker: 'KOVAN', + rpcPrefs: { + imageUrl: './images/kovan-token.svg', + }, + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('Updates Polygon ChainId (0x89) in ProviderConfig if exists, and ticker is set to MATIC, and updates imageUrl', async () => { + const oldState = { + NetworkController: { + providerConfig: { + chainId: '0x89', + ticker: 'MATIC', + rpcPrefs: { + imageUrl: './images/matic-token.svg', + }, + }, + }, + }; + + const expectedState = { + NetworkController: { + providerConfig: { + chainId: '0x89', + ticker: 'POL', + rpcPrefs: { + imageUrl: './images/pol-token.svg', + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(expectedState); + }); + + it('Does nothing if Polygon ChainId (0x89) is not in providerConfig', async () => { + const oldState = { + NetworkController: { + providerConfig: { + chainId: '0x1', + ticker: 'ETH', + rpcPrefs: { + imageUrl: './images/eth-token.svg', + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('Does not update ticker if Polygon ChainId (0x89) is in providerConfig, but ticker is not MATIC, but still updates imageUrl', async () => { + const oldState = { + NetworkController: { + providerConfig: { + chainId: '0x89', + ticker: 'NOT_MATIC', + rpcPrefs: { + imageUrl: './images/matic-token.svg', + }, + }, + }, + }; + + const expectedState = { + NetworkController: { + providerConfig: { + chainId: '0x89', + ticker: 'NOT_MATIC', + rpcPrefs: { + imageUrl: './images/pol-token.svg', + }, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: cloneDeep(oldState), + }); + + expect(transformedState.data).toStrictEqual(expectedState); + }); +}); diff --git a/app/scripts/migrations/121.2.ts b/app/scripts/migrations/121.2.ts new file mode 100644 index 000000000000..8cd84d3e10ab --- /dev/null +++ b/app/scripts/migrations/121.2.ts @@ -0,0 +1,93 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import { CHAIN_IDS } from '../../../shared/constants/network'; + +export const version = 121.2; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +/** + * Migrates MATIC ticker in Network Configuration to POL ticker as per the direction in https://polygon.technology/blog/save-the-date-matic-pol-migration-coming-september-4th-everything-you-need-to-know + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record): void { + const networkControllerState = state.NetworkController; + if ( + hasProperty(state, 'NetworkController') && + isObject(networkControllerState) && + hasProperty(networkControllerState, 'networkConfigurations') && + isObject(networkControllerState.networkConfigurations) + ) { + for (const networkConfiguration of Object.values( + networkControllerState.networkConfigurations, + )) { + if ( + isObject(networkConfiguration) && + networkConfiguration.chainId === CHAIN_IDS.POLYGON + ) { + // update image path regardless of ticker + if ( + hasProperty(networkConfiguration, 'rpcPrefs') && + isObject(networkConfiguration.rpcPrefs) && + hasProperty(networkConfiguration.rpcPrefs, 'imageUrl') && + networkConfiguration.rpcPrefs.imageUrl === './images/matic-token.svg' + ) { + networkConfiguration.rpcPrefs.imageUrl = './images/pol-token.svg'; + } + // update ticker only if MATIC + if ( + hasProperty(networkConfiguration, 'ticker') && + networkConfiguration.ticker === 'MATIC' + ) { + networkConfiguration.ticker = 'POL'; + } + } + } + } + + // handle legacy NetworkController versions (with providerConfig) + if ( + hasProperty(state, 'NetworkController') && + isObject(networkControllerState) && + hasProperty(networkControllerState, 'providerConfig') && + isObject(networkControllerState.providerConfig) && + hasProperty(networkControllerState.providerConfig, 'chainId') && + networkControllerState.providerConfig.chainId === CHAIN_IDS.POLYGON + ) { + // update image path regardless of ticker + if ( + hasProperty(networkControllerState.providerConfig, 'rpcPrefs') && + isObject(networkControllerState.providerConfig.rpcPrefs) && + hasProperty(networkControllerState.providerConfig.rpcPrefs, 'imageUrl') && + networkControllerState.providerConfig.rpcPrefs.imageUrl === + './images/matic-token.svg' + ) { + networkControllerState.providerConfig.rpcPrefs.imageUrl = + './images/pol-token.svg'; + } + // update ticker only if MATIC + if ( + hasProperty(networkControllerState.providerConfig, 'ticker') && + networkControllerState.providerConfig.ticker === 'MATIC' + ) { + networkControllerState.providerConfig.ticker = 'POL'; + } + } +} diff --git a/app/scripts/migrations/122.test.ts b/app/scripts/migrations/122.test.ts new file mode 100644 index 000000000000..0cfeb025150d --- /dev/null +++ b/app/scripts/migrations/122.test.ts @@ -0,0 +1,74 @@ +import { migrate, version } from './122'; + +const oldVersion = 121; + +describe('migration #122', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { + version: oldVersion, + }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + describe('set redesignedConfirmationsEnabled to true in PreferencesController', () => { + it('sets redesignedConfirmationsEnabled to true', async () => { + const oldStorage = { + PreferencesController: { + preferences: { + redesignedConfirmationsEnabled: false, + }, + }, + }; + + const expectedState = { + PreferencesController: { + preferences: { + redesignedConfirmationsEnabled: true, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldStorage, + }); + + expect(transformedState.data).toEqual(expectedState); + }); + + it( + 'sets redesignedConfirmationsEnabled to true even with original preferences object in the' + + 'state', + async () => { + const oldStorage = { + PreferencesController: {}, + }; + + const expectedState = { + PreferencesController: { + preferences: { + redesignedConfirmationsEnabled: true, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldStorage, + }); + + expect(transformedState.data).toEqual(expectedState); + }, + ); + }); +}); diff --git a/app/scripts/migrations/122.ts b/app/scripts/migrations/122.ts new file mode 100644 index 000000000000..cb0b2090472b --- /dev/null +++ b/app/scripts/migrations/122.ts @@ -0,0 +1,50 @@ +import { cloneDeep } from 'lodash'; +import { hasProperty, isObject } from '@metamask/utils'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 122; + +/** + * This migration sets preference redesignedConfirmationsEnabled to true + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +// TODO: Replace `any` with specific type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function transformState(state: Record) { + if (!hasProperty(state, 'PreferencesController')) { + return; + } + + if (!isObject(state.PreferencesController)) { + const controllerType = typeof state.PreferencesController; + global.sentry?.captureException?.( + new Error(`state.PreferencesController is type: ${controllerType}`), + ); + } + + if (!isObject(state.PreferencesController?.preferences)) { + state.PreferencesController = { + preferences: {}, + }; + } + + state.PreferencesController.preferences.redesignedConfirmationsEnabled = true; +} diff --git a/app/scripts/migrations/123.test.ts b/app/scripts/migrations/123.test.ts new file mode 100644 index 000000000000..d9d3d4b440a1 --- /dev/null +++ b/app/scripts/migrations/123.test.ts @@ -0,0 +1,119 @@ +import { migrate, version } from './123'; + +const oldVersion = 122; + +describe('migration #123', () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('does nothing if no preferences controller state is set', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('does nothing if no preferences state is set', async () => { + const oldState = { + PreferencesController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('returns state with advanced details opened if `useNonceField` is enabled', async () => { + const initialState = { + PreferencesController: { + preferences: { showConfirmationAdvancedDetails: false }, + useNonceField: true, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: initialState, + }); + + expect(transformedState).toEqual({ + meta: { version: oldVersion + 1 }, + data: { + ...initialState, + PreferencesController: { + ...initialState.PreferencesController, + preferences: { showConfirmationAdvancedDetails: true }, + }, + }, + }); + }); + + it('returns state with advanced details opened if `sendHexData` is enabled', async () => { + const initialState = { + PreferencesController: { + preferences: { showConfirmationAdvancedDetails: false }, + featureFlags: { + sendHexData: true, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: initialState, + }); + + expect(transformedState).toEqual({ + meta: { version: oldVersion + 1 }, + data: { + ...initialState, + PreferencesController: { + ...initialState.PreferencesController, + preferences: { showConfirmationAdvancedDetails: true }, + }, + }, + }); + }); + + it('returns state with advanced details closed if `sendHexData` and `useNonceField` are disabled', async () => { + const initialState = { + PreferencesController: { + preferences: { showConfirmationAdvancedDetails: false }, + useNonceField: false, + featureFlags: { + sendHexData: false, + }, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: initialState, + }); + + expect(transformedState).toEqual({ + meta: { version: oldVersion + 1 }, + data: { + ...initialState, + PreferencesController: { + ...initialState.PreferencesController, + preferences: { showConfirmationAdvancedDetails: false }, + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/123.ts b/app/scripts/migrations/123.ts new file mode 100644 index 000000000000..c674bac963d2 --- /dev/null +++ b/app/scripts/migrations/123.ts @@ -0,0 +1,45 @@ +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 123; + +/** + * This migration sets the preference `showConfirmationAdvancedDetails` to + * `true` if the user has enabled `useNonceField` or `sendHexData`. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function transformState(state: Record) { + const preferencesControllerState = state?.PreferencesController; + + if (preferencesControllerState?.preferences) { + const isCustomNonceFieldEnabled = preferencesControllerState?.useNonceField; + const isHexDataVisibilityEnabled = + preferencesControllerState?.featureFlags?.sendHexData; + + preferencesControllerState.preferences.showConfirmationAdvancedDetails = + isCustomNonceFieldEnabled || isHexDataVisibilityEnabled; + } + + return state; +} diff --git a/app/scripts/migrations/124.test.ts b/app/scripts/migrations/124.test.ts new file mode 100644 index 000000000000..9b5d6925ad36 --- /dev/null +++ b/app/scripts/migrations/124.test.ts @@ -0,0 +1,55 @@ +import { migrate, version } from './124'; + +const oldVersion = 123; + +describe('migration #124', () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('does nothing if no preferences controller state is set', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('adds property if migration runs', async () => { + const oldState = { + PreferencesController: { + preferences: { + somePreference: true, + }, + }, + }; + + const expectedState = { + PreferencesController: { + preferences: { + redesignedTransactionsEnabled: false, + somePreference: true, + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(expectedState); + }); +}); diff --git a/app/scripts/migrations/124.ts b/app/scripts/migrations/124.ts new file mode 100644 index 000000000000..3584a5052cdd --- /dev/null +++ b/app/scripts/migrations/124.ts @@ -0,0 +1,44 @@ +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 124; + +/** + * This migration sets the preference `redesignedTransactionsEnabled` if the + * user has existing data. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record): void { + const preferencesControllerState = state?.PreferencesController as + | Record + | undefined; + + const preferences = preferencesControllerState?.preferences as + | Record + | undefined; + + if (preferences) { + // Existing MetaMask users will have the option off by default + preferences.redesignedTransactionsEnabled = false; + } +} diff --git a/app/scripts/migrations/125.test.ts b/app/scripts/migrations/125.test.ts new file mode 100644 index 000000000000..a320d607263d --- /dev/null +++ b/app/scripts/migrations/125.test.ts @@ -0,0 +1,31 @@ +import { migrate, version } from './125'; + +const oldVersion = 124; + +describe('migration #125', () => { + afterEach(() => jest.resetAllMocks()); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('deletes the deprecated Txcontroller key', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: { + Txcontroller: { + transactions: [], + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.data.TxController).toStrictEqual(undefined); + }); +}); diff --git a/app/scripts/migrations/125.ts b/app/scripts/migrations/125.ts new file mode 100644 index 000000000000..66ea80a2a911 --- /dev/null +++ b/app/scripts/migrations/125.ts @@ -0,0 +1,36 @@ +import { hasProperty } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 125; + +/** + * This migration removes depreciated `Txcontroller` key if it is present in state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly + * what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by + * controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + if (hasProperty(state, 'TxController')) { + delete state.TxController; + } + return state; +} diff --git a/app/scripts/migrations/126.test.ts b/app/scripts/migrations/126.test.ts new file mode 100644 index 000000000000..bf05ce28a0d3 --- /dev/null +++ b/app/scripts/migrations/126.test.ts @@ -0,0 +1,50 @@ +import { migrate, version } from './126'; + +const oldVersion = 125; + +describe(`migration #${version}`, () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('Does nothing if `providerConfig` is not in the network controller state', async () => { + const oldState = { + NetworkController: { + selectedNetworkClientId: 'mainnet', + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toStrictEqual(oldState); + }); + + it('Removes providerConfig from the network controller state', async () => { + const oldState = { + NetworkController: { + selectedNetworkClientId: 'mainnet', + providerConfig: { + chainId: '0x1', + ticker: 'ETH', + } as object | undefined, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + delete oldState.NetworkController.providerConfig; + expect(transformedState.data).toStrictEqual(oldState); + }); +}); diff --git a/app/scripts/migrations/126.ts b/app/scripts/migrations/126.ts new file mode 100644 index 000000000000..87c099e7329c --- /dev/null +++ b/app/scripts/migrations/126.ts @@ -0,0 +1,39 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 126; + +/** + * This migration removes `providerConfig` from the network controller state. + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState( + state: Record, +): Record { + if ( + hasProperty(state, 'NetworkController') && + isObject(state.NetworkController) + ) { + delete state.NetworkController.providerConfig; + } + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index f5d9ee09f189..7e800337da3b 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -138,6 +138,13 @@ const migrations = [ require('./120.5'), require('./120.6'), require('./121'), + require('./121.1'), + require('./121.2'), + require('./122'), + require('./123'), + require('./124'), + require('./125'), + require('./126'), ]; export default migrations; diff --git a/app/scripts/offscreen.js b/app/scripts/offscreen.js index ba796874f2fc..159a2c8a5773 100644 --- a/app/scripts/offscreen.js +++ b/app/scripts/offscreen.js @@ -1,4 +1,6 @@ +import { captureException } from '@sentry/browser'; import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-communication'; +import { getSocketBackgroundToMocha } from '../../test/e2e/background-socket/socket-background-to-mocha'; /** * Creates an offscreen document that can be used to load additional scripts @@ -9,29 +11,57 @@ import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-c */ export async function createOffscreen() { const { chrome } = globalThis; - if (!chrome.offscreen || (await chrome.offscreen.hasDocument())) { + if (!chrome.offscreen) { return; } + let offscreenDocumentLoadedListener; const loadPromise = new Promise((resolve) => { - const messageListener = (msg) => { + offscreenDocumentLoadedListener = (msg) => { if ( msg.target === OffscreenCommunicationTarget.extensionMain && msg.isBooted ) { - chrome.runtime.onMessage.removeListener(messageListener); + chrome.runtime.onMessage.removeListener( + offscreenDocumentLoadedListener, + ); resolve(); + + // If the Offscreen Document sees `navigator.webdriver === true` and we are in a test environment, + // start the SocketBackgroundToMocha. + if (process.env.IN_TEST && msg.webdriverPresent) { + getSocketBackgroundToMocha(); + } } }; - chrome.runtime.onMessage.addListener(messageListener); + chrome.runtime.onMessage.addListener(offscreenDocumentLoadedListener); }); - await chrome.offscreen.createDocument({ - url: './offscreen.html', - reasons: ['IFRAME_SCRIPTING'], - justification: - 'Used for Hardware Wallet and Snaps scripts to communicate with the extension.', - }); + try { + await chrome.offscreen.createDocument({ + url: './offscreen.html', + reasons: ['IFRAME_SCRIPTING'], + justification: + 'Used for Hardware Wallet and Snaps scripts to communicate with the extension.', + }); + } catch (error) { + if (offscreenDocumentLoadedListener) { + chrome.runtime.onMessage.removeListener(offscreenDocumentLoadedListener); + } + if ( + error?.message?.startsWith( + 'Only a single offscreen document may be created', + ) + ) { + console.debug('Offscreen document already exists; skipping creation'); + } else { + // Report unrecongized errors without halting wallet initialization + // Failures to create the offscreen document does not compromise wallet data integrity or + // core functionality, it's just needed for specific features. + captureException(error); + } + return; + } // In case we are in a bad state where the offscreen document is not loading, timeout and let execution continue. const timeoutPromise = new Promise((resolve) => { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 2430be23f66a..7440b0cea9cc 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -44,38 +44,15 @@ export default class ExtensionPlatform { browser.windows.remove(windowDetails.id); } + /** + * Returns the version of the extension by reading the manifest. + */ getVersion() { - const { version, version_name: versionName } = - browser.runtime.getManifest(); - - const versionParts = version.split('.'); - if (versionName) { - if (versionParts.length < 4) { - throw new Error(`Version missing build number: '${version}'`); - } - // On Chrome, a more descriptive representation of the version is stored in the - // `version_name` field for display purposes. We use this field instead of the `version` - // field on Chrome for non-main builds (i.e. Flask, Beta) because we want to show the - // version in the SemVer-compliant format "v[major].[minor].[patch]-[build-type].[build-number]", - // yet Chrome does not allow letters in the `version` field. - return versionName; - // A fourth version part is sometimes present for "rollback" Chrome builds - } else if (![3, 4].includes(versionParts.length)) { - throw new Error(`Invalid version: ${version}`); - } else if (versionParts[2].match(/[^\d]/u)) { - // On Firefox, the build type and build version are in the third part of the version. - const [major, minor, patchAndPrerelease] = versionParts; - const matches = patchAndPrerelease.match(/^(\d+)([A-Za-z]+)(\d)+$/u); - if (matches === null) { - throw new Error(`Version contains invalid prerelease: ${version}`); - } - const [, patch, buildType, buildVersion] = matches; - return `${major}.${minor}.${patch}-${buildType}.${buildVersion}`; - } - - // If there is no `version_name` and there are only 3 or 4 version parts, then this is not a - // prerelease and the version requires no modification. - return version; + // return the "live" version of the extension, as the bundle of code running + // might be from a different version of the application than the manifest. + // This isn't supposed to happen, but we've seen it before in Sentry. + // This should *not* be updated to the static `process.env.METAMASK_VERSION` + return browser.runtime.getManifest().version; } getExtensionURL(route = null, queryString = null) { diff --git a/app/scripts/platforms/extension.test.js b/app/scripts/platforms/extension.test.js index 14ec2e66b40b..c26a0e43569f 100644 --- a/app/scripts/platforms/extension.test.js +++ b/app/scripts/platforms/extension.test.js @@ -17,14 +17,23 @@ jest.mock('webextension-polyfill', () => { }); describe('extension platform', () => { + const metamaskVersion = process.env.METAMASK_VERSION; beforeEach(() => { // TODO: Delete this an enable 'resetMocks' in `jest.config.js` instead jest.resetAllMocks(); }); + afterEach(() => { + // reset `METAMASK_VERSION` env var + process.env.METAMASK_VERSION = metamaskVersion; + }); + describe('getVersion', () => { it('should return non-prerelease version', () => { - browser.runtime.getManifest.mockReturnValue({ version: '1.2.3' }); + process.env.METAMASK_VERSION = 'should.not.return.me'; + browser.runtime.getManifest.mockReturnValue({ + version: '1.2.3', + }); const extensionPlatform = new ExtensionPlatform(); const version = extensionPlatform.getVersion(); @@ -33,29 +42,21 @@ describe('extension platform', () => { }); it('should return rollback version', () => { - browser.runtime.getManifest.mockReturnValue({ version: '1.2.3.1' }); - const extensionPlatform = new ExtensionPlatform(); - - const version = extensionPlatform.getVersion(); - - expect(version).toBe('1.2.3.1'); - }); - - it('should return SemVer-formatted version for Chrome style manifest of prerelease', () => { + process.env.METAMASK_VERSION = 'should.not.return.me'; browser.runtime.getManifest.mockReturnValue({ - version: '1.2.3.0', - version_name: '1.2.3-beta.0', + version: '1.2.3.1', }); const extensionPlatform = new ExtensionPlatform(); const version = extensionPlatform.getVersion(); - expect(version).toBe('1.2.3-beta.0'); + expect(version).toBe('1.2.3.1'); }); - it('should return SemVer-formatted version for Firefox style manifest of prerelease', () => { + it('should return SemVer-formatted version manifest of prerelease', () => { + process.env.METAMASK_VERSION = 'should.not.return.me'; browser.runtime.getManifest.mockReturnValue({ - version: '1.2.3beta0', + version: '1.2.3-beta.0', }); const extensionPlatform = new ExtensionPlatform(); @@ -63,40 +64,6 @@ describe('extension platform', () => { expect(version).toBe('1.2.3-beta.0'); }); - - it('should throw error if build version is missing from Chrome style prerelease manifest', () => { - browser.runtime.getManifest.mockReturnValue({ - version: '1.2.3', - version_name: '1.2.3-beta.0', - }); - const extensionPlatform = new ExtensionPlatform(); - - expect(() => extensionPlatform.getVersion()).toThrow( - 'Version missing build number:', - ); - }); - - it('should throw error if build version is missing from Firefox style prerelease manifest', () => { - browser.runtime.getManifest.mockReturnValue({ - version: '1.2.3beta', - }); - const extensionPlatform = new ExtensionPlatform(); - - expect(() => extensionPlatform.getVersion()).toThrow( - 'Version contains invalid prerelease:', - ); - }); - - it('should throw error if patch is missing from Firefox style prerelease manifest', () => { - browser.runtime.getManifest.mockReturnValue({ - version: '1.2.beta0', - }); - const extensionPlatform = new ExtensionPlatform(); - - expect(() => extensionPlatform.getVersion()).toThrow( - 'Version contains invalid prerelease:', - ); - }); }); describe('getExtensionURL', () => { diff --git a/app/scripts/runtime-cjs.ts b/app/scripts/runtime-cjs.ts new file mode 100644 index 000000000000..e0169d25bd2e --- /dev/null +++ b/app/scripts/runtime-cjs.ts @@ -0,0 +1,5 @@ +// currently only used in webpack build. + +import '@lavamoat/lavapack/src/runtime-cjs'; + +export {}; diff --git a/app/scripts/sentry-install.js b/app/scripts/sentry-install.js index cbbc03ab10e2..9a88a77c9f7e 100644 --- a/app/scripts/sentry-install.js +++ b/app/scripts/sentry-install.js @@ -1,7 +1,7 @@ import setupSentry from './lib/setupSentry'; // The root compartment will populate this with hooks -global.stateHooks = {}; +global.stateHooks = global.stateHooks || {}; // setup sentry error reporting global.sentry = setupSentry(); diff --git a/app/scripts/skip-onboarding.js b/app/scripts/skip-onboarding.js index 39c3b0b61865..17ce107927ec 100644 --- a/app/scripts/skip-onboarding.js +++ b/app/scripts/skip-onboarding.js @@ -60,14 +60,6 @@ function generateAnnouncementControllerState() { function generateNetworkControllerState() { return { ...defaultFixture().data.NetworkController, - providerConfig: { - chainId: '0xaa36a7', - rpcPrefs: { - blockExplorerUrl: 'https://sepolia.etherscan.io', - }, - ticker: 'SepoliaETH', - type: 'sepolia', - }, networkConfigurations: { networkConfigurationId: { chainId: '0xaa36a7', @@ -137,7 +129,6 @@ function generateAccountsControllerState(account) { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts index 0a014c350c21..b30945e76477 100644 --- a/app/scripts/snaps/preinstalled-snaps.ts +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -1,8 +1,17 @@ import type { PreinstalledSnap } from '@metamask/snaps-controllers'; import MessageSigningSnap from '@metamask/message-signing-snap/dist/preinstalled-snap.json'; +import AccountWatcherSnap from '@metamask/account-watcher/dist/preinstalled-snap.json'; +///: BEGIN:ONLY_INCLUDE_IF(build-flask) +import BitcoinWalletSnap from '@metamask/bitcoin-wallet-snap/dist/preinstalled-snap.json'; +///: END:ONLY_INCLUDE_IF -const PREINSTALLED_SNAPS: readonly PreinstalledSnap[] = Object.freeze([ +// The casts here are less than ideal but we expect the SnapController to validate the inputs. +const PREINSTALLED_SNAPS = Object.freeze([ MessageSigningSnap as PreinstalledSnap, + AccountWatcherSnap as PreinstalledSnap, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + BitcoinWalletSnap as unknown as PreinstalledSnap, + ///: END:ONLY_INCLUDE_IF ]); export default PREINSTALLED_SNAPS; diff --git a/app/scripts/ui.js b/app/scripts/ui.js index bb9785660c34..f85a1855f2f1 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -3,6 +3,7 @@ // This import sets up global functions required for Sentry to function. // It must be run first in case an error is thrown later during initialization. import './lib/setup-initial-state-hooks'; +import '../../development/wdyr'; // dev only, "react-devtools" import is skipped in prod builds import 'react-devtools'; @@ -24,6 +25,7 @@ import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils'; import { SUPPORT_LINK } from '../../shared/lib/ui-utils'; import { getErrorHtml } from '../../shared/lib/error-utils'; +import { endTrace, trace, TraceName } from '../../shared/lib/trace'; import ExtensionPlatform from './platforms/extension'; import { setupMultiplex } from './lib/stream-utils'; import { getEnvironmentType, getPlatform } from './lib/util'; @@ -43,6 +45,24 @@ let extensionPort; start().catch(log.error); async function start() { + const startTime = performance.now(); + + const traceContext = trace({ + name: TraceName.UIStartup, + startTime: performance.timeOrigin, + }); + + trace({ + name: TraceName.LoadScripts, + startTime: performance.timeOrigin, + parentContext: traceContext, + }); + + endTrace({ + name: 'Load Scripts', + timestamp: performance.timeOrigin + startTime, + }); + // create platform global global.platform = new ExtensionPlatform(); @@ -74,6 +94,7 @@ async function start() { // in later version we might try to improve it by reviving same streams. updateUiStreams(); } else { + endTrace({ name: TraceName.BackgroundConnect }); initializeUiWithTab(activeTab); } await loadPhishingWarningPage(); @@ -189,18 +210,30 @@ async function start() { extensionPort.onMessage.addListener(messageListener); extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners); + + trace({ + name: TraceName.BackgroundConnect, + parentContext: traceContext, + }); } else { const messageListener = async (message) => { if (message?.data?.method === 'startUISync') { + endTrace({ name: TraceName.BackgroundConnect }); initializeUiWithTab(activeTab); extensionPort.onMessage.removeListener(messageListener); } }; + extensionPort.onMessage.addListener(messageListener); + + trace({ + name: TraceName.BackgroundConnect, + parentContext: traceContext, + }); } function initializeUiWithTab(tab) { - initializeUi(tab, connectionStream, (err, store) => { + initializeUi(tab, connectionStream, traceContext, (err, store) => { if (err) { // if there's an error, store will be = metamaskState displayCriticalError('troubleStarting', err, store); @@ -208,6 +241,10 @@ async function start() { } isUIInitialised = true; + if (process.env.IN_TEST) { + window.document?.documentElement?.classList.add('controller-loaded'); + } + const state = store.getState(); const { metamask: { completedOnboarding } = {} } = state; @@ -272,7 +309,7 @@ async function queryCurrentActiveTab(windowType) { return { id, title, origin, protocol, url }; } -function initializeUi(activeTab, connectionStream, cb) { +function initializeUi(activeTab, connectionStream, traceContext, cb) { connectToAccountManager(connectionStream, (err, backgroundConnection) => { if (err) { cb(err, null); @@ -284,6 +321,7 @@ function initializeUi(activeTab, connectionStream, cb) { activeTab, container, backgroundConnection, + traceContext, }, cb, ); diff --git a/app/trezor-usb-permissions.html b/app/trezor-usb-permissions.html index 8c92552cfa02..ef87f9a1b8aa 100644 --- a/app/trezor-usb-permissions.html +++ b/app/trezor-usb-permissions.html @@ -2,13 +2,13 @@ - <% if (it.shouldIncludeSnow) { %> - - - <% } %> TrezorConnect | Trezor + <% if (it.shouldIncludeSnow) { %> + + + <% } %> diff --git a/attribution.txt b/attribution.txt index 820ffd0021e7..364128f620f3 100644 --- a/attribution.txt +++ b/attribution.txt @@ -50,34 +50,6 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -****************************** - -aes-js -3.0.0 -The MIT License (MIT) - -Copyright (c) 2015 Richard Moore - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - - ****************************** aes-js @@ -535,6 +507,33 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +asynckit +0.4.0 +The MIT License (MIT) + +Copyright (c) 2016 Alex Indigo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ****************************** async-mutex @@ -670,6 +669,19 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +axios +1.6.8 +# Copyright (c) 2014-present Matt Zabriskie & Collaborators + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ****************************** b4a @@ -1587,7 +1599,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ****************************** @babel/runtime -7.24.6 +7.24.5 MIT License Copyright (c) 2014-present Sebastian McKenzie and other contributors @@ -2398,7 +2410,7 @@ SOFTWARE. ****************************** @blockaid/ppom_release -1.5.1 <> +1.4.6 <> Blockaid BSL License, Version 1.0 (EPL-1.0) Licensor: Blockaid, Inc. @@ -4464,6 +4476,31 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +combined-stream +1.0.8 +Copyright (c) 2011 Debuggable Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ****************************** commander @@ -4608,6 +4645,60 @@ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +contentful +10.8.7 +The MIT License (MIT) + +Copyright (c) 2016 Contentful + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +contentful-resolve-response +1.8.1 +MIT License + +Copyright (c) 2018 Contentful + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ****************************** @contentful/rich-text-html-renderer @@ -4662,6 +4753,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +contentful-sdk-core +8.1.2 +The MIT License (MIT) + +Copyright (c) 2016 Contentful + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ****************************** convert-source-map @@ -5311,6 +5429,31 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +delayed-stream +1.0.0 +Copyright (c) 2011 Debuggable Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + ****************************** delimit-stream @@ -10208,33 +10351,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -****************************** - -@ethersproject/json-wallets -5.7.0 -MIT License - -Copyright (c) 2019 Richard Moore - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ****************************** @ethersproject/keccak256 @@ -10539,33 +10655,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -****************************** - -@ethersproject/wallet -5.7.0 -MIT License - -Copyright (c) 2019 Richard Moore - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ****************************** @ethersproject/web @@ -11196,6 +11285,33 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +fast-copy +2.1.7 +MIT License + +Copyright (c) 2018 Tony Quetano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ****************************** fast-deep-equal @@ -11419,7 +11535,7 @@ SOFTWARE. ****************************** fast-xml-parser -4.4.1 +4.3.4 MIT License Copyright (c) 2017 Amit Kumar Gupta @@ -11839,7 +11955,31 @@ SOFTWARE. ****************************** -for-each +follow-redirects +1.15.6 +Copyright 2014–present Olivier Lalonde , James Talmage , Ruben Verborgh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +for-each 0.3.3 The MIT License (MIT) @@ -11886,6 +12026,31 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +****************************** + +form-data +4.0.0 +Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + ****************************** @fortawesome/fontawesome-free @@ -12703,7 +12868,7 @@ SOFTWARE. ****************************** @grpc/grpc-js -1.9.15 +1.9.14 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -16998,6 +17163,87 @@ licenses; we recommend you read them, as their terms may differ from the terms above. +****************************** + +lodash.isplainobject +4.0.6 +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +****************************** + +lodash.isstring +4.0.1 +Copyright 2012-2016 The Dojo Foundation +Based on Underscore.js, copyright 2009-2016 Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ****************************** loglevel @@ -17943,7 +18189,7 @@ authors: Maarten Zuidhoorn ****************************** @metamask/accounts-controller -15.0.0 +14.0.0 MIT License Copyright (c) 2018 MetaMask @@ -18073,7 +18319,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ****************************** @metamask/assets-controllers -30.0.0 +29.0.0 MIT License Copyright (c) 2018 MetaMask @@ -18242,32 +18488,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -****************************** - -@metamask/controller-utils -10.0.0 -MIT License - -Copyright (c) 2018 MetaMask - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - - ****************************** @metamask/controller-utils @@ -19030,7 +19250,7 @@ SOFTWARE. ****************************** @metamask-institutional/custody-controller -0.2.30 +0.2.27 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19057,7 +19277,7 @@ SOFTWARE. ****************************** @metamask-institutional/custody-keyring -2.0.3 +2.0.0 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19084,7 +19304,7 @@ SOFTWARE. ****************************** @metamask-institutional/extension -0.3.27 +0.3.24 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19111,7 +19331,7 @@ SOFTWARE. ****************************** @metamask-institutional/institutional-features -1.3.5 +1.3.2 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19192,7 +19412,7 @@ SOFTWARE. ****************************** @metamask-institutional/sdk -0.1.30 +0.1.27 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19246,7 +19466,7 @@ SOFTWARE. ****************************** @metamask-institutional/transaction-update -0.2.5 +0.2.2 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19300,7 +19520,7 @@ SOFTWARE. ****************************** @metamask-institutional/websocket-client -0.2.5 +0.2.2 MIT License Copyright (c) 2023 ConsenSys Vertical Apps @@ -19387,27 +19607,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -****************************** - -@metamask/json-rpc-engine -9.0.0 -ISC License - -Copyright (c) 2022 MetaMask - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ****************************** @metamask/json-rpc-middleware-stream @@ -19429,27 +19628,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -****************************** - -@metamask/json-rpc-middleware-stream -8.0.0 -ISC License - -Copyright (c) 2020 MetaMask - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - ****************************** @metamask/keyring-api @@ -19460,7 +19638,7 @@ authors: undefined ****************************** @metamask/keyring-api -6.4.0 +6.3.1 license: Custom: https://docs.metamask.io/snaps/ authors: undefined @@ -19627,7 +19805,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ****************************** @metamask/name-controller -8.0.0 +6.0.1 MIT License Copyright (c) 2023 MetaMask @@ -19822,7 +20000,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ****************************** @metamask/obs-store -9.0.0 +8.1.0 ISC License Copyright (c) 2020 MetaMask @@ -19842,28 +20020,23 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ****************************** -@metamask/permission-controller -10.0.0 -MIT License - -Copyright (c) 2018 MetaMask +@metamask/obs-store +9.0.0 +ISC License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Copyright (c) 2020 MetaMask -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ****************************** @@ -19919,33 +20092,7 @@ If you have any questions, comments or interest in pursuing any other use cases, ****************************** @metamask/phishing-controller -10.0.0 -MIT License - -Copyright (c) 2018 MetaMask - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - - -****************************** - -@metamask/phishing-controller -9.0.4 +9.0.2 MIT License Copyright (c) 2018 MetaMask @@ -20104,7 +20251,7 @@ SOFTWARE. ****************************** @metamask/providers -17.0.0 +16.1.0 MIT License Copyright (c) 2020 MetaMask @@ -20131,7 +20278,7 @@ SOFTWARE. ****************************** @metamask/queued-request-controller -2.0.0 +0.10.0 MIT License Copyright (c) 2023 MetaMask @@ -20338,7 +20485,7 @@ authors: Dan Finlay ****************************** @metamask/smart-transactions-controller -10.1.2 +10.0.1 Copyright ConsenSys Software Inc. 2020. All rights reserved. You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. @@ -20362,31 +20509,7 @@ If you have any questions, comments or interest in pursuing any other use cases, ****************************** @metamask/snaps-controllers -8.4.0 -Copyright ConsenSys Software Inc. 2021. All rights reserved. - -You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. - -Subject to the limited license below, you may not (and you may not permit anyone else to) distribute, publish, copy, modify, merge, combine with another program, create derivative works of, reverse engineer, decompile or otherwise attempt to extract the source code of, the Program or any part thereof, except that you may contribute to this repository. - -You are granted a non-exclusive, non-transferable, non-sublicensable license to distribute, publish, copy, modify, merge, combine with another program or create derivative works of the Program (such resulting program, collectively, the “Resulting Program”) solely for Non-Commercial Use as long as you: - 1. give prominent notice (“Notice”) with each copy of the Resulting Program that the Program is used in the Resulting Program and that the Program is the copyright of ConsenSys; and - 2. subject the Resulting Program and any distribution, publication, copy, modification, merger therewith, combination with another program or derivative works thereof to the same Notice requirement and Non-Commercial Use restriction set forth herein. - -“Non-Commercial Use” means each use as described in clauses (1)-(3) below, as reasonably determined by ConsenSys in its sole discretion: - 1. personal use for research, personal study, private entertainment, hobby projects or amateur pursuits, in each case without any anticipated commercial application; - 2. use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization or government institution; or - 3. the number of monthly active users of the Resulting Program across all versions thereof and platforms globally do not exceed 10,000 at any time. - -You will not use any trade mark, service mark, trade name, logo of ConsenSys or any other company or organization in a way that is likely or intended to cause confusion about the owner or authorized user of such marks, names or logos. - -If you have any questions, comments or interest in pursuing any other use cases, please reach out to us at metamask.license@consensys.net. - - -****************************** - -@metamask/snaps-controllers -9.0.0 +8.2.0 Copyright ConsenSys Software Inc. 2021. All rights reserved. You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. @@ -20410,7 +20533,7 @@ If you have any questions, comments or interest in pursuing any other use cases, ****************************** @metamask/snaps-execution-environments -6.4.0 +6.2.0 Copyright ConsenSys Software Inc. 2022. All rights reserved. You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. @@ -20643,7 +20766,7 @@ If you have any questions, comments or interest in pursuing any other use cases, ****************************** @metamask/snaps-rpc-methods -9.1.3 +9.1.0 Copyright ConsenSys Software Inc. 2021. All rights reserved. You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. @@ -20667,7 +20790,7 @@ If you have any questions, comments or interest in pursuing any other use cases, ****************************** @metamask/snaps-sdk -5.0.0 +4.3.0 ISC License Copyright (c) 2023 MetaMask @@ -20688,7 +20811,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ****************************** @metamask/snaps-utils -7.6.0 +7.4.0 ISC License Copyright (c) 2022 MetaMask @@ -20861,6 +20984,64 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +mime-db +1.52.0 +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +mime-types +2.1.35 +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ****************************** mini-create-react-context @@ -23187,6 +23368,47 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +****************************** + +proxy-from-env +1.1.0 +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +p-throttle +4.1.1 +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ****************************** punycode @@ -23276,7 +23498,7 @@ authors: Kazuhiko Arase ****************************** qrcode.react -3.1.0 +1.0.1 ISC License Copyright (c) 2015, Paul O’Shannessy @@ -23293,8 +23515,53 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -This product bundles QR Code Generator, which is available under a -"MIT" license. For details, see src/third-party/qrcodegen. + +****************************** + +qr.js +0.0.0 +Copyright (c) 2013 Roman Shtylman + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +****************************** + +qs +6.11.2 +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ****************************** @@ -23748,7 +24015,7 @@ SOFTWARE. ****************************** react-redux -7.2.9 +7.2.0 The MIT License (MIT) Copyright (c) 2015-present Dan Abramov @@ -25597,7 +25864,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice ****************************** rpc-websockets -8.0.1 +7.11.1 Copyright (c) Elpheria j.d.o.o. rpc-websockets is an Open Source project licensed under the terms of @@ -28675,6 +28942,13 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +****************************** + +type-fest +4.15.0 +license: (MIT OR CC0-1.0) +authors: Sindre Sorhus + ****************************** typeforce @@ -28844,33 +29118,6 @@ authors: Mohamed Hegazy SOFTWARE -****************************** - -@types/hoist-non-react-statics -3.3.1 - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - - ****************************** @types/jsonwebtoken @@ -29141,33 +29388,6 @@ authors: Mohamed Hegazy SOFTWARE -****************************** - -@types/react-redux -7.1.33 - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE - - ****************************** @types/react-transition-group @@ -31335,7 +31555,34 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ****************************** ws -7.5.10 +7.4.6 +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +****************************** + +ws +7.5.9 The MIT License (MIT) Copyright (c) 2011 Einar Otto Stangvik @@ -31362,7 +31609,7 @@ SOFTWARE. ****************************** ws -8.17.1 +8.16.0 Copyright (c) 2011 Einar Otto Stangvik Copyright (c) 2013 Arnout Kazemier and contributors Copyright (c) 2016 Luigi Pinca and contributors diff --git a/builds.yml b/builds.yml index aa93f756db49..ff12b626a44f 100644 --- a/builds.yml +++ b/builds.yml @@ -14,6 +14,7 @@ default: &default main # Note: These build types should be kept in sync with the list in `.github/workflows/update-lavamoat-policies.yml` buildTypes: main: + id: 10 features: - build-main - keyring-snaps @@ -25,9 +26,8 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.5.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.6.2/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management - - BTC_BETA_SUPPORT: false # Main build uses the default browser manifest manifestOverrides: false # Build name used in multiple user-readable places @@ -35,6 +35,7 @@ buildTypes: buildNameOverride: MetaMask beta: + id: 11 features: - build-beta - keyring-snaps @@ -45,7 +46,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_BETA_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.5.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.6.2/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Modifies how the version is displayed. # eg. instead of 10.25.0 -> 10.25.0-beta.2 @@ -55,6 +56,7 @@ buildTypes: buildNameOverride: MetaMask Beta flask: + id: 15 # Code surrounded using code fences for that feature # will not be removed features: @@ -65,19 +67,19 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.5.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.6.2/index.html - SUPPORT_LINK: https://support.metamask.io/ - SUPPORT_REQUEST_LINK: https://support.metamask.io/ - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID - SEGMENT_WRITE_KEY_REF: SEGMENT_FLASK_WRITE_KEY - ACCOUNT_SNAPS_DIRECTORY_URL: https://metamask.github.io/snaps-directory-staging/main/account-management - EIP_4337_ENTRYPOINT: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' - - BTC_BETA_SUPPORT: false isPrerelease: true manifestOverrides: ./app/build-types/flask/manifest/ buildNameOverride: MetaMask Flask mmi: + id: 20 features: - build-mmi env: @@ -88,7 +90,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_MMI_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.5.0/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.6.2/index.html - MMI_CONFIGURATION_SERVICE_URL: https://configuration.metamask-institutional.io/v2/configuration/default - SUPPORT_LINK: https://mmi-support.metamask.io/hc/en-us - SUPPORT_REQUEST_LINK: https://mmi-support.metamask.io/hc/en-us/requests/new @@ -135,6 +137,7 @@ features: # env object supports both declarations (- FOO), and definitions (- FOO: BAR). # Variables that were declared have to be defined somewhere in the load chain before usage env: + - BRIDGE_USE_DEV_APIS: false - SWAPS_USE_DEV_APIS: false - PORTFOLIO_URL: https://portfolio.metamask.io - TOKEN_ALLOWANCE_IMPROVEMENTS: false @@ -148,6 +151,7 @@ env: - SUPPORT_LINK: https://support.metamask.io - SUPPORT_REQUEST_LINK: https://support.metamask.io - SKIP_BACKGROUND_INITIALIZATION: false + - PPOM_URI: ./ppom_bg.wasm # CDN for blockaid files - BLOCKAID_FILE_CDN: static.cx.metamask.io/api/v1/confirmations/ppom # Blockaid public key for verifying signatures of data files downloaded from CDN @@ -262,6 +266,10 @@ env: - TEST_GAS_FEE_FLOWS: false # Determines if feature flagged network ui new design - ENABLE_NETWORK_UI_REDESIGN: '' + # Temporary mechanism to enable security alerts API prior to release + - SECURITY_ALERTS_API_ENABLED: '' + # URL of security alerts API used to validate dApp requests + - SECURITY_ALERTS_API_URL: 'http://localhost:3000' # Enables the notifications feature within the build: - NOTIFICATIONS: '' @@ -280,3 +288,8 @@ env: ### - EIP_4337_ENTRYPOINT: null + ### + # Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render + # This should NEVER be enabled in production since it slows down react + ### + - ENABLE_WHY_DID_YOU_RENDER: false diff --git a/development/README.md b/development/README.md index b9247f66aa6f..33ab036975e4 100644 --- a/development/README.md +++ b/development/README.md @@ -55,7 +55,8 @@ You can inspect the requests in the `Network` tab of your browser's Developer To by filtering for `POST` requests to `/v1/batch`. The full url will be `http://localhost:9090/v1/batch` or `https://api.segment.io/v1/batch` respectively. -## Debugging Sentry +## Sentry +### Debugging Sentry 1. Set `SENTRY_DSN_DEV`, or `SENTRY_DSN` if using a production build, in `.metamaskrc` to a suitable Sentry URL. - The example value specified in `.metamaskrc.dist` uses the `test-metamask` project in the MetaMask account. @@ -72,10 +73,43 @@ or `https://api.segment.io/v1/batch` respectively. 6. Alternatively, call `window.stateHooks.throwTestError()` or `window.stateHooks.throwTestBackgroundError()` via the UI console. -## Source Maps +### Debugging the Publish Release Flow +#### Sentry UI Setup +1. Go to Sentry and Create an Organization, by clicking `Account Menu > Switch organization > Create a new organization` +2. Create a Javascript Project, by clicking `Projects > Create Project > Javascript` +3. Create a User Auth Token, by clicking `Account Menu > User auth tokens` +4. Select your newly created project and grant all permissions to your token +5. Copy your token to your clipboard + +[](sentry-auth-token.png) + +#### Sentry-Cli Setup +1. Go to your terminal, inside the `metamask-extension` project +2. Login to Sentry using the command line `yarn sentry-cli login --auth-token YOUR_TOKEN` +3. List your organizations and copy the id for the organization you want to see `yarn sentry-cli organizations list` +4. List your organization projects and copy the id for the you created `yarn sentry-cli projects list --org YOUR_ORG_ID` + +[](sentry-cli.png) + +#### Publish a Release to Sentry +1. Build your desired MetaMask project. Examples: + 1. `yarn dist` to create an MV3 build + 2. `yarn dist:mv2` to create an MV2 build + 3. (and so on) +2. Move the build to its corresponding folder. Ie: `mv dist dist-mv2` (skip this step, if you did the regular MV3 build) +3. Publish the release to Sentry: + 1. If it's an MV3 build `yarn sentry:publish --org YOUR_ORG_ID --project YOUR_PROJECT_ID` + 2. If it's an MV2 build `yarn sentry:publish --dist mv2 --org YOUR_ORG_ID --project YOUR_PROJECT_ID` +4. See build files and source maps are uploaded +5. Go to Sentry +6. Check the Source Maps have been uploaded correctly in Sentry: go to `Settings > Projects > Project Name > Source Maps` + +[](sentry-source-maps.png) + +Extra Note: if you already uploaded one version, you can change the `package.json` version and run again the publish step, to test the complete flow. +## Source Maps ### Debugging production builds using Source Maps - To unbundle the extensions compiled and minified JavaScript using Source Maps: - Open Chrome DevTools to inspect the `background.html` or `home.html` view diff --git a/development/build/index.js b/development/build/index.js index f9a778ed8966..9fcf43a19142 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -119,6 +119,7 @@ async function defineAndRunBuildTasks() { 'OffscreenCanvas', // Used by browser to generate notifications // globals chromedriver needs to function /cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu, + 'name', 'performance', 'parseFloat', 'innerWidth', diff --git a/development/build/manifest.js b/development/build/manifest.js index d0042af75c67..bc5325b372eb 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -2,14 +2,12 @@ const { promises: fs } = require('fs'); const path = require('path'); const childProcess = require('child_process'); const { mergeWith, cloneDeep } = require('lodash'); +const { isManifestV3 } = require('../../shared/modules/mv3.utils'); -const IS_MV3_ENABLED = - process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined; - -const baseManifest = IS_MV3_ENABLED +const baseManifest = isManifestV3 ? require('../../app/manifest/v3/_base.json') : require('../../app/manifest/v2/_base.json'); -const baradDurManifest = IS_MV3_ENABLED +const baradDurManifest = isManifestV3 ? require('../../app/manifest/v3/_barad_dur.json') : require('../../app/manifest/v2/_barad_dur.json'); const { loadBuildTypesConfig } = require('../lib/build-type'); @@ -38,7 +36,7 @@ function createManifestTasks({ '..', '..', 'app', - IS_MV3_ENABLED ? 'manifest/v3' : 'manifest/v2', + isManifestV3 ? 'manifest/v3' : 'manifest/v2', `${platform}.json`, ), ); @@ -71,6 +69,7 @@ function createManifestTasks({ ...manifest.permissions, 'webRequestBlocking', 'http://localhost/*', + 'tabs', // test builds need tabs permission for switchToWindowWithTitle ]; }); @@ -80,6 +79,7 @@ function createManifestTasks({ ...manifest.permissions, 'webRequestBlocking', 'http://localhost/*', + 'tabs', // test builds need tabs permission for switchToWindowWithTitle ]; }); @@ -143,7 +143,7 @@ function createManifestTasks({ buildType, applyLavaMoat, shouldIncludeSnow, - shouldIncludeMV3: IS_MV3_ENABLED, + isManifestV3, }); manifest.description = `${environment} build from git id: ${gitRevisionStr}`; diff --git a/development/build/scripts.js b/development/build/scripts.js index c87197bfaf1c..926acea8252d 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -29,6 +29,7 @@ const terser = require('terser'); const bifyModuleGroups = require('bify-module-groups'); const { streamFlatMap } = require('../stream-flat-map'); +const { isManifestV3 } = require('../../shared/modules/mv3.utils'); const { setEnvironmentVariables } = require('./set-environment-variables'); const { BUILD_TARGETS } = require('./constants'); const { getConfig } = require('./config'); @@ -52,9 +53,6 @@ const { createRemoveFencedCodeTransform, } = require('./transforms/remove-fenced-code'); -const isEnableMV3 = - process.env.ENABLE_MV3 === 'true' || process.env.ENABLE_MV3 === undefined; - // map dist files to bag of needed native APIs against LM scuttling const scuttlingConfigBase = { 'scripts/sentry-install.js': { @@ -194,7 +192,7 @@ function createScriptTasks({ // In MV3 we will need to build our offscreen entry point bundle and any // entry points for iframes that we want to lockdown with LavaMoat. - if (isEnableMV3) { + if (isManifestV3) { standardEntryPoints.push('offscreen'); } @@ -355,7 +353,7 @@ function createScriptTasks({ () => { // MV3 injects inpage into the tab's main world, but in MV2 we need // to do it manually: - if (isEnableMV3) { + if (isManifestV3) { return; } // stringify scripts/inpage.js into itself, and then make it inject itself into the page @@ -661,6 +659,12 @@ function createFactoredBuild({ const isTest = buildTarget === BUILD_TARGETS.TEST || buildTarget === BUILD_TARGETS.TEST_DEV; + const scripts = getScriptTags({ + groupSet, + commonSet, + shouldIncludeSnow, + applyLavaMoat, + }); switch (groupLabel) { case 'ui': { renderHtmlFile({ @@ -669,36 +673,39 @@ function createFactoredBuild({ shouldIncludeSnow, applyLavaMoat, isMMI: buildType === 'mmi', + scripts, }); renderHtmlFile({ htmlName: 'popup', browserPlatforms, shouldIncludeSnow, applyLavaMoat, + scripts, }); renderHtmlFile({ - htmlName: 'notification', + htmlName: 'popup-init', browserPlatforms, shouldIncludeSnow, applyLavaMoat, - isMMI: buildType === 'mmi', - isTest, + scripts, }); renderHtmlFile({ - htmlName: 'home', + htmlName: 'notification', browserPlatforms, shouldIncludeSnow, applyLavaMoat, isMMI: buildType === 'mmi', isTest, + scripts, }); - renderJavaScriptLoader({ - groupSet, - commonSet, + renderHtmlFile({ + htmlName: 'home', browserPlatforms, shouldIncludeSnow, applyLavaMoat, - destinationFileName: 'load-app.js', + isMMI: buildType === 'mmi', + isTest, + scripts, }); break; } @@ -710,16 +717,9 @@ function createFactoredBuild({ browserPlatforms, shouldIncludeSnow, applyLavaMoat, + scripts, }); - renderJavaScriptLoader({ - groupSet, - commonSet, - browserPlatforms, - shouldIncludeSnow, - applyLavaMoat, - destinationFileName: 'load-background.js', - }); - if (isEnableMV3) { + if (isManifestV3) { const jsBundles = [ ...commonSet.values(), ...groupSet.values(), @@ -747,17 +747,19 @@ function createFactoredBuild({ browserPlatforms, shouldIncludeSnow, applyLavaMoat: false, + scripts, }); break; } case 'offscreen': { - renderJavaScriptLoader({ + renderHtmlFile({ + htmlName: 'offscreen', groupSet, commonSet, browserPlatforms, shouldIncludeSnow, applyLavaMoat, - destinationFileName: 'load-offscreen.js', + scripts, }); break; } @@ -931,7 +933,11 @@ function setupBundlerDefaults( [ babelify, { - only: ['./**/node_modules/firebase', './**/node_modules/@firebase'], + only: [ + './**/node_modules/firebase', + './**/node_modules/@firebase', + './**/node_modules/marked', + ], global: true, }, ], @@ -976,7 +982,7 @@ function setupBundlerDefaults( // Setup source maps setupSourcemaps(buildConfiguration, { buildTarget }); // Setup wrapping of code against scuttling (before sourcemaps generation) - setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars); + setupScuttlingWrapping(buildConfiguration, applyLavaMoat); } } @@ -1030,13 +1036,10 @@ function setupMinification(buildConfiguration) { }); } -function setupScuttlingWrapping(buildConfiguration, applyLavaMoat, envVars) { - const scuttlingConfig = - envVars.ENABLE_MV3 === 'true' || - envVars.ENABLE_MV3 === undefined || - envVars.ENABLE_MV3 === true - ? mv3ScuttlingConfig - : standardScuttlingConfig; +function setupScuttlingWrapping(buildConfiguration, applyLavaMoat) { + const scuttlingConfig = isManifestV3 + ? mv3ScuttlingConfig + : standardScuttlingConfig; const { events } = buildConfiguration; events.on('configurePipeline', ({ pipeline }) => { pipeline.get('scuttle').push( @@ -1140,13 +1143,11 @@ async function createBundle(buildConfiguration, { reloadOnChange }) { } } -function renderJavaScriptLoader({ +function getScriptTags({ groupSet, commonSet, - browserPlatforms, shouldIncludeSnow, applyLavaMoat, - destinationFileName, }) { if (applyLavaMoat === undefined) { throw new Error( @@ -1180,17 +1181,8 @@ function renderJavaScriptLoader({ ...jsBundles, ]; - browserPlatforms.forEach((platform) => { - const appLoadFilePath = './app/scripts/load-app.js'; - const appLoadContents = readFileSync(appLoadFilePath, 'utf8'); - - const scriptDest = `./dist/${platform}/${destinationFileName}`; - const scriptOutput = appLoadContents.replace( - '/* SCRIPTS */', - `...${JSON.stringify(requiredScripts)}`, - ); - - writeFileSync(scriptDest, scriptOutput); + return requiredScripts.map((src) => { + return ``; }); } @@ -1201,6 +1193,7 @@ function renderHtmlFile({ applyLavaMoat, isMMI, isTest, + scripts = [], }) { if (applyLavaMoat === undefined) { throw new Error( @@ -1213,15 +1206,28 @@ function renderHtmlFile({ ); } - const htmlFilePath = `./app/${htmlName}.html`; + const scriptTags = scripts.join('\n '); + + const htmlFilePath = + htmlName === 'offscreen' + ? `./offscreen/${htmlName}.html` + : `./app/${htmlName}.html`; const htmlTemplate = readFileSync(htmlFilePath, 'utf8'); const eta = new Eta(); - const htmlOutput = eta.renderString(htmlTemplate, { - isMMI, - isTest, - shouldIncludeSnow, - }); + const htmlOutput = eta + .renderString(htmlTemplate, { isMMI, isTest, shouldIncludeSnow }) + // these replacements are added to support the webpack build's automatic + // compilation of html files, which the gulp-based process doesn't support. + .replace('./scripts/load/background.ts', './load-background.js') + .replace( + '', + `${scriptTags}\n `, + ) + .replace('', scriptTags) + .replace('', scriptTags) + .replace('../ui/css/index.scss', './index.css') + .replace('@lavamoat/snow/snow.prod.js', './scripts/snow.js'); browserPlatforms.forEach((platform) => { const dest = `./dist/${platform}/${htmlName}.html`; // we dont have a way of creating async events atm diff --git a/development/build/set-environment-variables.js b/development/build/set-environment-variables.js index f8d96c34d76e..88e50af873bf 100644 --- a/development/build/set-environment-variables.js +++ b/development/build/set-environment-variables.js @@ -189,28 +189,24 @@ function getPhishingWarningPageUrl({ variables, testing }) { }/`; } - // We add a hash/fragment to the URL dynamically, so we need to ensure it - // has a valid pathname to append a hash to. - const normalizedUrl = phishingWarningPageUrl.endsWith('/') - ? phishingWarningPageUrl - : `${phishingWarningPageUrl}/`; - let phishingWarningPageUrlObject; try { // eslint-disable-next-line no-new - phishingWarningPageUrlObject = new URL(normalizedUrl); + phishingWarningPageUrlObject = new URL(phishingWarningPageUrl); } catch (error) { throw new Error( - `Invalid phishing warning page URL: '${normalizedUrl}'`, + `Invalid phishing warning page URL: '${phishingWarningPageUrl}'`, error, ); } if (phishingWarningPageUrlObject.hash) { // The URL fragment must be set dynamically throw new Error( - `URL fragment not allowed in phishing warning page URL: '${normalizedUrl}'`, + `URL fragment not allowed in phishing warning page URL: '${phishingWarningPageUrl}'`, ); } - return normalizedUrl; + // return a normalized version of the URL; a `/` will be appended to the end + // of the domain if it is missing + return phishingWarningPageUrlObject.toString(); } diff --git a/development/build/static.js b/development/build/static.js index cf0cd1078423..2c0854d4b3c8 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -5,6 +5,7 @@ const glob = require('fast-glob'); const { loadBuildTypesConfig } = require('../lib/build-type'); +const { isManifestV3 } = require('../../shared/modules/mv3.utils'); const { TASKS } = require('./constants'); const { createTask, composeSeries } = require('./task'); const { getPathInsideNodeModules } = require('./utils'); @@ -145,9 +146,7 @@ function getCopyTargets(shouldIncludeLockdown, shouldIncludeSnow) { ...(shouldIncludeSnow ? [ { - src: shouldIncludeSnow - ? `./node_modules/@lavamoat/snow/snow.prod.js` - : EMPTY_JS_FILE, + src: `./node_modules/@lavamoat/snow/snow.prod.js`, dest: `scripts/snow.js`, }, { @@ -166,10 +165,6 @@ function getCopyTargets(shouldIncludeLockdown, shouldIncludeSnow) { src: './app/scripts/init-globals.js', dest: 'scripts/init-globals.js', }, - { - src: './app/scripts/load-app.js', - dest: 'scripts/load-app.js', - }, { src: shouldIncludeLockdown ? `./app/scripts/lockdown-run.js` @@ -192,22 +187,12 @@ function getCopyTargets(shouldIncludeLockdown, shouldIncludeSnow) { dest: `scripts/runtime-lavamoat.js`, pattern: '', }, - { - src: `./offscreen/`, - pattern: `*.html`, - dest: '', - }, { src: getPathInsideNodeModules('@blockaid/ppom_release', '/'), pattern: '*.wasm', - dest: - process.env.ENABLE_MV3 === 'true' || - process.env.ENABLE_MV3 === undefined - ? 'scripts/' - : '', + dest: isManifestV3 ? 'scripts/' : '', }, - ...(process.env.ENABLE_MV3 === 'true' || - process.env.ENABLE_MV3 === undefined + ...(isManifestV3 ? [ { src: getPathInsideNodeModules( diff --git a/development/build/transforms/remove-fenced-code.test.js b/development/build/transforms/remove-fenced-code.test.js index 345fc55a0926..de6960ba1ec9 100644 --- a/development/build/transforms/remove-fenced-code.test.js +++ b/development/build/transforms/remove-fenced-code.test.js @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ const buildUtils = require('@metamask/build-utils'); const { createRemoveFencedCodeTransform } = require('./remove-fenced-code'); const transformUtils = require('./utils'); @@ -36,6 +39,10 @@ describe('build/transforms/remove-fenced-code', () => { lintTransformedFileMock.mockImplementation(() => Promise.resolve()); }); + afterEach(() => { + jest.resetAllMocks(); + }); + it('returns a PassThrough stream for files with ignored extensions', async () => { const fileContent = '"Valid JSON content"\n'; const stream = createRemoveFencedCodeTransform( diff --git a/development/build/transforms/utils.test.js b/development/build/transforms/utils.test.js index d5cd57bfd461..952d0f46f181 100644 --- a/development/build/transforms/utils.test.js +++ b/development/build/transforms/utils.test.js @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ const { getESLintInstance } = require('./utils'); let mockESLint; diff --git a/development/build/utils.js b/development/build/utils.js index 746290fb8e2b..525815d2520a 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -70,10 +70,38 @@ function getBrowserVersionMap(platforms, version) { const versionParts = [major, minor, patch]; const browserSpecificVersion = {}; if (prerelease) { - if (platform === 'firefox') { - versionParts[2] = `${versionParts[2]}${buildType}${buildVersion}`; - } else { - versionParts.push(buildVersion); + const { id } = loadBuildTypesConfig().buildTypes[buildType]; + if (id < 10 || id > 64 || buildVersion < 0 || buildVersion > 999) { + throw new Error( + `Build id must be 10-64 and release version must be 0-999 +(inclusive). Received an id of '${id}' and a release version of +'${buildVersion}'. + +Wait, but that seems so arbitrary? +================================== + +We encode the build id and the release version into the extension version by +concatenating the two numbers together. The maximum value for the concatenated +number is 65535 (a Chromium limitation). The value cannot start with a '0'. We +utilize 2 digits for the build id and 3 for the release version. This affords us +55 release types and 1000 releases per 'version' + build type (for a minimum +value of 10000 and a maximum value of 64999). + +Okay, so how do I fix it? +========================= + +You'll need to adjust the build 'id' (in builds.yml) or the release version to +fit within these limits or bump the version number in package.json and start the +release version number over from 0. If you can't do that you'll need to come up +with a new way of encoding this information, or re-evaluate the need for this +metadata. + +Good luck on your endeavors.`, + ); + } + versionParts.push(`${id}${buildVersion}`); + if (platform !== 'firefox') { + // firefox doesn't support `version_name` browserSpecificVersion.version_name = version; } } @@ -227,7 +255,7 @@ function getPathInsideNodeModules(packageName, pathToFiles) { * @param {string} options.buildType - The build type of the current build. * @param {boolean} options.applyLavaMoat - Flag if lavamoat was applied. * @param {boolean} options.shouldIncludeSnow - Flag if snow should be included in the build name. - * @param {boolean} options.shouldIncludeMV3 - Flag if mv3 should be included in the build name. + * @param {boolean} options.isManifestV3 - Flag if mv3 should be included in the build name. * @param options.environment * @returns {string} The build name. */ @@ -236,7 +264,7 @@ function getBuildName({ buildType, applyLavaMoat, shouldIncludeSnow, - shouldIncludeMV3, + isManifestV3, }) { const config = loadBuildTypesConfig(); @@ -245,7 +273,7 @@ function getBuildName({ `MetaMask ${capitalize(buildType)}`; if (environment !== ENVIRONMENT.PRODUCTION) { - const mv3Str = shouldIncludeMV3 ? ' MV3' : ''; + const mv3Str = isManifestV3 ? ' MV3' : ''; const lavamoatStr = applyLavaMoat ? ' lavamoat' : ''; const snowStr = shouldIncludeSnow ? ' snow' : ''; name += `${mv3Str}${lavamoatStr}${snowStr}`; diff --git a/development/fitness-functions/common/constants.ts b/development/fitness-functions/common/constants.ts index 0514c6ed9002..5758d4e2a6e1 100644 --- a/development/fitness-functions/common/constants.ts +++ b/development/fitness-functions/common/constants.ts @@ -1,7 +1,7 @@ // include JS, TS, JSX, TSX files only excluding files in the e2e tests and // fitness functions directories const EXCLUDE_E2E_TESTS_REGEX = - '^(?!test/e2e)(?!development/fitness).*.(js|ts|jsx|tsx)$'; + '^(?!test/e2e)(?!development/fitness|development/webpack).*.(js|ts|jsx|tsx)$'; // include JS and JSX files in the shared directory only const SHARED_FOLDER_JS_REGEX = '^(shared).*.(js|jsx)$'; diff --git a/development/generate-attributions/package.json b/development/generate-attributions/package.json index 4102bbb8b887..f6da9feb6d82 100644 --- a/development/generate-attributions/package.json +++ b/development/generate-attributions/package.json @@ -9,7 +9,7 @@ }, "engines": { "node": ">= 20", - "yarn": "^4.0.2" + "yarn": "^4.2.2" }, "lavamoat": { "allowScripts": { diff --git a/development/generate-rc-commits.js b/development/generate-rc-commits.js index 4de83ccaebce..8b91ec0ff4f1 100644 --- a/development/generate-rc-commits.js +++ b/development/generate-rc-commits.js @@ -10,7 +10,7 @@ const octokit = new Octokit({ /** * This script is used to filter and group commits by teams based on unique commit messages. * It takes two branches as input and generates a CSV file with the commit message, author,PR link, team,release tag and commit hash - * The teams and their members are defined in the 'authorTeams' object. + * The teams and their members are defined in the 'teams.json' file. * * Command to run the script: node development/generate-rc-commits.js origin/branchA origin/branchB * @@ -19,90 +19,24 @@ const octokit = new Octokit({ * Output: the generated commits will be in a file named 'commits.csv'. */ -// JSON mapping authors to teams -const authorTeams = { - Accounts: [ - 'Owen Craston', - 'Gustavo Antunes', - 'Monte Lai', - 'Daniel Rocha', - 'Howard Braham', - 'Kate Johnson', - 'Xiaoming Wang', - 'Charly Chevalier', - 'Mike B', - ], - 'Wallet UX': ['David Walsh', 'Nidhi Kumari', 'Jony Bursztyn'], - 'Extension Platform': [ - 'chloeYue', - 'Chloe Gao', - 'danjm', - 'Danica Shen', - 'Brad Decker', - 'hjetpoluru', - 'Harika Jetpoluru', - 'Marina Boboc', - 'Gauthier Petetin', - 'Dan Miller', - 'Dan J Miller', - 'David Murdoch', - 'Niranjana Binoy', - 'Victor Thomas', - 'vthomas13', - ], - DappAPI: ['tmashuang', 'jiexi', 'BelfordZ', 'Shane'], - 'Confirmation UX': [ - 'Pedro Figueiredo', - 'Sylva Elendu', - 'Olusegun Akintayo', - 'Jyoti Puri', - 'Ariella Vu', - 'seaona', - ], - 'Confirmation Systems': [ - 'OGPoyraz', - 'vinistevam', - 'Matthew Walsh', - 'cryptotavares', - 'Vinicius Stevam', - 'Derek Brans', - 'sleepytanya', - ], - 'Design Systems': ['georgewrmarshall', 'Garrett Bear', 'George Marshall'], - Snaps: [ - 'David Drazic', - 'hmalik88', - 'Montoya', - 'Mrtenz', - 'Frederik Bolding', - 'Bowen Sanders', - 'Guillaume Roux', - 'Hassan Malik', - 'Maarten Zuidhoorn', - 'Jonathan Ferreira', - ], - Assets: ['salimtb', 'sahar-fehri', 'Brian Bergeron'], - Linea: ['VGau', 'Victorien Gauch'], - lavamoat: ['weizman', 'legobeat', 'kumavis', 'LeoTM'], - 'Shared Libraries': [ - 'Michele Esposito', - 'Elliot Winkler', - 'Gudahtt', - 'Jongsun Suh', - 'Mark Stacey', - ], - MMI: [ - 'António Regadas', - 'Albert Olivé', - 'Ramon AC', - 'Shane T', - 'Bernardo Garces Chapero', - ], - Swaps: ['Daniel', 'Davide Brocchetto', 'Nicolas Ferro'], - Devex: ['Thomas Huang', 'Alex Donesky', 'jiexi', 'Zachary Belford'], - Notifications: ['Prithpal-Sooriya', 'Matteo Scurati', 'Prithpal Sooriya'], - Bridging: ['Bilal', 'micaelae'], -}; +// Function to fetch author teams mapping file from teams.json +async function fetchAuthorTeamsFile() { + try { + const { data } = await octokit.request( + 'GET /repos/{owner}/{repo}/contents/{path}', + { + owner: 'MetaMask', + repo: 'MetaMask-planning', + path: 'teams.json', + }, + ); + const content = Buffer.from(data.content, 'base64').toString('utf-8'); + return JSON.parse(content); // Assuming the file is in JSON format + } catch (error) { + console.error('Error fetching author teams mapping file:', error); + return {}; + } +} // Function to get PR labels async function getPRLabels(owner, repo, prNumber) { @@ -123,18 +57,26 @@ async function getPRLabels(owner, repo, prNumber) { } } -// Function to get the team for a given author -function getTeamForAuthor(authorName) { - for (const [team, authors] of Object.entries(authorTeams)) { - if (authors.includes(authorName)) { - return team; - } +// Function to get the GitHub username for a given commit hash +async function getGitHubUsername(commitHash) { + try { + const { data } = await octokit.request( + 'GET /repos/{owner}/{repo}/commits/{ref}', + { + owner: 'MetaMask', + repo: 'metamask-extension', + ref: commitHash, + }, + ); + return data.author ? data.author.login : null; + } catch (error) { + console.error('Error fetching GitHub username:', error); + return null; } - return 'Other/Unknown'; // Default team for unknown authors } // Function to filter commits based on unique commit messages and group by teams -async function filterCommitsByTeam(branchA, branchB) { +async function filterCommitsByTeam(branchA, branchB, authorTeams) { try { const git = simpleGit(); @@ -151,17 +93,27 @@ async function filterCommitsByTeam(branchA, branchB) { const log = await git.log(logOptions); const seenMessages = new Set(); const commitsByTeam = {}; + let processedCommits = 0; const MAX_COMMITS = 500; // Limit the number of commits to process + console.log('Generation of the CSV file "commits.csv" is in progress...'); for (const commit of log.all) { - const { author, message, hash } = commit; - if (commitsByTeam.length >= MAX_COMMITS) { + if (processedCommits >= MAX_COMMITS) { break; } - const team = getTeamForAuthor(author); + const { author, message, hash } = commit; + const githubUsername = await getGitHubUsername(hash); + let team = authorTeams[githubUsername] || 'Other/Unknown'; + + // Format the team label + team = team + .replace(/^team-/u, '') // Remove the "team-" prefix + .split('-') // Split the string into an array of words + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize the first letter of each word + .join(' '); // Join the words back into a string with spaces // Extract PR number from the commit message using regex const prMatch = message.match(/\(#(\d+)\)/u); @@ -200,9 +152,9 @@ async function filterCommitsByTeam(branchA, branchB) { releaseLabel, hash: hash.substring(0, 10), }); + processedCommits += 1; } } - return commitsByTeam; } catch (error) { console.error(error); @@ -227,7 +179,7 @@ function formatAsCSV(commitsByTeam) { }); } csvContent.unshift( - 'Commit Message,Author,PR Link,Team,Release Label, Commit Hash', + 'Commit Message,Author,PR Link,Team,Release Label,Commit Hash', ); return csvContent; @@ -244,7 +196,14 @@ async function main() { const branchA = args[0]; const branchB = args[1]; - const commitsByTeam = await filterCommitsByTeam(branchA, branchB); + // Fetch author teams mapping from the teams.json file + const authorTeams = await fetchAuthorTeamsFile(); + + const commitsByTeam = await filterCommitsByTeam( + branchA, + branchB, + authorTeams, + ); if (Object.keys(commitsByTeam).length === 0) { console.log('No unique commits found.'); diff --git a/development/jest.config.js b/development/jest.config.js deleted file mode 100644 index d95157a49635..000000000000 --- a/development/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - displayName: '/development', - collectCoverageFrom: ['/build/transforms/**/*.js'], - coverageDirectory: '../coverage', - coverageReporters: ['json'], - resetMocks: true, - restoreMocks: true, - testEnvironment: 'node', - testMatch: ['/build/transforms/**/*.test.js'], - testTimeout: 2500, -}; diff --git a/development/lib/build-type.js b/development/lib/build-type.js index 2d96557cc84a..621122921fb6 100644 --- a/development/lib/build-type.js +++ b/development/lib/build-type.js @@ -12,6 +12,7 @@ const { boolean, coerce, union, + number, unknown, validate, nullable, @@ -62,7 +63,26 @@ const EnvArrayStruct = unique( }, ); +/** + * Ensures a number is within a given range + * + * @param {number} min + * @param {number} max + */ +const isInRange = (min, max) => { + /** + * + * @param {number} value + * @returns boolean + */ + function check(value) { + return value >= min && value <= max; + } + return refine(number(), 'range', check); +}; + const BuildTypeStruct = object({ + id: isInRange(10, 64), features: optional(unique(array(string()))), env: optional(EnvArrayStruct), isPrerelease: optional(boolean()), diff --git a/development/lib/retry.js b/development/lib/retry.js index e6e5dfc040af..813a63aa44e4 100644 --- a/development/lib/retry.js +++ b/development/lib/retry.js @@ -11,12 +11,12 @@ * @param {string} [args.rejectionMessage] - The message for the rejected promise * this function will return in the event of failure. (Default: "Retry limit * reached") - * @param {boolean} [args.retryUntilFailure] - Retries until the function fails. + * @param {boolean} [args.stopAfterOneFailure] - Retries until the function fails. * @param {Function} functionToRetry - The function that is run and tested for * failure. * @returns {Promise<* | null | Error>} a promise that either resolves with one of the following: * - If successful, resolves with the return value of functionToRetry. - * - If functionToRetry fails while retryUntilFailure is true, resolves with null. + * - If functionToRetry fails while stopAfterOneFailure is true, resolves with null. * - Otherwise it is rejected with rejectionMessage. */ async function retry( @@ -24,7 +24,7 @@ async function retry( retries, delay = 0, rejectionMessage = 'Retry limit reached', - retryUntilFailure = false, + stopAfterOneFailure = false, }, functionToRetry, ) { @@ -36,7 +36,7 @@ async function retry( try { const result = await functionToRetry(); - if (!retryUntilFailure) { + if (!stopAfterOneFailure) { return result; } } catch (error) { @@ -46,18 +46,22 @@ async function retry( console.error('error caught in retry():', error); } - if (attempts < retries) { - console.log('Ready to retry() again'); + if (stopAfterOneFailure) { + throw new Error('Test failed. No more retries will be performed'); } - if (retryUntilFailure) { - return null; + if (attempts < retries) { + console.log('Ready to retry() again'); } } finally { attempts += 1; } } + if (stopAfterOneFailure) { + return null; + } + throw new Error(rejectionMessage); } diff --git a/development/lib/run-command.js b/development/lib/run-command.js index fab1eca361d4..62fe2284ea6d 100644 --- a/development/lib/run-command.js +++ b/development/lib/run-command.js @@ -96,7 +96,7 @@ async function runInShell(command, args, output) { const internalError = new Error('Internal'); try { await new Promise((resolve, reject) => { - const childProcess = spawn(command, args); + const childProcess = spawn(command, args, { shell: true }); childProcess.stdout.setEncoding('utf8'); childProcess.stderr.setEncoding('utf8'); childProcess.stdout.pipe(process.stdout); diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 5d9ef37850a8..f85d64faa887 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -38,12 +38,6 @@ function getPercentageChange(from, to) { return parseFloat(((to - from) / Math.abs(from)) * 100).toFixed(2); } -function getBetaVersion(commitMsg, defaultVersion = VERSION) { - const versionPattern = /Version\s(v\d+\.\d+\.\d+-beta\.\d+)/u; - - return commitMsg.match(versionPattern)?.[1] ?? defaultVersion; -} - async function start() { const { GITHUB_COMMENT_TOKEN, @@ -52,7 +46,6 @@ async function start() { CIRCLE_BUILD_NUM, CIRCLE_WORKFLOW_JOB_ID, PARENT_COMMIT, - SHA1_COMMIT_TITLE, } = process.env; console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST); @@ -82,9 +75,7 @@ async function start() { return `${platform}`; }) .join(', '); - - const betaVersion = getBetaVersion(SHA1_COMMIT_TITLE, VERSION); - const betaBuildLinks = `chrome`; + const betaBuildLinks = `chrome`; const flaskBuildLinks = platforms .map((platform) => { const url = diff --git a/development/sourcemap-validator.js b/development/sourcemap-validator.js index 231e582530ae..545751400aae 100755 --- a/development/sourcemap-validator.js +++ b/development/sourcemap-validator.js @@ -22,25 +22,38 @@ start().catch((error) => { async function start() { const targetFiles = [ - `common-0.js`, - `background-0.js`, - `ui-0.js`, - `scripts/contentscript.js`, - // `scripts/inpage.js`, skipped because the validator can't sample the inlined `scripts/inpage.js` script + 'background-0.js', + 'common-0.js', + 'content-script-0.js', + 'ui-0.js', + 'scripts/contentscript.js', + 'scripts/disable-console.js', + 'scripts/policy-load.js', + // TODO: Investigate why these are failing + // 'scripts/sentry-install.js', + // `scripts/inpage.js`, ]; + const optionalTargetFiles = ['scripts/app-init.js', 'offscreen-0.js']; let valid = true; for (const buildName of targetFiles) { const fileIsValid = await validateSourcemapForFile({ buildName }); valid = valid && fileIsValid; } + for (const buildName of optionalTargetFiles) { + const fileIsValid = await validateSourcemapForFile({ + buildName, + optional: true, + }); + valid = valid && fileIsValid; + } if (!valid) { process.exit(1); } } -async function validateSourcemapForFile({ buildName }) { +async function validateSourcemapForFile({ buildName, optional = false }) { console.log(`build "${buildName}"`); const platform = `chrome`; // load build and sourcemaps @@ -56,6 +69,12 @@ async function validateSourcemapForFile({ buildName }) { // empty } if (!rawBuild) { + if (optional) { + console.warn( + `SourcemapValidator - file not found, skipping "${buildName}"`, + ); + return true; + } throw new Error( `SourcemapValidator - failed to load source file for "${buildName}"`, ); diff --git a/development/ts-migration-dashboard/app/public/index.html b/development/ts-migration-dashboard/app/public/index.html index 2a196a45e656..67412836f505 100644 --- a/development/ts-migration-dashboard/app/public/index.html +++ b/development/ts-migration-dashboard/app/public/index.html @@ -2,7 +2,7 @@ - + Extension TypeScript Migration Status diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index ed5615e8c9d0..d5063250db16 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -641,10 +641,6 @@ "ui/components/app/selected-account/selected-account-component.test.js", "ui/components/app/selected-account/selected-account.component.js", "ui/components/app/selected-account/selected-account.container.js", - "ui/components/app/signature-request-original/index.js", - "ui/components/app/signature-request-original/signature-request-original.component.js", - "ui/components/app/signature-request-original/signature-request-original.container.js", - "ui/components/app/signature-request-original/signature-request-original.stories.js", "ui/components/app/signature-request-siwe/index.js", "ui/components/app/signature-request-siwe/signature-request-siwe-header/index.js", "ui/components/app/signature-request-siwe/signature-request-siwe-header/signature-request-siwe-header.js", diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js index fd679815d52f..75f9ca52cd4c 100755 --- a/development/verify-locale-strings.js +++ b/development/verify-locale-strings.js @@ -192,7 +192,7 @@ async function verifyEnglishLocale() { 'app/scripts/constants/**/*.js', 'app/scripts/constants/**/*.ts', 'app/scripts/platforms/**/*.js', - 'app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts', + 'app/scripts/controllers/**/*.ts', ], { ignore: [...globsToStrictSearch, testGlob], diff --git a/development/wdyr.ts b/development/wdyr.ts new file mode 100644 index 000000000000..a976e6bdc20c --- /dev/null +++ b/development/wdyr.ts @@ -0,0 +1,11 @@ +// eslint-disable-next-line spaced-comment +/// +import React from 'react'; + +if (process.env.ENABLE_WHY_DID_YOU_RENDER) { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const whyDidYouRender = require('@welldone-software/why-did-you-render'); + whyDidYouRender(React, { + trackAllPureComponents: true, + }); +} diff --git a/development/webpack/.eslintrc.js b/development/webpack/.eslintrc.js new file mode 100644 index 000000000000..1b142e9a39d1 --- /dev/null +++ b/development/webpack/.eslintrc.js @@ -0,0 +1,41 @@ +// this file is named .eslintrc.js because eslint checks for that file first + +module.exports = { + rules: { + '@typescript-eslint/no-shadow': [ + 'error', + { + allow: [ + // so uh, these aren't always globals, ya know. + 'describe', + 'it', + 'test', + 'afterEach', + 'beforeEach', + ], + }, + ], + // useful for lazy `require`s (makes start up faster) + '@typescript-eslint/no-require-imports': 'off', + // useful for modifying properties of `require`d modules (something `import`ed modules don't allow) + '@typescript-eslint/no-var-requires': 'off', + // Fun fact: ESM imports _require_ extensions. So silly. + 'import/extensions': 'off', + // sometimes its nice to do things like `something = else = null;` + 'no-multi-assign': ['error', { ignoreNonDeclaration: true }], + // Why? What's next, no addition? + 'no-bitwise': 'off', + // `void` is useful to ignore return values, the option `allowAsStatement: true` is broken for lambda functions, e.g., `() => void something()`. + 'no-void': 'off', + // `if (condition) return;` is useful for early returns without adding noise. + curly: ['error', 'multi-line'], + // require is required to load dynamic modules (well, JSON, mostly) synchronously (with Node's require cache, too!). + 'import/no-dynamic-require': 'off', + // uh, they're bullet points in markdown in a JSDoc comment. Stop this nonsense. + 'jsdoc/no-multi-asterisks': ['error', { allowWhitespace: true }], + // Really? I was joking about "no addition" above, but its (almost) real! + 'no-plusplus': 'off', + // I want to increment a variable outside my loop. This prevents that. + 'no-loop-func': 'off', + }, +}; diff --git a/development/webpack/README.md b/development/webpack/README.md new file mode 100644 index 000000000000..aa85fb67d444 --- /dev/null +++ b/development/webpack/README.md @@ -0,0 +1,214 @@ +# MetaMask Development Build Tool + +This tool is used to build the MetaMask extension for development purposes. It is not (yet) intended for production builds. + +## Usage + +For usage, examples, and options, run the following command: + +```bash +yarn webpack --help +``` + +To build the MetaMask extension, run the following command: + +```bash +yarn webpack +``` + +This will create a `dist/chrome` directory containing the built extension. See usage for more options. + +To watch for changes and rebuild the extension automatically, run the following command: + +```bash +yarn webpack --watch +``` + +### Set options using a `config.json` file + +You can skip using command line options and specify options using a JSON file +instead. You can use the same options as the command line, but in JSON form. For +example, to build a zip of the extension for Chrome and Firefox, create a +`config.json` file as follows (notice the use of an array for the `browser` +option): + +```json +{ + "browser": ["chrome", "firefox"], + "zip": true +} +``` + +Then you can use it as follows: + +```bash +yarn webpack --config config.json +``` + +And you can combine it with CLI options, too: + +```bash +yarn webpack --config config.json --dry-run +``` + +Run `yarn webpack --help` for the list of options. + +### Set options using environment variables + +You can use environment variables instead of command line options: + +```bash +BUNDLE_MINIFY=true yarn webpack +``` + +Run `yarn webpack --help` for the list of options. + +Note: multiple array options cannot be set this way, due to this bug in yargs: https://github.com/yargs/yargs/issues/821 + +You can also combine environment variables with `--config` and CLI options: + +```bash +BUNDLE_MINIFY=true yarn webpack --config config.json --dry-run +``` + +## Cache Invalidation + +The cache is invalidated when the build tool's code itself changes, or when the `package.json` file changes. The cache +is keyed by the effective options, so changing the options will also invalidate the cache. Not all options affect +the cache, but most do. Search for "`cacheKey`" in [./utils/cli.ts](./utils/cli.ts) to see which options affect the cache. + +## Tips + +- You can use the `--config` flag to specify your own JSON config file to use as the build configuration. This is useful + if you want to customize the defaults +- You can specify options via environment variables by prefixing the option name with `BUNDLE_`, e.g., + `BUNDLE_BROWSER=opera yarn webpack` on \*nix. +- don't run the build process with the Node Debugger attached; it will make things build much more slowly. + +## Development + +### Debugging the Build Process + +Webpack makes use of a cache to speed up builds. If you encounter issues with the build tool, try clearing the cache by +running the following command: + +```bash +yarn webpack:clearcache +``` + +You can also avoid using the cache by setting the `--no-cache` option. + +``` +yarn webpack --no-cache +``` + +Please to file an issue if you do encounter issues! + +### Linting + +Linting is exactly the same as the rest of the MetaMask project. To lint the build tool, run the following command: + +```bash +yarn lint +``` + +That said, the webpack build has its [own eslint configuration](./.eslintrc.js) that overrides some restrictive rules +that either: don't work well when optimizing for performance, or disable JavaScript features that are useful and +generally necessary. + +### Testing + +To run the build tool's test suite, run the following command: + +```bash +yarn test:unit:webpack +``` + +This will run the test suite for the build tool. These tests are also run as part of the MetaMask test suite in CI. + +To output an HTML, JSON, and text coverage reports, run the following command: + +```bash +yarn test:unit:webpack:coverage +``` + +Test coverage should be around 100% for the build tool, exceptions are made for some edge cases that are overly +difficult or complex to test, like exceptions. + +Testing uses node's built-in `node:test` and `node:assert` modules instead of jest/mocha. + +Unit tests are organized in `development/webpack/test` and are named `*.test.ts`, where \* is the name of the file being +tested. This is a guideline and not a rule. + +When checking coverage its is sometimes good to check if your coverage is intentional. One way to do that is to run the +test coverage on a single test file. This can be done by running the following command: + +```bash +yarn nyc --reporter=html tsx --test development/webpack/test/your-test-file.test.ts +``` + +### Performance + +The build tool only exists to build the project quickly. Don't make it slow. If you're adding a feature that makes the +build tool slower, go for a walk and maybe don't come back until you change your mind. + +Some things that might make the build tool slower: + +- using JavaScript (this tool is only fast because it uses [SWC](https://swc.rs/) for compilation, which is written in + Rust) +- requiring/importing large libraries +- functional programming paradigms (JavaScript is not Haskell after all) + - like chaining map, filter, reduce, etc. when a single loop would do. + - try to avoid looping over the same data/file multiple times +- using async IO when sync IO would do + - non-blocking IO is great, but not when it's the only IO happening at the time and we don't care about blocking the + main process. +- launching shells, workers, or other processes without measuring the cost +- unnecessary IO +- validation, linting, or other checks that are not necessary + +If you must add something that slows it down, consider putting it behind a flag. If it must be in the default mode, try +to run it in parallel with other tasks. + +### The Cache + +The build process uses a cache to speed up successive builds. The cache is stored in the `node_modules/.cache/webpack` +directory. + +The cache is slow. Very slow. It takes about 50% of the total time just to create the cache. But you shouldn't notice +that because the caching step is pushed to a background process. + +The way this works is by running the build in a background child process, and then detaching that child process from the +parent process once the build is complete (and cache reconciliation and persistance begins). + +Launching the build in a background process does take time, but its much less time than cache creation, so it works out. + +The child process is run with its own TTY for `stderr` and `stdout`; the child's stdio dimensions are kept in sync with +the parent's, and all TTY features of the parent are available in the child (formatting, colors, cursors, etc.). On +Windows an IPC channel is used to communicate between the parent and child processes, on \*nix this is done via signals. +The parent process listens for the child process and signal the parent, and when it does, the parent disconnects from +the child and shuts down, leaving the child to run in the background so the cache can be processed and persisted. + +### To do: + +- [define and wrangle the difference between `lockdown` and `lavamoat` options.](https://github.com/MetaMask/metamask-extension/issues/26254) +- [MV3 support](https://github.com/MetaMask/metamask-extension/issues/26255) + - Service workers, used by MV3, must load dependencies via `importScripts`. + - there are existing webpack plugins that do this, but they are not yet integrated into this build tool and would + require changes to our code and existing gulp-based build process to work. +- [Make lavamoat work so we can run production builds](https://github.com/MetaMask/metamask-extension/issues/26256) +- [Make LiveReload, Hot Module Reloading, and/or React Refresh work](https://github.com/MetaMask/metamask-extension/issues/26257) + - prerequisite: https://github.com/MetaMask/metamask-extension/issues/22450 +- [Make the build tool even faster (switch to RSPack once it hits 1.0.0?)](https://github.com/MetaMask/metamask-extension/issues/26258) +- [enable `yarn webpack completion`](https://github.com/MetaMask/metamask-extension/issues/26259) + - It doesn't work with multiple-word commands (`yarn webpack ...`) and is currently disabled. +- [implement overrides for icons and manifests fields for non-main builds](https://github.com/MetaMask/metamask-extension/issues/26260) + +### Ideas + +- investigate using `DLLPlugin` for even faster builds +- make it work in Bun.js and/or Deno +- investigate adding a long-running background daemon mode for always up-to-date builds +- investigate adding linting, testing, validation, AI code review, etc.; especially in `--watch` mode +- investigate a "one CLI to rule them all" approach to MetaMask developer tooling and scripts +- allow changing some options without restarting the build process diff --git a/development/webpack/build.ts b/development/webpack/build.ts new file mode 100644 index 000000000000..12ce2c95e693 --- /dev/null +++ b/development/webpack/build.ts @@ -0,0 +1,62 @@ +import { webpack } from 'webpack'; +import type WebpackDevServerType from 'webpack-dev-server'; +import { noop, logStats, __HMR_READY__ } from './utils/helpers'; +import config from './webpack.config.js'; + +// disable browserslist stats as it needlessly traverses the filesystem multiple +// times looking for a stats file that doesn't exist. +require('browserslist/node').getStat = noop; + +/** + * Builds the extension + * + * @param onComplete + */ +export function build(onComplete: () => void = noop) { + const isDevelopment = config.mode === 'development'; + + const { watch, ...options } = config; + const compiler = webpack(options); + if (__HMR_READY__ && watch) { + // DISABLED BECAUSE WE AREN'T `__HMR_READY__` YET + // Use `webpack-dev-server` to enable HMR + const WebpackDevServer: typeof WebpackDevServerType = require('webpack-dev-server'); + const serverOptions = { + hot: isDevelopment, + liveReload: isDevelopment, + server: { + // TODO: is there any benefit to using https? + type: 'https', + }, + // always use loopback, as 0.0.0.0 tends to fail on some machines (WSL2?) + host: 'localhost', + devMiddleware: { + // browsers need actual files on disk + writeToDisk: true, + }, + // we don't need/have a "static" directory, so disable it + static: false, + allowedHosts: 'all', + } as const satisfies WebpackDevServerType.Configuration; + + const server = new WebpackDevServer(serverOptions, compiler); + server.start().then(() => console.log('🦊 Watching for changes…')); + } else { + console.error(`🦊 Running ${options.mode} build…`); + if (watch) { + // once HMR is ready (__HMR_READY__ variable), this section should be removed. + compiler.watch(options.watchOptions, (err, stats) => { + logStats(err ?? undefined, stats); + console.error('🦊 Watching for changes…'); + }); + } else { + compiler.run((err, stats) => { + logStats(err ?? undefined, stats); + // `onComplete` must be called synchronously _before_ `compiler.close` + // or the caller might observe output from the `close` command. + onComplete(); + compiler.close(noop); + }); + } + } +} diff --git a/development/webpack/fork.mts b/development/webpack/fork.mts new file mode 100644 index 000000000000..73fb7c4a7e4d --- /dev/null +++ b/development/webpack/fork.mts @@ -0,0 +1,27 @@ +/** + * @file Executes the build process in a child process environment, ensuring it + * was correctly spawned by checking for a `PPID` environment variable that + * matches the parent's process ID. This script is responsible for running the + * build logic defined in './build' and managing output streams to prevent + * unwanted output after completion. It leverages IPC for communication back to + * the parent process or falls back to sending a POSIX signal (`SIGUSR2`) to + * signal completion. + * @see {@link ./launch.ts} + */ + +const PPID = Number(process.env.PPID); +if (isNaN(PPID) || PPID !== process.ppid) { + throw new Error( + `${__filename} must be run with a \`PPID\` environment variable. See ${__dirname}/launch.ts for an example.`, + ); +} + +const { build } = await import('./build.ts'); +build(() => { + // stop writing now because the parent process is still listening to these + // streams and we don't want any more output to be shown to the user. + process.stdout.write = process.stderr.write = () => true; + + // use IPC if we have it, otherwise send a POSIX signal + process.send?.('SIGUSR2') || process.kill(PPID, 'SIGUSR2'); +}); diff --git a/development/webpack/launch.ts b/development/webpack/launch.ts new file mode 100755 index 000000000000..acf1149045fe --- /dev/null +++ b/development/webpack/launch.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env -S node --require "./node_modules/tsx/dist/preflight.cjs" --import "./node_modules/tsx/dist/loader.mjs" + +/** + * @file This script optimizes build processes by conditionally forking child + * processes based on command-line arguments. It handles memory management, + * stdio stream creation, and process lifecycle to improve performance and + * maintainability. Supports cross-platform execution with specific + * considerations for Windows environments. + * + * On Linux-like systems you can skip the overhead of running `yarn` by + * executing this file directly, e.g., `./development/webpack/launch.ts`, or via + * bun or tsx. + */ + +// Note: minimize non-`type` imports to decrease load time. +import { join } from 'node:path'; +import { spawn, type StdioOptions } from 'node:child_process'; +import parser from 'yargs-parser'; +import type { Child, PTY, Stdio, StdName } from './types.ts'; + +const rawArgv = process.argv.slice(2); + +const alias = { cache: 'c', help: 'h', watch: 'h' }; +type Args = { [x in keyof typeof alias]?: boolean }; +const args = parser(rawArgv, { alias, boolean: Object.keys(alias) }) as Args; + +if (args.cache === false || args.help === true || args.watch === true) { + // there are no time savings to running the build in a child process if: the + // cache is disabled, we need to output "help", or we're in watch mode. + require('./build.ts').build(); +} else { + fork(process, join(__dirname, 'fork.mts'), rawArgv); +} + +/** + * Runs the `file` in a child process. This allows the parent process to + * exit as soon as the build completes, but lets the child process continue to + * serialize and persist the cache in the background. + * + * @param process - The parent process, like `globalThis.process` + * @param file - Path to the file to run, given as an argument to the command + * @param argv - Arguments to pass to the executable + */ +function fork(process: NodeJS.Process, file: string, argv: string[]) { + const env = { NODE_OPTIONS: '', ...process.env, PPID: `${process.pid}` }; + // node recommends using 75% of the available memory for `max-old-space-size` + // https://github.com/nodejs/node/blob/dd67bf08cb1ab039b4060d381cc68179ee78701a/doc/api/cli.md#--max-old-space-sizesize-in-megabytes + const maxOldSpaceMB = ~~((require('node:os').totalmem() * 0.75) / (1 << 20)); + // `--huge-max-old-generation-size` and `--max-semi-space-size=128` reduce + // garbage collection pauses; 128MB provided max benefit in perf testing. + const nodeOptions = [ + `--max-old-space-size=${maxOldSpaceMB}`, + '--max-semi-space-size=128', + '--huge-max-old-generation-size', + ]; + + // run the build in a child process so that we can exit the parent process as + // soon as the build completes, but let the cache serialization finish in the + // background (the cache can take 30% of build-time to serialize and persist). + const { connectToChild, destroy, stdio } = createOutputStreams(process); + + const node = process.execPath; + const options = { detached: true, env, stdio }; + spawn(node, [...nodeOptions, ...process.execArgv, file, ...argv], options) + .once('close', destroy) // clean up if the child crashes + .once('spawn', connectToChild); +} + +/** + * Create the stdio streams (stderr and stdout) for the child process to use and + * for the parent to control and listen to. + * + * @param process - The parent process, like `globalThis.process` + * @returns The stdio streams for the child process to use + */ +function createOutputStreams(process: NodeJS.Process) { + const { isatty } = require('node:tty'); + const isWindows = process.platform === 'win32'; + // use IPC for communication on Windows, as it doesn't support POSIX signals + const ipc = isWindows ? 'ipc' : 'ignore'; + const outs = (['stdout', 'stderr'] as const).map(function createStream(name) { + const parentStream = process[name]; + // TODO: get Windows PTY working + return !isWindows && isatty(parentStream.fd) + ? createTTYStream(parentStream) + : createNonTTYStream(parentStream, name); + }) as [Stdio, Stdio]; + + return { + /** + * + * @param this + * @param child + */ + connectToChild(this: Child, child = this) { + // hook up the child's stdio to the parent's & unref so we can exit later + outs.forEach((stream) => { + stream.listen(child); + stream.unref(child); + }); + + listenForShutdownSignal(process, child); + + process + // kill the child process if we didn't exit cleanly + .on('exit', (code) => code > 128 && child.kill(code - 128)) + // `SIGWINCH` means the terminal was resized + .on('SIGWINCH', function handleSigwinch(signal) { + // resize the tty's + outs.forEach((out) => out.resize()); + // then tell the child process to update its dimensions + child.kill(signal); + }); + }, + destroy: () => outs.forEach((out) => out.destroy()), + stdio: ['ignore', outs[0].pty, outs[1].pty, ipc] as StdioOptions, + }; +} + +/** + * Create a non-TTY (pipe) stream for the child process to use as its stdio. + * + * @param stream - The parent process's stdio stream + * @param name - Either `stdout` or `stderr` + * @returns The stream for the child process to use + */ +function createNonTTYStream(stream: NodeJS.WriteStream, name: StdName): Stdio { + return { + destroy: () => undefined, + listen: (child: Child) => void child[name].pipe(stream), + pty: 'pipe', // let Node create the Pipes + resize: () => undefined, + unref: (child: Child) => void child[name].unref(), + }; +} + +/** + * Create a PTY stream for the child process to use as its stdio. + * + * @param stream - The parent process's stdio stream + * @returns The PTY stream for the child process to use + */ +function createTTYStream(stream: NodeJS.WriteStream): Stdio { + // create a PTY (Pseudo TTY) so the child stream behaves like a TTY + const options = { cols: stream.columns, encoding: null, rows: stream.rows }; + const pty: PTY = require('@lydell/node-pty').open(options); + + return { + destroy: () => { + pty.master.destroy(); + pty.slave.destroy(); + }, + listen: (_child: Child) => void pty.master.pipe(stream), + pty: pty.slave, + resize: () => pty.resize(stream.columns, stream.rows), + unref: (_child: Child) => { + pty.master.unref(); + pty.slave.unref(); + }, + }; +} + +/** + * Listens for a shutdown signal either on the child's IPC channel or via the + * parent process's `SIGUSR2` event. When the signal is received, the child + * process is unref'd so that it can continue running in the background. + * + * Once the child process is unref'd, the parent process may exit on its own. + * + * @param process - The parent process, like `globalThis.process` + * @param child - The child process to listen to + */ +function listenForShutdownSignal(process: NodeJS.Process, child: Child) { + // exit gracefully when the child signals the parent via `SIGUSR2` + if (child.channel === null || child.channel === undefined) { + process.on('SIGUSR2', () => child.unref()); + } else { + child.channel.unref(); + child.on('message', (signal) => signal === 'SIGUSR2' && child.unref()); + } +} diff --git a/development/webpack/test/cli.test.ts b/development/webpack/test/cli.test.ts new file mode 100644 index 000000000000..7f9d5f2fea53 --- /dev/null +++ b/development/webpack/test/cli.test.ts @@ -0,0 +1,83 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { getDryRunMessage, parseArgv } from '../utils/cli'; +import { getBuildTypes } from '../utils/config'; +import { Browsers } from '../utils/helpers'; + +describe('./utils/cli.ts', () => { + const defaultArgs = { + env: 'development', + watch: false, + cache: true, + progress: true, + releaseVersion: 0, + devtool: 'source-map', + sentry: false, + test: false, + zip: false, + minify: false, + browser: ['chrome'], + manifest_version: 2, + type: 'main', + lavamoat: false, + lockdown: false, + snow: false, + dryRun: false, + stats: false, + }; + + it('should return defaults', () => { + const { args, cacheKey, features } = parseArgv([], getBuildTypes()); + assert.deepStrictEqual(args, defaultArgs); + assert.strictEqual( + typeof cacheKey, + 'string', + 'cacheKey should be a string', + ); + assert(cacheKey.length > 0, 'cacheKey should not be empty'); + // features come from build.yml, and change often, so let's just check the shape + assert(features, 'features should be defined'); + assert(features.all instanceof Set, 'features.all should be a Set'); + assert(features.active instanceof Set, 'features.active should be a Set'); + }); + + it('getDryRunMessage', () => { + const { args, features } = parseArgv([], getBuildTypes()); + const message = getDryRunMessage(args, features); + // testing the exact message could be nice, but verbose and maybe a bit + // brittle, so we just check that it returns a string + assert.strictEqual( + typeof message, + 'string', + 'Dry run message should be a string', + ); + assert(message.length > 0, 'Dry run message should not be empty'); + }); + + it('should allow for build types with no features', () => { + const buildTypesConfig = getBuildTypes(); + delete buildTypesConfig.buildTypes.main.features; + const { features } = parseArgv([], buildTypesConfig); + assert.strictEqual( + features.active.size, + 0, + 'features.active should be an empty Set', + ); + }); + + it('should allow for a build type with no features section', () => { + const buildTypesConfig = getBuildTypes(); + delete buildTypesConfig.buildTypes.main.features; + const { features } = parseArgv([], buildTypesConfig); + assert.strictEqual( + features.active.size, + 0, + 'features.active should be an empty Set', + ); + }); + + it('should return all browsers when `--browser all` is specified', () => { + const { args } = parseArgv(['--browser', 'all'], getBuildTypes()); + assert.deepStrictEqual(args.browser, Browsers); + }); +}); diff --git a/development/webpack/test/config.test.ts b/development/webpack/test/config.test.ts new file mode 100644 index 000000000000..c19e4871fbb8 --- /dev/null +++ b/development/webpack/test/config.test.ts @@ -0,0 +1,110 @@ +import fs from 'node:fs'; +import { describe, it, after, mock } from 'node:test'; +import assert from 'node:assert'; +import { resolve } from 'node:path'; +import * as config from '../utils/config'; +import { parseArgv } from '../utils/cli'; +import { version } from '../../../package.json'; + +describe('./utils/config.ts', () => { + // variables logic is complex, and is "owned" mostly by the other build + // system, so we don't check for everything, just that the interface is + // behaving + describe('variables', () => { + const originalReadFileSync = fs.readFileSync; + function mockRc(env: Record = {}) { + mock.method(fs, 'readFileSync', (path: string, options: object) => { + if (path === resolve(__dirname, '../../../.metamaskrc')) { + // mock `.metamaskrc`, as users might have customized it which may + // break our tests + return ` +${Object.entries(env) + .map(([key, value]) => `${key}=${value}`) + .join('\n')} +`; + } + return originalReadFileSync(path, options); + }); + } + after(() => mock.restoreAll()); + + it('should return valid build variables for the default build', () => { + const buildTypes = config.getBuildTypes(); + const { args } = parseArgv([], buildTypes); + const { variables, safeVariables } = config.getVariables( + args, + buildTypes, + ); + + assert.strictEqual(variables.get('METAMASK_VERSION'), version); + assert.strictEqual(variables.get('IN_TEST'), args.test); + assert.strictEqual(variables.get('METAMASK_BUILD_TYPE'), args.type); + assert.strictEqual(variables.get('NODE_ENV'), args.env); + + // PPOM_URI is unique in that it is code, and has not been JSON.stringified, so we check it separately: + assert.strictEqual( + safeVariables.PPOM_URI, + `new URL('@blockaid/ppom_release/ppom_bg.wasm', import.meta.url)`, + ); + }); + + it('should prefer .metamaskrc variables over others', () => { + const buildTypes = config.getBuildTypes(); + const { args } = parseArgv([], buildTypes); + const defaultVars = config.getVariables(args, buildTypes); + + // verify the default value of the main build is false + assert.strictEqual(defaultVars.variables.get('ALLOW_LOCAL_SNAPS'), false); + + mockRc({ + ALLOW_LOCAL_SNAPS: 'true', + }); + + const overrides = config.getVariables(args, buildTypes); + + // verify the value of the main build is set to the value in .metamaskrc + assert.strictEqual(overrides.variables.get('ALLOW_LOCAL_SNAPS'), true); + }); + + it('should return valid build variables for a non-default build', () => { + mockRc({ + // required by the `beta` build type + SEGMENT_BETA_WRITE_KEY: '.', + }); + const buildTypes = config.getBuildTypes(); + const { args } = parseArgv( + ['--type', 'beta', '--test', '--env', 'production'], + buildTypes, + ); + const { variables } = config.getVariables(args, buildTypes); + assert.strictEqual( + variables.get('METAMASK_VERSION'), + `${version}-${args.type}.0`, + ); + assert.strictEqual(variables.get('IN_TEST'), args.test); + assert.strictEqual(variables.get('METAMASK_BUILD_TYPE'), args.type); + assert.strictEqual(variables.get('NODE_ENV'), args.env); + }); + + it("should handle true/false/null/'' in rc", () => { + const buildTypes = config.getBuildTypes(); + const { args } = parseArgv([], buildTypes); + + mockRc({ + TESTING_TRUE: 'true', + TESTING_FALSE: 'false', + TESTING_NULL: 'null', + TESTING_MISC: 'MISC', + TESTING_EMPTY_STRING: '', + }); + + const { variables } = config.getVariables(args, buildTypes); + + assert.strictEqual(variables.get('TESTING_TRUE'), true); + assert.strictEqual(variables.get('TESTING_FALSE'), false); + assert.strictEqual(variables.get('TESTING_NULL'), null); + assert.strictEqual(variables.get('TESTING_MISC'), 'MISC'); + assert.strictEqual(variables.get('TESTING_EMPTY_STRING'), null); + }); + }); +}); diff --git a/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/_base.json b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/_base.json new file mode 100644 index 000000000000..46c463a53ef8 --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/_base.json @@ -0,0 +1,4 @@ +{ + "description": "base description", + "web_accessible_resources": ["file.png"] +} diff --git a/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/chrome.json b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/chrome.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v2/chrome.json @@ -0,0 +1 @@ +{} diff --git a/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/_base.json b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/_base.json new file mode 100644 index 000000000000..57cd1ac506d9 --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/_base.json @@ -0,0 +1,10 @@ +{ + "description": "base description", + "manifest_version": 3, + "web_accessible_resources": [ + { + "matches": [""], + "resources": ["file.png"] + } + ] +} diff --git a/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/chrome.json b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/chrome.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/complex/manifest/v3/chrome.json @@ -0,0 +1 @@ +{} diff --git a/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/_base.json b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/_base.json new file mode 100644 index 000000000000..5e74046537e7 --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/_base.json @@ -0,0 +1,3 @@ +{ + "description": "base description" +} diff --git a/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/chrome.json b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/chrome.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v2/chrome.json @@ -0,0 +1 @@ +{} diff --git a/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/_base.json b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/_base.json new file mode 100644 index 000000000000..4869ffc9cb61 --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/_base.json @@ -0,0 +1,4 @@ +{ + "description": "base description", + "manifest_version": 3 +} diff --git a/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/chrome.json b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/chrome.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/development/webpack/test/fixtures/ManifestPlugin/empty/manifest/v3/chrome.json @@ -0,0 +1 @@ +{} diff --git a/development/webpack/test/fixtures/git/HEAD b/development/webpack/test/fixtures/git/HEAD new file mode 100644 index 000000000000..b870d82622c1 --- /dev/null +++ b/development/webpack/test/fixtures/git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/development/webpack/test/fixtures/git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/development/webpack/test/fixtures/git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 000000000000..adf64119a33d Binary files /dev/null and b/development/webpack/test/fixtures/git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 differ diff --git a/development/webpack/test/fixtures/git/objects/63/4cad6dd342dc5317b6c1677dc9ead3fb72f680 b/development/webpack/test/fixtures/git/objects/63/4cad6dd342dc5317b6c1677dc9ead3fb72f680 new file mode 100644 index 000000000000..332274685e38 Binary files /dev/null and b/development/webpack/test/fixtures/git/objects/63/4cad6dd342dc5317b6c1677dc9ead3fb72f680 differ diff --git a/development/webpack/test/fixtures/git/refs/heads/main b/development/webpack/test/fixtures/git/refs/heads/main new file mode 100644 index 000000000000..38d8162043b8 --- /dev/null +++ b/development/webpack/test/fixtures/git/refs/heads/main @@ -0,0 +1 @@ +634cad6dd342dc5317b6c1677dc9ead3fb72f680 diff --git a/development/webpack/test/git.test.ts b/development/webpack/test/git.test.ts new file mode 100644 index 000000000000..ee7d9bef65d1 --- /dev/null +++ b/development/webpack/test/git.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { join } from 'node:path'; +import { getLatestCommit } from '../utils/git'; + +describe('getLatestCommit', () => { + const gitDir = join(__dirname, '.', 'fixtures', 'git'); + it('should return some values by default', () => { + const { hash, timestamp } = getLatestCommit(); + + assert.strictEqual(hash().length, 8, 'The hash length is wrong'); + assert.ok(typeof timestamp() === 'number', 'The timestamp type is wrong'); + }); + + it('should return the latest commit hash and timestamp', () => { + const { hash, timestamp } = getLatestCommit(gitDir); + + assert.strictEqual(hash(), '634cad6d', 'The hash is wrong'); + assert.strictEqual(timestamp(), 1711385030000, 'The timestamp is wrong'); + }); + + it('should use the cache', () => { + const firstCallCustom = getLatestCommit(gitDir); + const firstCallDefault = getLatestCommit(); + const secondCallCustom = getLatestCommit(gitDir); + const secondCallDefault = getLatestCommit(); + + assert.notStrictEqual(firstCallCustom, firstCallDefault); + assert.notStrictEqual(secondCallCustom, secondCallDefault); + assert.strictEqual(firstCallCustom, secondCallCustom); + assert.strictEqual(firstCallDefault, secondCallDefault); + }); +}); diff --git a/development/webpack/test/helpers.test.ts b/development/webpack/test/helpers.test.ts new file mode 100644 index 000000000000..fc1955eec3c6 --- /dev/null +++ b/development/webpack/test/helpers.test.ts @@ -0,0 +1,371 @@ +import fs from 'node:fs'; +import { describe, it, afterEach, beforeEach, mock } from 'node:test'; +import assert from 'node:assert'; +import { join } from 'node:path'; +import { + version, + type Chunk, + type Stats, + type Compilation, + type StatsOptions, + type StatsCompilation, +} from 'webpack'; +import * as helpers from '../utils/helpers'; +import { type Combination, generateCases } from './helpers'; + +describe('./utils/helpers.ts', () => { + afterEach(() => mock.restoreAll()); + + it('should return undefined when noop it called', () => { + const nothing = helpers.noop(); + assert.strictEqual(nothing, undefined); + }); + + it('should return all entries listed in the manifest and file system for manifest_version 2', () => { + const originalReaddirSync = fs.readdirSync; + const otherHtmlEntries = ['one.html', 'two.html']; + const appRoot = ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mock.method(fs, 'readdirSync', function (path: string, options: any) { + if (path === appRoot) { + return [...otherHtmlEntries, 'three.not-html']; + } + return originalReaddirSync.call(fs, path, options); + }); + + const manifest = { + manifest_version: 2, + background: { + scripts: ['background.js'], + page: 'background.html', + }, + browser_action: { + // use one from `otherHtmlEntries`, to ensure we don't duplicate things + default_popup: otherHtmlEntries[0], + }, + // images/test.ing.png will be omitted from entry points + web_accessible_resources: ['images/test.ing.png', 'testing.js'], + content_scripts: [ + { + matches: ['file://*/*', 'http://*/*', 'https://*/*'], + js: ['scripts/contentscript.js', 'scripts/inpage.js'], + run_at: 'document_start', + all_frames: true, + }, + { + matches: ['*://connect.trezor.io/*/popup.html'], + js: ['vendor/trezor/content-script.js'], + }, + ], + } as helpers.ManifestV2; + const { entry, canBeChunked } = helpers.collectEntries(manifest, appRoot); + const expectedScripts = { + 'background.js': { + chunkLoading: false, + filename: 'background.js', + import: join(appRoot, `background.js`), + }, + 'scripts/contentscript.js': { + chunkLoading: false, + filename: 'scripts/contentscript.js', + import: join(appRoot, `scripts/contentscript.js`), + }, + 'scripts/inpage.js': { + chunkLoading: false, + filename: 'scripts/inpage.js', + import: join(appRoot, `/scripts/inpage.js`), + }, + 'vendor/trezor/content-script.js': { + chunkLoading: false, + filename: 'vendor/trezor/content-script.js', + import: join(appRoot, `vendor/trezor/content-script.js`), + }, + 'testing.js': { + chunkLoading: false, + filename: 'testing.js', + import: join(appRoot, `testing.js`), + }, + }; + const expectedHtml = { + background: join(appRoot, `background.html`), + one: join(appRoot, `one.html`), + two: join(appRoot, `two.html`), + // notice: three.not-html is NOT included, since it doesn't have an `.html` extension + }; + const expectedEntries = { ...expectedScripts, ...expectedHtml }; + assert.deepStrictEqual(entry, expectedEntries); + + const jsFiles = Object.keys(entry).filter((key) => key.endsWith('.js')); + assert(jsFiles.length > 0, "JS files weren't found in the manifest"); + jsFiles.forEach((name) => { + assert.strictEqual(canBeChunked({ name } as Chunk), false); + }); + + // scripts that are *not* in our manifest *can* be chunked + assert.strictEqual(canBeChunked({ name: 'anything.js' } as Chunk), true); + }); + + it('should return all entries listed in the manifest and file system for manifest_version 3', () => { + const originalReaddirSync = fs.readdirSync; + const otherHtmlEntries = ['one.html', 'two.html']; + const appRoot = ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mock.method(fs, 'readdirSync', (path: string, options: any) => { + if (path === appRoot) { + return [...otherHtmlEntries, 'three.not-html']; + } + return originalReaddirSync.call(fs, path, options); + }); + + const manifest = { + name: 'MetaMask', + version: '1.0.0', + manifest_version: 3, + background: { + service_worker: 'background.js', + }, + web_accessible_resources: [ + { + matches: [''], + // images/test.ing.png will be omitted from entry points + resources: ['images/test.ing.png', 'testing.js'], + }, + ], + browser_action: { + // use one from `otherHtmlEntries`, to ensure we don't duplicate things + default_popup: otherHtmlEntries[0], + }, + content_scripts: [ + { + matches: ['file://*/*', 'http://*/*', 'https://*/*'], + js: ['scripts/contentscript.js'], + run_at: 'document_start', + all_frames: true, + }, + { + matches: ['*://connect.trezor.io/*/popup.html'], + js: ['vendor/trezor/content-script.js'], + }, + ], + } as helpers.ManifestV3; + const { entry, canBeChunked } = helpers.collectEntries(manifest, appRoot); + const expectedScripts = { + 'scripts/contentscript.js': { + chunkLoading: false, + filename: 'scripts/contentscript.js', + import: join(appRoot, `scripts/contentscript.js`), + }, + 'vendor/trezor/content-script.js': { + chunkLoading: false, + filename: 'vendor/trezor/content-script.js', + import: join(appRoot, `vendor/trezor/content-script.js`), + }, + 'background.js': { + chunkLoading: false, + filename: 'background.js', + import: join(appRoot, `background.js`), + }, + 'testing.js': { + chunkLoading: false, + filename: 'testing.js', + import: join(appRoot, `testing.js`), + }, + }; + const expectedHtml = { + one: join(appRoot, `one.html`), + two: join(appRoot, `two.html`), + // notice: three.not-html is NOT included, since it doesn't have an `.html` extension + }; + const expectedEntries = { + ...expectedScripts, + ...expectedHtml, + }; + assert.deepStrictEqual(entry, expectedEntries); + + const jsFiles = Object.keys(entry).filter((key) => key.endsWith('.js')); + assert(jsFiles.length > 0, "JS files weren't found in the manifest"); + jsFiles.forEach((name) => { + assert.strictEqual(canBeChunked({ name } as Chunk), false); + }); + + // scripts that are *not* in our manifest *can* be chunked + assert.strictEqual(canBeChunked({ name: 'anything.js' } as Chunk), true); + }); + + it('should handle manifest.json files with empty sections', () => { + const originalReaddirSync = fs.readdirSync; + const appRoot = ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mock.method(fs, 'readdirSync', (path: string, options: any) => { + if (path === appRoot) { + return []; + } + return originalReaddirSync.call(fs, path, options); + }); + + const manifestv2 = { + manifest_version: 2, + background: {}, + } as helpers.ManifestV2; + const { entry: entryv2 } = helpers.collectEntries(manifestv2, appRoot); + assert.deepStrictEqual(entryv2, {}); + + const manifestv3 = { + name: 'MetaMask', + version: '1.0.0', + manifest_version: 3, + background: {}, + } as helpers.ManifestV3; + const { entry: entryv3 } = helpers.collectEntries(manifestv3, appRoot); + assert.deepStrictEqual(entryv3, {}); + }); + + it('should throw if an entry file starts with an underscore', () => { + const manifest = { + manifest_version: 2, + background: { + page: '_badfile.html', + }, + } as helpers.ManifestV2; + assert.throws( + () => helpers.collectEntries(manifest, ''), + /Error: Invalid Entrypoint Filename Detected/u, + ); + }); + + describe('logStats', () => { + const getStatsMock = ( + stats: 'normal' | 'none', + mode: 'development' | 'production', + hasError: boolean, + hasWarning: boolean, + ) => { + return { + hash: 'test-hash', + toJson: null as unknown as () => StatsCompilation, + endTime: 1000, + startTime: 0, + hasErrors: mock.fn(() => hasError), + hasWarnings: mock.fn(() => hasWarning), + compilation: { + options: { + mode, + stats, + }, + compiler: { + name: 'test-compiler-name', + }, + } as Compilation, + toString: mock.fn((_?: unknown) => 'test-stats'), + } as const satisfies Stats; + }; + + it('should log nothing if err and stats are both not defined', () => { + const { mock: error } = mock.method(console, 'error', helpers.noop); + helpers.logStats(undefined, undefined); + assert.strictEqual(error.callCount(), 0, 'error should not be called'); + }); + + it('should log only the error when error and stats are provided', () => { + const stats = getStatsMock('normal', 'production', false, false); + const { mock: error } = mock.method(console, 'error', helpers.noop); + const errorToLog = new Error('test error'); + + // should only log the error, and nothing else + helpers.logStats(errorToLog, stats); + + assert.strictEqual(error.callCount(), 1, 'error should be called'); + assert.deepStrictEqual( + error.calls[0].arguments, + [errorToLog], + 'error should be logged', + ); + assert.strictEqual( + stats.toString.mock.callCount(), + 0, + 'stats.toString should not be called', + ); + }); + + const matrix = { + colorDepth: [undefined, 1, 4, 8, 24] as const, + level: ['normal', 'none'] as const, + env: ['development', 'production'] as const, + hasErrors: [true, false] as const, + hasWarnings: [true, false] as const, + }; + + generateCases(matrix).forEach(runTest); + + function runTest(settings: Combination) { + const { colorDepth, level, env, hasErrors, hasWarnings } = settings; + + let testHelpers: typeof import('../utils/helpers'); + const originalGetColorDepth = process.stderr.getColorDepth; + beforeEach(() => { + // getColorDepth is undefined sometimes, so we need to mock it like this + process.stderr.getColorDepth = ( + colorDepth ? mock.fn(() => colorDepth) : colorDepth + ) as (env?: object | undefined) => number; + + // helpers caches `getColorDepth` on initialization, so we need to a new + // one after we mock `getColorDepth`. + delete require.cache[require.resolve('../utils/helpers')]; + testHelpers = require('../utils/helpers'); + }); + + afterEach(() => { + process.stderr.getColorDepth = originalGetColorDepth; + }); + + it(`should log message when stats is "${level}" and env is "${env}", with errors: \`${hasErrors}\` and warnings: \`${hasWarnings}\``, () => { + const stats = getStatsMock(level, env, hasErrors, hasWarnings); + const { mock: error } = mock.method(console, 'error', testHelpers.noop); + + testHelpers.logStats(null, stats); // <- this is what we are testing + + assert.strictEqual(error.callCount(), 1, 'error should be called once'); + + let toStringOptions: StatsOptions | undefined; + if (level === 'normal') { + toStringOptions = { colors: testHelpers.colors }; + } else if (hasErrors || hasWarnings) { + toStringOptions = { + colors: testHelpers.colors, + preset: 'errors-warnings', + }; + } + if (toStringOptions) { + assert.strictEqual( + stats.toString.mock.callCount(), + 1, + 'stats.toString should be called once', + ); + assert.deepStrictEqual( + stats.toString.mock.calls[0].arguments, + [toStringOptions], + 'stats should be called with the colors option', + ); + assert.deepStrictEqual( + error.calls[0].arguments, + [stats.toString(toStringOptions)], + 'stats should be logged', + ); + } else { + assert.strictEqual( + stats.toString.mock.callCount(), + 0, + 'stats.toString should not be called', + ); + const colorFn = + env === 'production' ? testHelpers.toOrange : testHelpers.toPurple; + const name = colorFn(`🦊 ${stats.compilation.compiler.name}`); + const status = testHelpers.toGreen('successfully'); + const time = stats.endTime - stats.startTime; + const expectedMessage = `${name} (webpack ${version}) compiled ${status} in ${time} ms`; + assert.deepStrictEqual(error.calls[0].arguments, [expectedMessage]); + } + }); + } + }); +}); diff --git a/development/webpack/test/helpers.ts b/development/webpack/test/helpers.ts new file mode 100644 index 000000000000..b57ec7b46a6e --- /dev/null +++ b/development/webpack/test/helpers.ts @@ -0,0 +1,115 @@ +import { mock } from 'node:test'; +import { + sources, + type Compiler, + type Chunk, + type WebpackOptionsNormalized, + type Asset, + type Compilation, +} from 'webpack'; + +const { SourceMapSource, RawSource } = sources; + +type Assets = { [k: string]: unknown }; + +export type Combination = { + [P in keyof T]: T[P] extends readonly (infer U)[] ? U : never; +}; + +export function generateCases(obj: T): Combination[] { + return Object.entries(obj).reduce( + (acc, [key, value]) => { + return acc.flatMap((cases) => + value.map((cas: unknown) => ({ ...cases, [key]: cas })), + ); + }, + [{} as Combination], + ); +} + +export function mockWebpack( + files: string[], + contents: (string | Buffer)[], + maps: (string | null)[], + devtool: 'source-map' | 'hidden-source-map' | false = 'source-map', +) { + const assets = files.reduce((acc, name, i) => { + const source = contents[i]; + const map = maps?.[i]; + const webpackSource = map + ? new SourceMapSource(source, name, map) + : new RawSource(source); + acc[name] = { + name, + info: { + size: webpackSource.size(), + }, + source: webpackSource, + }; + return acc; + }, {} as Record); + let done: () => void; + const promise = new Promise((resolve) => { + done = resolve; + }); + const compilation = { + get assets() { + return Object.fromEntries( + Object.entries(assets).map(([name, asset]) => [name, asset.source]), + ); + }, + emitAsset: mock.fn((name, source, info) => { + assets[name] = { + name, + info, + source, + }; + }), + options: { + devtool, + } as unknown as WebpackOptionsNormalized, + chunks: new Set([ + { + files: new Set(Object.keys(assets)), + } as Chunk, + ]), + getAsset: mock.fn((name) => assets[name]), + updateAsset: mock.fn( + (name: string, fn: (source: sources.Source) => sources.Source) => { + return fn(assets[name].source); + }, + ), + deleteAsset: mock.fn((name: string) => { + delete assets[name]; + }), + hooks: { + processAssets: { + async tapPromise(_: unknown, fn: (assets: Assets) => Promise) { + await fn(compilation.assets); + done(); + }, + tap(_: unknown, fn: (assets: Assets) => void) { + fn(compilation.assets); + done(); + }, + }, + }, + }; + const compiler = { + hooks: { + compilation: { + tap(_: unknown, fn: (compilation: Compilation) => void) { + fn(compilation as unknown as Compilation); + }, + }, + }, + webpack: { + sources: { SourceMapSource, RawSource }, + }, + } as Compiler; + return { + compiler, + compilation: compilation as Compilation & typeof compilation, + promise, + }; +} diff --git a/development/webpack/test/loaders.codeFenceLoader.test.ts b/development/webpack/test/loaders.codeFenceLoader.test.ts new file mode 100644 index 000000000000..a8a4dda59508 --- /dev/null +++ b/development/webpack/test/loaders.codeFenceLoader.test.ts @@ -0,0 +1,100 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { LoaderContext } from 'webpack'; +import { FeatureLabels } from '@metamask/build-utils'; +import codeFenceLoader, { + getCodeFenceLoader, + CodeFenceLoaderOptions, +} from '../utils/loaders/codeFenceLoader'; + +describe('codeFenceLoader', () => { + type CallbackArgs = Parameters< + LoaderContext['callback'] + >; + + function generateData({ omitFeature }: { omitFeature: boolean }) { + const featureLabel = 'feature-label'; + const fencedSource = `///: BEGIN:ONLY_INCLUDE_IF(${featureLabel}) +console.log('I am Groot.'); +///: END:ONLY_INCLUDE_IF`; + const source = ` +console.log('I am Groot.'); +${fencedSource} +console.log('I am Groot.'); +`; + const expected = omitFeature + ? source.replace(`${fencedSource}\n`, '') + : source; + + let resolveCallback: (value: CallbackArgs) => void; + const mockContext = { + getOptions: () => { + return { + features: { + active: new Set(omitFeature ? [] : [featureLabel]), + all: new Set([featureLabel]), + }, + }; + }, + resourcePath: '', + callback: (...args: CallbackArgs) => resolveCallback(args), + } as unknown as LoaderContext; + const deferredPromise = new Promise((resolve) => { + resolveCallback = resolve; + }); + mockContext.callback = mockContext.callback.bind(mockContext); + return { context: mockContext, source, expected, deferredPromise }; + } + + [false, true].forEach((omitFeature) => { + it(`should ${omitFeature ? '' : 'not '}remove source when feature is ${ + omitFeature ? 'not ' : '' + }active`, async () => { + const data = generateData({ omitFeature }); + const returnValue = codeFenceLoader.call(data.context, data.source); + + assert.strictEqual(returnValue, undefined, 'should return undefined'); + const [err, content] = await data.deferredPromise; + assert.strictEqual(err, null); + assert.strictEqual(content, data.expected); + }); + }); + + it('should throw an error when options are invalid', () => { + const data = generateData({ omitFeature: false }); + data.context.getOptions = () => { + // invalid options + return {} as unknown as CodeFenceLoaderOptions; + }; + assert.throws( + () => codeFenceLoader.call(data.context, data.source), + /Invalid configuration object/u, + ); + }); + + it('should return an error when code fences are invalid', async () => { + const data = generateData({ omitFeature: false }); + data.source = '///: BEGIN:ONLY_INCLUDE_IF\nconsole.log("I am Groot.");\n'; // invalid because there is no end comment + const returnValue = codeFenceLoader.call(data.context, data.source); + assert.strictEqual(returnValue, undefined, 'should return undefined'); + const [err, content] = await data.deferredPromise; + assert(err); + assert.deepStrictEqual( + err.message, + 'Invalid code fence parameters in file "":\nNo parameters specified.', + ); + assert.strictEqual(content, undefined); + }); + + describe('getCodeFenceLoader', () => { + it('should return a loader with correct properties', () => { + const features: FeatureLabels = { active: new Set(), all: new Set() }; + const result = getCodeFenceLoader(features); + + assert.deepStrictEqual(result, { + loader: require.resolve('../utils/loaders/codeFenceLoader'), + options: { features }, + }); + }); + }); +}); diff --git a/development/webpack/test/loaders.swcLoader.test.ts b/development/webpack/test/loaders.swcLoader.test.ts new file mode 100644 index 000000000000..4e2e787c0e73 --- /dev/null +++ b/development/webpack/test/loaders.swcLoader.test.ts @@ -0,0 +1,132 @@ +import { describe, it, afterEach } from 'node:test'; +import assert from 'node:assert'; +import { LoaderContext } from 'webpack'; +import swcLoader, { + type SwcLoaderOptions, + type SwcConfig, +} from '../utils/loaders/swcLoader'; +import { Combination, generateCases } from './helpers'; + +describe('swcLoader', () => { + type CallbackArgs = Parameters['callback']>; + + function generateData() { + const source = ` export function hello(message: string) { + console.log(message) +}; `; + const expected = `export function hello(message) { + console.log(message); +} +`; + + // swc doesn't use node's fs module, so we can't mock + const resourcePath = 'test.ts'; + + let resolveCallback: (value: CallbackArgs) => void; + const mockContext = { + mode: 'production', + sourceMap: true, + getOptions: () => { + return {}; + }, + resourcePath, + async: () => { + return (...args: CallbackArgs) => { + resolveCallback(args); + }; + }, + } as unknown as LoaderContext; + const deferredPromise = new Promise((resolve) => { + resolveCallback = resolve; + }); + mockContext.async = mockContext.async.bind(mockContext); + return { context: mockContext, source, expected, deferredPromise }; + } + + it('should transform code', async () => { + const { context, source, deferredPromise, expected } = generateData(); + const returnValue = swcLoader.call(context, source); + + assert.strictEqual(returnValue, undefined, 'should return undefined'); + const [err, content, map] = await deferredPromise; + assert.strictEqual(err, null); + assert.strictEqual(content, expected); + const mapObj = JSON.parse(map as string); + assert.deepStrictEqual(mapObj.sources, [context.resourcePath]); + }); + + it('should throw an error when options are invalid', () => { + const { context, source } = generateData(); + context.getOptions = () => { + return { + invalid: true, + } as unknown as SwcLoaderOptions; + }; + assert.throws( + () => swcLoader.call(context, source), + /[ValidationError]: Invalid configuration object/u, + ); + }); + + it('should return an error when code is invalid', async () => { + const { context, deferredPromise } = generateData(); + const brokenSource = 'this is not real code;'; + swcLoader.call(context, brokenSource); + const [err, content, map] = await deferredPromise; + assert(err); + assert.match(err.message, /Syntax Error/u); + assert.strictEqual(content, undefined); + assert.strictEqual(map, undefined); + }); + + describe('getSwcLoader', () => { + const matrix = { + syntax: ['typescript', 'ecmascript'] as const, + enableJsx: [true, false] as const, + watch: [true, false] as const, + isDevelopment: [true, false] as const, + }; + generateCases(matrix).forEach(runTest); + + type TestCase = Combination; + + afterEach(() => { + delete process.env.__HMR_READY__; + }); + function runTest({ syntax, enableJsx, watch, isDevelopment }: TestCase) { + it(`should return a loader with correct properties when syntax is ${syntax}, jsx is ${enableJsx}, watch is ${watch}, and isDevelopment is ${isDevelopment}`, () => { + process.env.__HMR_READY__ = 'true'; + // helpers caches `__HMR_READY__` on initialization, so we need to a new + // one after we mock `process.env.__HMR_READY__`. + delete require.cache[require.resolve('../utils/helpers')]; + delete require.cache[require.resolve('../utils/loaders/swcLoader')]; + const { + getSwcLoader, + }: typeof import('../utils/loaders/swcLoader') = require('../utils/loaders/swcLoader'); + + // note: this test isn't exhaustive of all possible `swcConfig` + // properties; it is mostly intended as sanity check. + const swcConfig: SwcConfig = { + args: { watch }, + safeVariables: {}, + browsersListQuery: '', + isDevelopment, + }; + + const loader = getSwcLoader(syntax, enableJsx, swcConfig); + assert.strictEqual( + loader.loader, + require.resolve('../utils/loaders/swcLoader'), + ); + assert.deepStrictEqual(loader.options.jsc.parser, { + syntax, + [syntax === 'typescript' ? 'tsx' : 'jsx']: enableJsx, + }); + assert.deepStrictEqual(loader.options.jsc.transform.react, { + development: isDevelopment, + refresh: isDevelopment && watch, + }); + }); + } + }); +}); diff --git a/development/webpack/test/plugins.ManifestPlugin.test.ts b/development/webpack/test/plugins.ManifestPlugin.test.ts new file mode 100644 index 000000000000..ff14904bb8d6 --- /dev/null +++ b/development/webpack/test/plugins.ManifestPlugin.test.ts @@ -0,0 +1,284 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { join } from 'node:path'; +import { type Compilation } from 'webpack'; +import { ManifestPlugin } from '../utils/plugins/ManifestPlugin'; +import { ZipOptions } from '../utils/plugins/ManifestPlugin/types'; +import { Manifest } from '../utils/helpers'; +import { transformManifest } from '../utils/plugins/ManifestPlugin/helpers'; +import { generateCases, type Combination, mockWebpack } from './helpers'; + +describe('ManifestPlugin', () => { + describe('Plugin', () => { + const matrix = { + zip: [true, false], + files: [ + [ + { + // will be compressed + name: 'filename.js', + source: Buffer.from('console.log(1 + 2);', 'utf8'), + }, + ], + [ + { + // will be compressed + name: 'filename.js', + source: Buffer.from('console.log(1 + 2);', 'utf8'), + }, + { + // will be omitted + name: 'filename.js.map', + source: Buffer.alloc(0), + }, + { + // will not be compressed + name: 'pixel.png', + source: Buffer.from([ + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, + 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 55, 110, 249, 36, 0, 0, 0, 10, + 73, 68, 65, 84, 120, 1, 99, 96, 0, 0, 0, 2, 0, 1, 115, 117, 1, 24, + 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, + ]), + }, + ], + ], + browsers: [['chrome', 'firefox'], ['chrome']] as const, + fixture: ['empty', 'complex'], + description: [null, 'description'], + manifestVersion: [2, 3] as const, + webAccessibleResources: [undefined, ['filename.map.js']], + }; + generateCases(matrix).forEach(runTest); + + type TestCase = Combination; + + function runTest(testCase: TestCase) { + const { + browsers, + fixture, + files, + description, + manifestVersion, + webAccessibleResources, + zip, + } = testCase; + const context = join(__dirname, `fixtures/ManifestPlugin/${fixture}`); + const baseManifest = require(join( + context, + `manifest/v${manifestVersion}`, + '_base.json', + )); + const expectedAssets = getExpectedAssets(zip, browsers, files); + const validateManifest = getValidateManifest(testCase, baseManifest); + + it(`should produce a ${ + zip ? 'zip file' : 'folder' + } for browsers [${browsers.join( + ', ', + )}] using the v${manifestVersion} "${fixture}" manifest, including files [${files + .map((file) => file.name) + .join(', ')}], ${ + description ? 'a description' : 'no description' + }, and ${ + webAccessibleResources ? webAccessibleResources.length : 0 + } web_accessible_resources`, async () => { + const { compiler, compilation, promise } = mockWebpack( + files.map(({ name }) => name), + files.map(({ source }) => source), + files.map(() => null), + ); + compilation.options.context = context; + const manifestPlugin = new ManifestPlugin({ + browsers, + manifest_version: manifestVersion, + version: '1.0.0.0', + versionName: '1.0.0', + description, + web_accessible_resources: webAccessibleResources, + ...getZipOptions(zip), + }); + manifestPlugin.apply(compiler); + await promise; + + assert.deepStrictEqual(Object.keys(compilation.assets), expectedAssets); + validateManifest(compilation as unknown as Compilation); + }); + } + + function getZipOptions( + zip: boolean, + ): ({ zip: true } & ZipOptions) | { zip: false } { + if (zip) { + return { + zip: true, + zipOptions: { + level: 0, + mtime: 1711141205825, + excludeExtensions: ['.map'], + outFilePath: '[browser]/extension.zip', + }, + }; + } + return { + zip: false, + }; + } + + function getExpectedAssets( + zip: boolean, + browsers: readonly string[], + files: { name: string }[], + ) { + const assets: string[] = []; + if (zip) { + browsers.forEach((browser) => { + assets.push(`${browser}/extension.zip`); + }); + } + browsers.forEach((browser) => { + assets.push(`${browser}/manifest.json`); + assets.push(...files.map(({ name }) => `${browser}/${name}`)); + }); + return [...new Set(assets)]; // unique + } + function getValidateManifest(testCase: TestCase, baseManifest: Manifest) { + // Handle case when the output is a zip file + if (testCase.zip) { + return () => { + // Assume the validation is successful, as unzipping and checking contents is skipped + assert.ok(true, 'Zip file creation assumed successful.'); + }; + } + + // Common validation for non-zip outputs, applicable to both manifest versions 2 and 3 + return (compilation: Compilation) => { + testCase.browsers.forEach((browser) => { + const manifest = compilation.assets[`${browser}/manifest.json`]; + const json = JSON.parse(manifest.source().toString()) as Manifest; + + // Validate description, if applicable + if (testCase.description) { + assert( + json.description, + "should have a 'description' in the manifest", + ); + const descMessage = `should have the correct description in ${browser} manifest`; + assert( + json.description.endsWith(testCase.description), + descMessage, + ); + } + + // Validate web accessible resources + let expectedWar: Manifest['web_accessible_resources']; + if (testCase.webAccessibleResources) { + if (baseManifest.manifest_version === 3) { + // Extend expected resources for manifest version 3 + expectedWar = baseManifest.web_accessible_resources || []; + expectedWar = [ + { + // the manifest plugin only supports `` for manifest version 3 + // so we don't test other `matches`. + matches: [''], + resources: [ + ...(expectedWar[0]?.resources || []), + ...testCase.webAccessibleResources, + ], + }, + ]; + } else { + expectedWar = baseManifest.web_accessible_resources || []; + // Keep or extend expected resources for manifest version 2 + expectedWar = [ + ...expectedWar, + ...testCase.webAccessibleResources, + ]; + } + } else { + expectedWar = baseManifest.web_accessible_resources || []; + } + + assert.deepStrictEqual( + json.web_accessible_resources || [], + expectedWar, + "should have the correct 'web_accessible_resources' in the manifest", + ); + }); + }; + } + }); + + describe('should transform the manifest object', () => { + const keep = ['scripts/contentscript.js', 'scripts/inpage.js']; + const argsMatrix = { + lockdown: [true, false], + test: [true, false], + }; + const manifestMatrix = { + content_scripts: [ + undefined, + [], + [{ js: [...keep] }], + [{ js: ['lockdown.js', ...keep] }], + ], + permissions: [undefined, [], ['tabs'], ['something']], + }; + generateCases(argsMatrix).forEach(setupTests); + + function setupTests(args: Combination) { + generateCases(manifestMatrix).forEach(runTest); + + function runTest(baseManifest: Combination) { + const manifest = baseManifest as unknown as chrome.runtime.Manifest; + const hasTabsPermission = (manifest.permissions || []).includes('tabs'); + const transform = transformManifest(args); + + if (args.test && hasTabsPermission) { + it("throws in test mode when manifest already contains 'tabs' permission", () => { + assert(transform, 'transform should be truthy'); + const p = () => { + transform(manifest, 'chrome'); + }; + assert.throws( + p, + /manifest contains 'tabs' already; this transform should be removed./u, + 'should throw when manifest contains tabs already', + ); + }); + } else if (!args.lockdown || args.test) { + it(`works for args.test of ${args.test} and args.lockdown of ${ + args.lockdown + }. Manifest: ${JSON.stringify(manifest)}`, () => { + assert(transform, 'transform should be truthy'); + const transformed = transform(manifest, 'chrome'); + if (args.lockdown) { + assert.deepStrictEqual( + transformed.content_scripts, + manifest.content_scripts, + 'nothing should change in lockdown mode', + ); + } else { + const stripped = manifest.content_scripts?.[0]?.js?.filter( + (js) => js !== 'lockdown.js', + ); + assert.deepStrictEqual( + transformed.content_scripts?.[0]?.js, + stripped, + 'lockdown.js should be removed when not in lockdown mode.', + ); + } + + if (args.test) { + assert.deepStrictEqual( + transformed.permissions, + [...(manifest.permissions || []), 'tabs'], + "manifest should have 'tabs' permission", + ); + } + }); + } + } + } + }); +}); diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts new file mode 100644 index 000000000000..3a3ef729eacf --- /dev/null +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -0,0 +1,95 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { SelfInjectPlugin } from '../utils/plugins/SelfInjectPlugin'; +import { generateCases, type Combination, mockWebpack } from './helpers'; + +describe('SelfInjectPlugin', () => { + const matrix = { + test: [/\.js$/u, /\.ts$/u] as const, + filename: ['file.js', 'file.ts'], + source: ['console.log(3);'], + // sourceMap generated via https://www.digitalocean.com/community/tools/minify + map: [ + null, + '{"version":3,"file":"file.js","names":["console","log"],"sources":["0"],"mappings":"AAAAA,QAAQC,IAAI"}', + ], + devtool: ['source-map', 'hidden-source-map', false] as const, + }; + + generateCases(matrix).forEach(runTest); + + function runTest({ + test, + filename, + source, + map, + devtool, + }: Combination) { + it(`should produce valid output when test is ${test}, filename is ${filename}, map is ${ + map ? 'available' : 'missing' + }, and devtool is ${devtool}`, () => { + const { compiler, compilation } = mockWebpack( + [filename], + [source], + [map], + devtool, + ); + + const plugin = new SelfInjectPlugin({ test }); + plugin.apply(compiler); + + if (filename.match(test)) { + // we should have matched our file so it should have been updated: + + assert.strictEqual(compilation.updateAsset.mock.callCount(), 1); + const newAsset = compilation.updateAsset.mock.calls[0].result; + assert(newAsset, 'newAsset should be defined'); + const { source: newSource, map: newMap } = newAsset.sourceAndMap(); + + // `newMap` should be `null` here, because the file has been transformed + // to be self-injecting, so there is no way to map it anymore. + assert.strictEqual(newMap, null); + + if (map !== null && devtool === 'source-map') { + // if we have a map and devtool is `source-map` the newSource should + // reference the `sourceMappingURL` + assert.strictEqual( + newSource, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + ); + } else { + // the new source should NOT reference the new sourcemap, since it's + // "hidden" (or we aren't generating source maps at all). Notice that + // we DO still include `sourceURL`, as this aids in debugging + // (and development) gives the injected source a name that will show + // in the console if the source throws an exception or logs to the + // console. + assert.strictEqual( + newSource, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + ); + } + + if (map) { + // If we provided a `map` the source map should have been emitted as + // a separate asset. Note that this happens even when devtool is set + // to `false`, as this means the map file already existed and we + // should not remove it (we don't care how it got there). The devtool + // directive is about whether to generate a new map, not whether to + // emit an existing one. + assert.strictEqual(compilation.emitAsset.mock.callCount(), 1); + const [sourceMapFilename, sourceMapSource] = + compilation.emitAsset.mock.calls[0].arguments; + assert.strictEqual(sourceMapFilename, `${filename}.map`); + assert.strictEqual(sourceMapSource.source(), map); + } + } else { + // we should not have matched our file so there should be no changes + assert.strictEqual(compilation.updateAsset.mock.callCount(), 0); + + // and no new assets should have been emitted + assert.strictEqual(compilation.emitAsset.mock.callCount(), 0); + } + }); + } +}); diff --git a/development/webpack/test/version.test.ts b/development/webpack/test/version.test.ts new file mode 100644 index 000000000000..14a98939df98 --- /dev/null +++ b/development/webpack/test/version.test.ts @@ -0,0 +1,118 @@ +import { describe, it, before } from 'node:test'; +import assert from 'node:assert'; +import { getExtensionVersion } from '../utils/version'; + +describe('getMetaMaskVersion', () => { + const MIN_ID = 10; + const MAX_ID = 64; + const MIN_RELEASE = 0; + const MAX_RELEASE = 999; + + describe('exceptions', () => { + it(`should throw for build with negative id (-1)`, () => { + const test = () => getExtensionVersion('main', { id: -1 }, 0); + assert.throws(test); + }); + + it('should throw for build with an invalid id (0)', () => { + const test = () => getExtensionVersion('main', { id: 0 }, 0); + assert.throws(test); + }); + + it(`should throw for build with an invalid id (${MIN_ID - 1})`, () => { + const test = () => getExtensionVersion('main', { id: MIN_ID - 1 }, 0); + assert.throws(test); + }); + + it(`should throw for build with invalid id (${MAX_ID + 1})`, () => { + const test = () => getExtensionVersion('main', { id: MAX_ID + 1 }, 0); + assert.throws(test); + }); + + it('should throw when computing the version for build with prerelease implicitly disallowed, release version: 1', () => { + const test = () => getExtensionVersion('main', { id: 10 }, 1); + assert.throws(test); + }); + + it('should throw when computing the version for build with prerelease explicitly disallowed, release version: 1', () => { + const test = () => + getExtensionVersion('main', { id: 10, isPrerelease: false }, 1); + assert.throws(test); + }); + + it(`should throw when computing the version for build with prerelease disallowed, release version: ${ + MAX_RELEASE + 1 + }`, () => { + const test = () => + getExtensionVersion('main', { id: 10 }, MAX_RELEASE + 1); + assert.throws(test); + }); + + it(`should throw for allowed prerelease, bad release version: ${ + MIN_RELEASE - 1 + }`, () => { + const test = () => + getExtensionVersion( + 'beta', + { id: 11, isPrerelease: true }, + MIN_RELEASE - 1, + ); + assert.throws(test); + }); + + it(`should throw when computing the version for allowed prerelease, bad release version: ${ + MAX_RELEASE + 1 + }`, () => { + const test = () => + getExtensionVersion( + 'beta', + { id: 11, isPrerelease: true }, + MAX_RELEASE + 1, + ); + assert.throws(test); + }); + }); + + describe('success', () => { + let pVersion: string; + before(() => { + pVersion = require('../../../package.json').version; + }); + + it(`for build with prerelease disallowed, id: ${MIN_ID}, release version: ${MIN_RELEASE}`, () => { + const mmVersion = getExtensionVersion( + 'main', + { id: MIN_ID }, + MIN_RELEASE, + ); + assert.deepStrictEqual(mmVersion, { + version: `${pVersion}.0`, + versionName: pVersion, + }); + }); + + it(`should return the computed version for allowed prerelease, id: ${MIN_ID}, release version: ${MIN_RELEASE}`, () => { + const mmVersion = getExtensionVersion( + 'beta', + { id: MIN_ID, isPrerelease: true }, + MIN_RELEASE, + ); + assert.deepStrictEqual(mmVersion, { + version: `${pVersion}.${MIN_ID}${MIN_RELEASE}`, + versionName: `${pVersion}-beta.${MIN_RELEASE}`, + }); + }); + + it(`should return the computed version for allowed prerelease, id: ${MAX_ID}, release version: ${MAX_RELEASE}`, () => { + const mmVersion = getExtensionVersion( + 'beta', + { id: MAX_ID, isPrerelease: true }, + MAX_RELEASE, + ); + assert.deepStrictEqual(mmVersion, { + version: `${pVersion}.${MAX_ID}${MAX_RELEASE}`, + versionName: `${pVersion}-beta.${MAX_RELEASE}`, + }); + }); + }); +}); diff --git a/development/webpack/test/webpack.config.test.ts b/development/webpack/test/webpack.config.test.ts new file mode 100644 index 000000000000..e1d11f953829 --- /dev/null +++ b/development/webpack/test/webpack.config.test.ts @@ -0,0 +1,307 @@ +import fs from 'node:fs'; +import { describe, it, afterEach, before, after, mock } from 'node:test'; +import assert from 'node:assert'; +import process from 'node:process'; +import { resolve } from 'node:path'; +import { + type Configuration, + webpack, + Compiler, + WebpackPluginInstance, +} from 'webpack'; +import { noop } from '../utils/helpers'; +import { ManifestPlugin } from '../utils/plugins/ManifestPlugin'; +import { getLatestCommit } from '../utils/git'; +import { ManifestPluginOptions } from '../utils/plugins/ManifestPlugin/types'; + +function getWebpackInstance(config: Configuration) { + // webpack logs a warning if we pass config.watch to it without a callback + // we don't want a callback because that will cause the build to run + // so we just delete the watch property. + delete config.watch; + return webpack(config); +} + +/** + * These tests are aimed at testing conditional branches in webpack.config.ts. + * These tests do *not* test the actual webpack build process itself, or that + * the parsed command line args are even valid. Instead, these tests ensure the + * branches of configuration options are reached and applied correctly. + */ + +describe('webpack.config.test.ts', () => { + let originalArgv: string[]; + let originalEnv: NodeJS.ProcessEnv; + const originalReadFileSync = fs.readFileSync; + before(() => { + // cache originals before we start messing with them + originalArgv = process.argv; + originalEnv = process.env; + }); + after(() => { + // restore originals for other tests + process.argv = originalArgv; + process.env = originalEnv; + }); + afterEach(() => { + // reset argv to avoid affecting other tests + process.argv = [process.argv0, process.argv[1]]; + // each test needs to load a fresh config, so we need to clear webpack's cache + // TODO: can we use `await import` instead to get a fresh copy each time? + const cliPath = require.resolve('../utils/cli.ts'); + const helpersPath = require.resolve('../utils/helpers.ts'); + const webpackConfigPath = require.resolve('../webpack.config.ts'); + delete require.cache[cliPath]; + delete require.cache[helpersPath]; + delete require.cache[webpackConfigPath]; + mock.restoreAll(); + }); + + function getWebpackConfig(args: string[] = [], env: NodeJS.ProcessEnv = {}) { + // argv is automatically read when webpack.config is required/imported. + // first two args are always ignored. + process.argv = [...process.argv.slice(0, 2), ...args]; + process.env = { ...env }; + mock.method(fs, 'readFileSync', (path: string, options?: null) => { + if (path === resolve(__dirname, '../../../.metamaskrc')) { + // mock `.metamaskrc`, as users might have customized it which may + // break our tests + return ` +${Object.entries(env) + .map(([key, value]) => `${key}=${value}`) + .join('\n')} +`; + } + return originalReadFileSync.call(fs, path, options); + }); + return require('../webpack.config.ts').default; + } + + it('should have the correct defaults', () => { + const config: Configuration = getWebpackConfig(); + // check that options are valid + const { options } = webpack(config); + assert.strictEqual(options.name, 'MetaMask – development'); + assert.strictEqual(options.mode, 'development'); + assert(options.cache); + assert.strictEqual(options.cache.type, 'filesystem'); + assert.strictEqual(options.devtool, 'source-map'); + const stats = options.stats as { preset: string }; + assert.strictEqual(stats.preset, 'none'); + const fallback = options.resolve.fallback as Record; + assert.strictEqual(typeof fallback['react-devtools'], 'string'); + assert.strictEqual(typeof fallback['remote-redux-devtools'], 'string'); + assert.strictEqual(options.optimization.minimize, false); + assert.strictEqual(options.optimization.sideEffects, false); + assert.strictEqual(options.optimization.providedExports, false); + assert.strictEqual(options.optimization.removeAvailableModules, false); + assert.strictEqual(options.optimization.usedExports, false); + assert.strictEqual(options.watch, false); + + const runtimeChunk = options.optimization.runtimeChunk as + | { + name?: (chunk: { name?: string }) => string | false; + } + | undefined; + assert(runtimeChunk); + assert(runtimeChunk.name); + assert(typeof runtimeChunk.name, 'function'); + assert.strictEqual( + runtimeChunk.name({ name: 'snow.prod' }), + false, + 'snow.prod should not be chunked', + ); + assert.strictEqual( + runtimeChunk.name({ name: 'use-snow' }), + false, + 'use-snow should not be chunked', + ); + assert.strictEqual( + runtimeChunk.name({ name: '< random >' }), + 'runtime', + 'other names should be chunked', + ); + assert.strictEqual( + runtimeChunk.name({}), + 'runtime', + 'chunks without a name name should be chunked', + ); + + const manifestPlugin = options.plugins.find( + (plugin) => plugin && plugin.constructor.name === 'ManifestPlugin', + ) as ManifestPlugin; + assert(manifestPlugin, 'Manifest plugin should be present'); + assert.deepStrictEqual(manifestPlugin.options.web_accessible_resources, [ + 'scripts/inpage.js.map', + 'scripts/contentscript.js.map', + ]); + assert.deepStrictEqual( + manifestPlugin.options.description, + `development build from git id: ${getLatestCommit().hash()}`, + ); + assert(manifestPlugin.options.transform); + assert.deepStrictEqual( + manifestPlugin.options.transform( + { + manifest_version: 3, + name: 'name', + version: '1.2.3', + content_scripts: [ + { + js: [ + 'ignored', + 'scripts/contentscript.js', + 'scripts/inpage.js', + 'ignored', + ], + }, + ], + }, + 'brave', + ), + { + manifest_version: 3, + name: 'name', + version: '1.2.3', + content_scripts: [ + { + js: ['scripts/contentscript.js', 'scripts/inpage.js'], + }, + ], + }, + ); + assert.strictEqual(manifestPlugin.options.zip, false); + const manifestOpts = manifestPlugin.options as ManifestPluginOptions; + assert.strictEqual(manifestOpts.zipOptions, undefined); + + const progressPlugin = options.plugins.find( + (plugin) => plugin && plugin.constructor.name === 'ProgressPlugin', + ); + assert(progressPlugin, 'Progress plugin should present'); + }); + + it('should apply non-default options', () => { + const removeUnsupportedFeatures = ['--no-lavamoat']; + const config: Configuration = getWebpackConfig( + [ + '--env', + 'production', + '--watch', + '--stats', + '--no-progress', + '--no-cache', + '--zip', + ...removeUnsupportedFeatures, + ], + { + INFURA_PROD_PROJECT_ID: '00000000000000000000000000000000', + SEGMENT_WRITE_KEY: '-', + SEGMENT_PROD_WRITE_KEY: '-', + }, + ); + // webpack logs a warning if we specify `watch: true`, `getWebpackInstance` + // removes the property, so we test it here instead + assert.strictEqual(config.watch, true); + + // check that options are valid + const instance: Compiler = getWebpackInstance(config); + assert.strictEqual(instance.options.name, 'MetaMask – production'); + assert.strictEqual(instance.options.mode, 'production'); + assert.ok(instance.options.cache); + assert.strictEqual(instance.options.cache.type, 'memory'); + assert.strictEqual(instance.options.devtool, 'hidden-source-map'); + const stats = instance.options.stats as { preset: string }; + assert.strictEqual(stats.preset, 'normal'); + const fallback = instance.options.resolve.fallback as Record; + assert.strictEqual(fallback['react-devtools'], false); + assert.strictEqual(fallback['remote-redux-devtools'], false); + assert.strictEqual(instance.options.optimization.minimize, true); + assert.strictEqual(instance.options.optimization.sideEffects, true); + assert.strictEqual(instance.options.optimization.providedExports, true); + assert.strictEqual( + instance.options.optimization.removeAvailableModules, + true, + ); + assert.strictEqual(instance.options.optimization.usedExports, true); + + const manifestPlugin = instance.options.plugins.find( + (plugin) => plugin && plugin.constructor.name === 'ManifestPlugin', + ) as WebpackPluginInstance; + assert.deepStrictEqual(manifestPlugin.options.web_accessible_resources, []); + assert.deepStrictEqual(manifestPlugin.options.description, null); + assert.deepStrictEqual(manifestPlugin.options.zip, true); + assert(manifestPlugin.options.zipOptions, 'Zip options should be present'); + assert.strictEqual(manifestPlugin.options.transform, undefined); + + const progressPlugin = instance.options.plugins.find( + (plugin) => plugin && plugin.constructor.name === 'ProgressPlugin', + ); + assert.strictEqual( + progressPlugin, + undefined, + 'Progress plugin should be absent', + ); + }); + + it('should allow disabling source maps', () => { + const config: Configuration = getWebpackConfig(['--devtool', 'none']); + // check that options are valid + const instance = getWebpackInstance(config); + assert.strictEqual(instance.options.devtool, false); + }); + + it('should write the `dry-run` message then call exit(0)', () => { + const exit = mock.method(process, 'exit', noop, { times: 1 }); + const error = mock.method(console, 'error', noop, { times: 1 }); + + // we don't care about the return value, just that it logs and calls `exit` + getWebpackConfig(['--dry-run']); + assert.strictEqual(error.mock.calls.length, 1); + assert.strictEqual(error.mock.calls[0].arguments.length, 1); + // we don't care about the message, just that it is logged + assert.strictEqual(typeof error.mock.calls[0].arguments[0], 'string'); + + assert.strictEqual(exit.mock.calls.length, 1); + assert.strictEqual(exit.mock.calls[0].arguments.length, 1); + assert.strictEqual(exit.mock.calls[0].arguments[0], 0); + }); + + it('should write the `dryRun` message then call exit(0)', () => { + const exit = mock.method(process, 'exit', noop, { times: 1 }); + const error = mock.method(console, 'error', noop, { times: 1 }); + + // we don't care about the return value, just that it logs and calls `exit` + getWebpackConfig(['--dryRun']); + assert.strictEqual(error.mock.calls.length, 1); + assert.strictEqual(error.mock.calls[0].arguments.length, 1); + // we don't care about the message, just that it is logged + assert.strictEqual(typeof error.mock.calls[0].arguments[0], 'string'); + + assert.strictEqual(exit.mock.calls.length, 1); + assert.strictEqual(exit.mock.calls[0].arguments.length, 1); + assert.strictEqual(exit.mock.calls[0].arguments[0], 0); + }); + + it('should enable ReactRefreshPlugin in a development env when `--watch` is specified', () => { + const config: Configuration = getWebpackConfig(['--watch'], { + __HMR_READY__: 'true', + }); + delete config.watch; + const instance = webpack(config); + const reactRefreshPlugin = instance.options.plugins.find( + (plugin) => plugin && plugin.constructor.name === 'ReactRefreshPlugin', + ); + assert(reactRefreshPlugin, 'ReactRefreshPlugin should be present'); + }); + + // these tests should be temporary until the below options are supported + const unsupportedOptions = [['--lavamoat'], ['--manifest_version', '3']]; + for (const args of unsupportedOptions) { + it(`should throw on unsupported option \`${args.join('=')}\``, () => { + assert.throws( + () => getWebpackConfig(args), + `Unsupported option: ${args.join(' ')}`, + ); + }); + } +}); diff --git a/development/webpack/types.ts b/development/webpack/types.ts new file mode 100644 index 000000000000..f2739861246b --- /dev/null +++ b/development/webpack/types.ts @@ -0,0 +1,34 @@ +import type { ChildProcess } from 'node:child_process'; +import { type Readable } from 'node:stream'; +import { type Socket } from 'node:net'; +import { type IPty } from '@lydell/node-pty'; + +/** + * A more complete type for the `node-pty` module's `IPty` interface + */ +export type PTY = IPty & { + master: Socket; + slave: Socket; +}; + +/** + * Node's ChildProcess type extended with `stderr` and `stdout`'s `unref` + * method, which is missing from the standard Node.js types. + */ +export type Child = ChildProcess & { + stderr: Readable & { unref: () => Readable }; + stdout: Readable & { unref: () => Readable }; +}; + +export type StdName = 'stdout' | 'stderr'; + +/** + * The control interface for a child process's stdio streams. + */ +export type Stdio = { + destroy: () => void; + listen: (child: Child) => void; + pty: Socket | 'pipe'; + resize: () => void; + unref: (child: Child) => void; +}; diff --git a/development/webpack/utils/cli.ts b/development/webpack/utils/cli.ts new file mode 100644 index 000000000000..05e18f8cf3b8 --- /dev/null +++ b/development/webpack/utils/cli.ts @@ -0,0 +1,391 @@ +/** + * @file This file contains the CLI parser for the webpack build script. + * It is responsible for parsing the command line arguments and returning a + * structured object representing the parsed arguments. + */ + +import type { Options as YargsOptions } from 'yargs'; +import yargs from 'yargs/yargs'; +import parser from 'yargs-parser'; +import { + Browsers, + type Manifest, + type Browser, + uniqueSort, + toOrange, +} from './helpers'; +import { type BuildConfig } from './config'; + +const ENV_PREFIX = 'BUNDLE'; +const addFeat = 'addFeature' as const; +const omitFeat = 'omitFeature' as const; +type YargsOptionsMap = { [key: string]: YargsOptions }; +type OptionsKeys = keyof Omit; + +/** + * Some options affect the default values of other options. + */ +const prerequisites = { + env: { + alias: 'e', + array: false, + default: 'development' as const, + description: 'Enables/disables production optimizations/development hints', + choices: ['development', 'production'] as const, + group: toOrange('Build options:'), + type: 'string', + }, + // `as const` makes it easier for developers to see the values of the type + // when hovering over it in their IDE. `satisfies Options` enables type + // checking, without loosing the `const` property of the values, which is + // necessary for yargs to infer the final types +} as const satisfies YargsOptionsMap; + +/** + * Parses the given args from `argv` and returns whether or not the requested + * build is a production build or not. + * + * @param argv + * @param opts + * @returns `true` if this is a production build, otherwise `false` + */ +function preParse( + argv: string[], + opts: typeof prerequisites, +): { env: 'production' | 'development' } { + const options: { [k: string]: { [k: string]: unknown } } = { + configuration: { + envPrefix: ENV_PREFIX, + }, + }; + // convert the `opts` object into a format that `yargs-parser` can understand + for (const [arg, val] of Object.entries(opts)) { + for (const [key, valEntry] of Object.entries(val)) { + if (!options[key]) { + options[key] = {}; + } + options[key][arg] = valEntry; + } + } + + const { env } = parser(argv, options); + return { env }; +} + +/** + * Type representing the parsed arguments + */ +export type Args = ReturnType['args']; +export type Features = ReturnType['features']; + +/** + * Parses an array of command line arguments into a structured format. + * + * @param argv - An array of command line arguments, excluding the program + * executable and file name. Typically used as + * `parseArgv(process.argv.slice(2))`. + * @param buildConfig - The build config. + * @param buildConfig.buildTypes - The build types. + * @param buildConfig.features - The features. + * @returns An object representing the parsed arguments. + */ +export function parseArgv( + argv: string[], + { buildTypes, features }: BuildConfig, +) { + const allBuildTypeNames = Object.keys(buildTypes); + const allFeatureNames = Object.keys(features); + + // args like `production` may change our CLI defaults, so we pre-parse them + const preconditions = preParse(argv, prerequisites); + const options = getOptions(preconditions, allBuildTypeNames, allFeatureNames); + const args = getCli(options, 'yarn webpack').parseSync(argv); + // the properties `$0` and `_` are added by yargs, but we don't need them. We + // transform `add` and `omit`, so we also remove them from the config object. + const { $0, _, addFeature: add, omitFeature: omit, ...config } = args; + + // set up feature flags + const active = new Set(); + const defaultFeaturesForBuildType = buildTypes[config.type].features ?? []; + const setActive = (f: string) => omit.includes(f) || active.add(f); + [defaultFeaturesForBuildType, add].forEach((feat) => feat.forEach(setActive)); + + const ignore = new Set(['$0', 'conf', 'progress', 'stats', 'watch']); + const cacheKey = Object.entries(args) + .filter(([key]) => key.length > 1 && !ignore.has(key) && !key.includes('-')) + .sort(([x], [y]) => x.localeCompare(y)); + return { + // narrow the `config` type to only the options we're returning + args: config as { [key in OptionsKeys]: (typeof config)[key] }, + cacheKey: JSON.stringify(cacheKey), + features: { + active, + all: new Set(allFeatureNames), + }, + }; +} + +/** + * Gets a yargs instance for parsing CLI arguments. + * + * @param options + * @param name + */ +function getCli(options: T, name: string) { + const cli = yargs() + // Ensure unrecognized commands/options are reported as errors. + .strict() + // disable yargs's version, as we use it ourselves + .version(false) + // use the scriptName in `--help` output + .scriptName(name) + // wrap output at a maximum of 120 characters or `process.stdout.columns` + .wrap(Math.min(120, process.stdout.columns)) + // enable the `--config` command, which allows the user to specify a custom + // config file containing webpack options + .config() + .parserConfiguration({ + 'strip-aliased': true, + 'strip-dashed': true, + }) + // enable ENV parsing, which allows the user to specify webpack options via + // environment variables prefixed with `BUNDLE_` + // TODO: choose a better name than `BUNDLE` (it looks like `MM` is already being used in CI for ✨something✨) + .env(ENV_PREFIX) + // TODO: enable completion once https://github.com/yargs/yargs/pull/2422 is released. + // enable the `completion` command, which outputs a bash completion script + // .completion( + // 'completion', + // 'Enable bash/zsh completions; concat the script generated by running this command to your .bashrc or .bash_profile', + // ) + .example( + '$0 --env development --browser brave --browser chrome --zip', + 'Builds the extension for development for Chrome & Brave; generate zip files for both', + ) + // TODO: enable completion once https://github.com/yargs/yargs/pull/2422 is released. + // .example( + // '$0 completion', + // `Generates a bash completion script for the \`${name}\` command`, + // ) + .updateStrings({ + 'Options:': toOrange('Options:'), + 'Examples:': toOrange('Examples:'), + }) + .options(options); + return cli; +} + +type Options = ReturnType; + +function getOptions( + { env }: ReturnType, + buildTypes: string[], + allFeatures: string[], +) { + const isProduction = env === 'production'; + const prodDefaultDesc = "If `env` is 'production', `true`, otherwise `false`"; + return { + watch: { + alias: 'w', + array: false, + default: false, + description: 'Build then watch for files changes', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + cache: { + alias: 'c', + array: false, + default: true, + description: 'Cache build for faster rebuilds', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + progress: { + alias: 'p', + array: false, + default: true, + description: 'Show build progress', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + devtool: { + alias: 'd', + array: false, + default: isProduction ? 'hidden-source-map' : 'source-map', + defaultDescription: + "If `env` is 'production', 'hidden-source-map', otherwise 'source-map'", + description: 'Sourcemap type to generate', + choices: ['none', 'source-map', 'hidden-source-map'] as const, + group: toOrange('Developer assistance:'), + type: 'string', + }, + sentry: { + array: false, + default: isProduction, + defaultDescription: prodDefaultDesc, + description: 'Enables/disables Sentry Application Monitoring', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + test: { + array: false, + default: false, + description: 'Enables/disables testing mode', + group: toOrange('Developer assistance:'), + type: 'boolean', + }, + + ...prerequisites, + zip: { + alias: 'z', + array: false, + default: false, + description: 'Generate a zip file of the build', + group: toOrange('Build options:'), + type: 'boolean', + }, + minify: { + alias: 'm', + array: false, + default: isProduction, + defaultDescription: "If `env` is 'production', `true`, otherwise `false`", + description: 'Minify the output', + group: toOrange('Build options:'), + type: 'boolean', + }, + browser: { + alias: 'b', + array: true, + choices: ['all', ...Browsers], + coerce: (browsers: (Browser | 'all')[]) => { + type OneOrMoreBrowsers = [Browser, ...Browser[]]; + // sort browser for determinism (important for caching) + const set = new Set(browsers.sort()); + return (set.has('all') ? [...Browsers] : [...set]) as OneOrMoreBrowsers; + }, + default: 'chrome', + description: 'Browsers to build for', + group: toOrange('Build options:'), + type: 'string', + }, + manifest_version: { + alias: 'v', + array: false, + choices: [2, 3] as Manifest['manifest_version'][], + default: 2 as Manifest['manifest_version'], + description: "Changes manifest.json format to the given version's schema", + group: toOrange('Build options:'), + type: 'number', + }, + releaseVersion: { + alias: 'r', + array: false, + default: 0, + description: + 'The (pre)release version of the extension, e.g., the `6` in `18.7.25-flask.6`.', + group: toOrange('Build options:'), + type: 'number', + }, + type: { + alias: 't', + array: false, + choices: ['none', ...buildTypes], + default: 'main' as const, + description: 'Configure features for the build (main, beta, etc)', + group: toOrange('Build options:'), + type: 'string', + }, + [addFeat]: { + alias: 'a', + array: true, + choices: allFeatures, + coerce: uniqueSort, + default: [] as typeof allFeatures, + description: 'Add features not be included in the selected build `type`', + group: toOrange('Build options:'), + type: 'string', + }, + [omitFeat]: { + alias: 'o', + array: true, + choices: allFeatures, + coerce: uniqueSort, + default: [] as typeof allFeatures, + description: 'Omit features included in the selected build `type`', + group: toOrange('Build options:'), + type: 'string', + }, + + lavamoat: { + alias: 'l', + array: false, + default: isProduction, + defaultDescription: prodDefaultDesc, + description: 'Apply LavaMoat to the build assets', + group: toOrange('Security:'), + type: 'boolean', + }, + lockdown: { + alias: 'k', + array: false, + default: isProduction, + defaultDescription: prodDefaultDesc, + description: 'Enable/disable runtime hardening (also see --snow)', + group: toOrange('Security:'), + type: 'boolean', + }, + snow: { + alias: 's', + array: false, + default: isProduction, + defaultDescription: prodDefaultDesc, + description: 'Apply Snow to the build assets', + group: toOrange('Security:'), + type: 'boolean', + }, + + dryRun: { + array: false, + default: false, + description: 'Outputs the config without building', + group: toOrange('Options:'), + type: 'boolean', + }, + stats: { + array: false, + default: false, + description: 'Display build stats after building', + group: toOrange('Options:'), + type: 'boolean', + }, + } as const satisfies YargsOptionsMap; +} + +/** + * Returns a string representation of the given arguments and features. + * + * @param args + * @param features + */ +export function getDryRunMessage(args: Args, features: Features) { + return `🦊 Build Config 🦊 + +Environment: ${args.env} +Minify: ${args.minify} +Watch: ${args.watch} +Cache: ${args.cache} +Progress: ${args.progress} +Zip: ${args.zip} +Snow: ${args.snow} +LavaMoat: ${args.lavamoat} +Lockdown: ${args.lockdown} +Manifest version: ${args.manifest_version} +Release version: ${args.releaseVersion} +Browsers: ${args.browser.join(', ')} +Devtool: ${args.devtool} +Build type: ${args.type} +Features: ${[...features.active].join(', ')} +Test: ${args.test} +`; +} diff --git a/development/webpack/utils/config.ts b/development/webpack/utils/config.ts new file mode 100644 index 000000000000..49f4f9e97ba4 --- /dev/null +++ b/development/webpack/utils/config.ts @@ -0,0 +1,221 @@ +import { join } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { parse as parseYaml } from 'yaml'; +import { parse } from 'dotenv'; +import { setEnvironmentVariables } from '../../build/set-environment-variables'; +import type { Variables } from '../../lib/variables'; +import { type Args } from './cli'; +import { getExtensionVersion } from './version'; + +const BUILDS_YML_PATH = join(__dirname, '../../../builds.yml'); + +/** + * Coerce `"true"`, `"false"`, and `"null"` to their respective JavaScript + * values. Coerce the empty string (`""`) to `undefined`; + * + * @param value + * @returns + */ +function coerce(value: string) { + if (value === 'true') return true; + if (value === 'false') return false; + if (value === 'null') return null; + if (value === '') return null; + return value; +} + +/** + * @returns The definitions loaded from process.env. + */ +function loadEnv(): Map { + const definitions = new Map(); + Object.entries(process.env).forEach(([key, value]) => { + if (typeof value === 'undefined') return; + definitions.set(key, coerce(value)); + }); + return definitions; +} + +/** + * @param definitions + * @param rcFilePath - The path to the rc file. + */ +function addRc(definitions: Map, rcFilePath: string): void { + try { + const rc = parse(readFileSync(rcFilePath, 'utf8')); + Object.entries(rc).forEach(([key, value]) => { + if (definitions.has(key)) return; + definitions.set(key, coerce(value)); + }); + } catch { + // ignore + } +} + +/** + * Get the name for the current build. + * + * @param type + * @param build + * @param isDev + * @param args + */ +export function getBuildName( + type: string, + build: BuildType, + isDev: boolean, + args: Pick, +) { + const buildName = + build.buildNameOverride || + `MetaMask ${type.slice(0, 1).toUpperCase()}${type.slice(1)}`; + if (isDev) { + const mv3Str = args.manifest_version === 3 ? ' MV3' : ''; + const lavamoatStr = args.lavamoat ? ' lavamoat' : ''; + const snowStr = args.snow ? ' snow' : ''; + const lockdownStr = args.lockdown ? ' lockdown' : ''; + return `${buildName}${mv3Str}${lavamoatStr}${snowStr}${lockdownStr}`; + } + return buildName; +} + +/** + * Computes the `variables` (extension runtime's `process.env.*`). + * + * @param args + * @param args.type + * @param args.test + * @param args.env + * @param buildConfig + */ +export function getVariables( + { type, env, ...args }: Args, + buildConfig: BuildConfig, +) { + const activeBuild = buildConfig.buildTypes[type]; + const variables = loadConfigVars(activeBuild, buildConfig); + const version = getExtensionVersion(type, activeBuild, args.releaseVersion); + const isDevBuild = env === 'development'; + + function set(key: string, value: unknown): void; + function set(key: Record): void; + function set(key: string | Record, value?: unknown): void { + if (typeof key === 'object') { + Object.entries(key).forEach(([k, v]) => variables.set(k, v)); + } else { + variables.set(key, value); + } + } + + // use the gulp-build's function to set the environment variables + setEnvironmentVariables({ + buildName: getBuildName(type, activeBuild, isDevBuild, args), + buildType: type, + environment: env, + isDevBuild, + isTestBuild: args.test, + version: version.versionName, + variables: { + set, + isDefined(key: string): boolean { + return variables.has(key); + }, + get(key: string): unknown { + return variables.get(key); + }, + getMaybe(key: string): unknown { + return variables.get(key); + }, + } as Variables, + }); + + // variables that are used in the webpack build's entry points. Our runtime + // code checks for the _string_ `"true"`, so we cast to string here. + variables.set('ENABLE_SENTRY', args.sentry.toString()); + variables.set('ENABLE_SNOW', args.snow.toString()); + variables.set('ENABLE_LAVAMOAT', args.lavamoat.toString()); + variables.set('ENABLE_LOCKDOWN', args.lockdown.toString()); + + // convert the variables to a format that can be used by SWC, which expects + // values be JSON stringified, as it JSON.parses them internally. + const safeVariables: Record = {}; + variables.forEach((value, key) => { + if (value === null || value === undefined) return; + safeVariables[key] = JSON.stringify(value); + }); + + // special location for the PPOM_URI, as we don't want to copy the wasm file + // to the build directory like the gulp build does + variables.set( + 'PPOM_URI', + `new URL('@blockaid/ppom_release/ppom_bg.wasm', import.meta.url)`, + ); + // the `PPOM_URI` shouldn't be JSON stringified, as it's actually code + safeVariables.PPOM_URI = variables.get('PPOM_URI') as string; + + return { variables, safeVariables, version }; +} + +export type BuildType = { + id: number; + features?: string[]; + env?: (string | { [k: string]: unknown })[]; + isPrerelease?: boolean; + buildNameOverride?: string; +}; + +export type BuildConfig = { + buildTypes: Record; + env: (string | Record)[]; + features: Record< + string, + null | { env?: (string | { [k: string]: unknown })[] } + >; +}; + +/** + * + */ +export function getBuildTypes(): BuildConfig { + return parseYaml(readFileSync(BUILDS_YML_PATH, 'utf8')); +} + +/** + * Loads configuration variables from process.env, .metamaskrc, and build.yml. + * + * The order of precedence is: + * 1. process.env + * 2. .metamaskrc + * 3. build.yml + * + * i.e., if a variable is defined in `process.env`, it will take precedence over + * the same variable defined in `.metamaskrc` or `build.yml`. + * + * @param activeBuild + * @param build + * @param build.env + * @param build.features + * @returns + */ +function loadConfigVars( + activeBuild: Pick, + { env, features }: BuildConfig, +) { + const definitions = loadEnv(); + addRc(definitions, join(__dirname, '../../../.metamaskrc')); + addVars(activeBuild.env); + activeBuild.features?.forEach((feature) => addVars(features[feature]?.env)); + addVars(env); + + function addVars(pairs?: (string | Record)[]): void { + pairs?.forEach((pair) => { + if (typeof pair === 'string') return; + Object.entries(pair).forEach(([key, value]) => { + if (definitions.has(key)) return; + definitions.set(key, value); + }); + }); + } + + return definitions; +} diff --git a/development/webpack/utils/git.ts b/development/webpack/utils/git.ts new file mode 100644 index 000000000000..887db184ea01 --- /dev/null +++ b/development/webpack/utils/git.ts @@ -0,0 +1,60 @@ +import { spawnSync } from 'node:child_process'; +import { join, normalize } from 'node:path'; + +type Commit = { + hash: () => string; + timestamp: () => number; +}; + +/** + * Cache variable to store the most recent commit information. This is used to + * avoid repeated calls to the git command for performance optimization. + */ +const cache: Map = new Map(); + +/** + * Retrieves the most recent Git commit information, including its short hash + * and timestamp. If the information is cached, the cached value is returned to + * improve performance. Otherwise, it executes a git command to retrieve the + * latest commit's hash and timestamp, caches it, and then returns the + * information. + * + * @param gitDir + * @returns The latest commit's hash and timestamp. + */ +export function getLatestCommit( + gitDir: string = join(__dirname, '../../../.git'), +): Commit { + const cached = cache.get(gitDir); + if (cached) return cached; + + // execute the `git` command to get the latest commit's 8 character hash + // (`%h` and `--abbrev=8`) and authorship timestamp (seconds since the Unix + // epoch) + const hashLength = 8; + const args = [ + `--git-dir`, + normalize(gitDir), + 'log', + '-1', + '--format=%h%at', + `--abbrev=${hashLength}`, + ] as const; + const { stdout } = spawnSync('git', args, { + encoding: 'buffer', + env: process.env, + maxBuffer: 256, // we really only need like 19 bytes + stdio: ['ignore', 'pipe', 'ignore'], + }); + const response = { + hash() { + return stdout.toString('utf8', 0, hashLength); + }, + timestamp() { + // convert to milliseconds + return Number(stdout.toString('utf8', hashLength)) * 1000; + }, + }; + cache.set(gitDir, response); + return response; +} diff --git a/development/webpack/utils/helpers.ts b/development/webpack/utils/helpers.ts new file mode 100644 index 000000000000..2e7dc25b6da3 --- /dev/null +++ b/development/webpack/utils/helpers.ts @@ -0,0 +1,234 @@ +import { readdirSync } from 'node:fs'; +import { parse, join, relative, sep } from 'node:path'; +import type { Chunk, EntryObject, Stats } from 'webpack'; +import type TerserPluginType from 'terser-webpack-plugin'; + +export type Manifest = chrome.runtime.Manifest; +export type ManifestV2 = chrome.runtime.ManifestV2; +export type ManifestV3 = chrome.runtime.ManifestV3; + +// HMR (Hot Module Reloading) can't be used until all circular dependencies in +// the codebase are removed +// See: https://github.com/MetaMask/metamask-extension/issues/22450 +// TODO: remove this variable when HMR is ready. The env var is for tests and +// must also be removed everywhere. +export const __HMR_READY__ = Boolean(process.env.__HMR_READY__) || false; + +/** + * Target browsers + */ +export const Browsers = ['brave', 'chrome', 'firefox'] as const; +export type Browser = (typeof Browsers)[number]; + +const slash = `(?:\\${sep})?`; +/** + * Regular expression to match files in any `node_modules` directory + * Uses a platform-specific path separator: `/` on Unix-like systems and `\` on + * Windows. + */ +export const NODE_MODULES_RE = new RegExp(`${slash}node_modules${slash}`, 'u'); + +/** + * No Operation. A function that does nothing and returns nothing. + * + * @returns `undefined` + */ +export const noop = () => undefined; + +/** + * Collects all entry files for use with webpack. + * + * TODO: move this logic into the ManifestPlugin + * + * @param manifest - Base manifest file + * @param appRoot - Absolute directory to search for entry files listed in the + * base manifest + * @returns an `entry` object containing html and JS entry points for use with + * webpack, and an array, `manifestScripts`, list of filepaths of all scripts + * that were added to it. + */ +export function collectEntries(manifest: Manifest, appRoot: string) { + const entry: EntryObject = {}; + /** + * Scripts that must be self-contained and not split into chunks. + */ + const selfContainedScripts: Set = new Set([ + // Snow shouldn't be chunked + 'snow.prod', + 'use-snow', + ]); + + function addManifestScript(filename?: string) { + if (filename) { + selfContainedScripts.add(filename); + entry[filename] = { + chunkLoading: false, + filename, // output filename + import: join(appRoot, filename), // the path to the file to use as an entry + }; + } + } + + function addHtml(filename?: string) { + if (filename) { + assertValidEntryFileName(filename, appRoot); + entry[parse(filename).name] = join(appRoot, filename); + } + } + + // add content_scripts to entries + manifest.content_scripts?.forEach((s) => s.js?.forEach(addManifestScript)); + + if (manifest.manifest_version === 3) { + addManifestScript(manifest.background?.service_worker); + manifest.web_accessible_resources?.forEach(({ resources }) => + resources.forEach((filename) => { + filename.endsWith('.js') && addManifestScript(filename); + }), + ); + } else { + manifest.web_accessible_resources?.forEach((filename) => { + filename.endsWith('.js') && addManifestScript(filename); + }); + manifest.background?.scripts?.forEach(addManifestScript); + addHtml(manifest.background?.page); + } + + for (const filename of readdirSync(appRoot)) { + // ignore non-htm/html files + if (/\.html?$/iu.test(filename)) { + addHtml(filename); + } + } + + /** + * Ignore scripts that were found in the manifest, as these are only loaded by + * the browser extension platform itself. + * + * @param chunk + * @param chunk.name + * @returns + */ + function canBeChunked({ name }: Chunk): boolean { + return !name || !selfContainedScripts.has(name); + } + return { entry, canBeChunked }; +} + +/** + * @param filename + * @param appRoot + * @throws Throws an `Error` if the file is an invalid entrypoint filename + * (a file starting with "_") + */ +function assertValidEntryFileName(filename: string, appRoot: string) { + if (!filename.startsWith('_')) { + return; + } + + const relativeFile = relative(process.cwd(), join(appRoot, filename)); + const error = `Invalid Entrypoint Filename Detected\nPath: ${relativeFile}`; + const reason = `Filenames at the root of the extension directory starting with "_" are reserved for use by the browser.`; + const newFile = filename.slice(1); + const solutions = [ + `Rename this file to remove the underscore, e.g., '${filename}' to '${newFile}'`, + `Move this file to a subdirectory and, if necessary, add it manually to the build 😱`, + ]; + const context = `This file was included in the build automatically by our build script, which adds all HTML files at the root of '${appRoot}'.`; + + const message = `${error} + Reason: ${reason} + + Suggested Actions: + ${solutions.map((solution) => ` • ${solution}`).join('\n')} + ${`\n ${context}`} + `; + + throw new Error(message); +} + +/** + * It gets minimizers for the webpack build. + */ +export function getMinimizers() { + const TerserPlugin: typeof TerserPluginType = require('terser-webpack-plugin'); + return [ + new TerserPlugin({ + // use SWC to minify (about 7x faster than Terser) + minify: TerserPlugin.swcMinify, + // do not minify snow. + exclude: /snow\.prod/u, + }), + ]; +} + +/** + * Helpers for logging to the console with color. + */ +export const { colors, toGreen, toOrange, toPurple } = ((depth, esc) => { + if (depth === 1) { + const echo = (message: string): string => message; + return { colors: false, toGreen: echo, toOrange: echo, toPurple: echo }; + } + // 24: metamask green, 8: close to metamask green, 4: green + const green = { 24: '38;2;186;242;74', 8: '38;5;191', 4: '33' }[depth]; + // 24: metamask orange, 8: close to metamask orange, 4: red :-( + const orange = { 24: '38;2;247;85;25', 8: '38;5;208', 4: '31' }[depth]; + // 24: metamask purple, 8: close to metamask purple, 4: purple + const purple = { 24: '38;2;208;117;255', 8: '38;5;177', 4: '35' }[depth]; + return { + colors: { green: `${esc}[1;${green}m`, orange: `${esc}[1;${orange}m` }, + toGreen: (message: string) => `${esc}[1;${green}m${message}${esc}[0m`, + toOrange: (message: string) => `${esc}[1;${orange}m${message}${esc}[0m`, + toPurple: (message: string) => `${esc}[1;${purple}m${message}${esc}[0m`, + }; +})((process.stderr.getColorDepth?.() as 1 | 4 | 8 | 24) || 1, '\u001b'); + +/** + * Logs a summary of build information to `process.stderr` (webpack logs to + * stderr). + * + * Note: `err` and stats.hasErrors() are different. `err` prevents compilation + * from starting, while `stats.hasErrors()` is true if there were errors during + * compilation itself. + * + * @param err - If not `undefined`, logs the error to `process.stderr`. + * @param stats - If not `undefined`, logs the stats to `process.stderr`. + */ +export function logStats(err?: Error | null, stats?: Stats) { + if (err) { + console.error(err); + return; + } + + if (!stats) { + // technically this shouldn't happen, but webpack's TypeScript interface + // doesn't enforce that `err` and `stats` are mutually exclusive. + return; + } + + const { options } = stats.compilation; + // orange for production builds, purple for development + const colorFn = options.mode === 'production' ? toOrange : toPurple; + stats.compilation.name = colorFn(`🦊 ${stats.compilation.compiler.name}`); + if (options.stats === 'normal') { + // log everything (computing stats is slow, so we only do it if asked). + console.error(stats.toString({ colors })); + } else if (stats.hasErrors() || stats.hasWarnings()) { + // always log errors and warnings, if we have them. + console.error(stats.toString({ colors, preset: 'errors-warnings' })); + } else { + // otherwise, just log a simple update + const { name } = stats.compilation; + const status = toGreen('successfully'); + const time = `${stats.endTime - stats.startTime} ms`; + const { version } = require('webpack'); + console.error(`${name} (webpack ${version}) compiled ${status} in ${time}`); + } +} + +/** + * @param array + * @returns a new array with duplicate values removed and sorted + */ +export const uniqueSort = (array: string[]) => [...new Set(array)].sort(); diff --git a/development/webpack/utils/loaders/codeFenceLoader.ts b/development/webpack/utils/loaders/codeFenceLoader.ts new file mode 100644 index 000000000000..5088cb161f61 --- /dev/null +++ b/development/webpack/utils/loaders/codeFenceLoader.ts @@ -0,0 +1,59 @@ +import type { LoaderContext, RuleSetRule } from 'webpack'; +import type { JSONSchema7 } from 'schema-utils/declarations/validate'; +import { validate } from 'schema-utils'; +import { removeFencedCode, type FeatureLabels } from '@metamask/build-utils'; + +const schema: JSONSchema7 = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: ['features'], + properties: { + features: { + type: 'object', + description: + 'Configuration for code fence removal, specifying active and all possible features.', + required: ['active', 'all'], + properties: { + active: { + description: 'Features that should be included in the output.', + type: 'object', + }, + all: { + description: 'All features that can be toggled.', + type: 'object', + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, +}; + +export type CodeFenceLoaderOptions = { features: FeatureLabels }; + +type Context = LoaderContext; +function codeFenceLoader(this: Context, content: string, map?: string) { + const options = this.getOptions(); + validate(schema, options, { name: 'codeFenceLoader' }); + try { + const result = removeFencedCode( + this.resourcePath, + content, + options.features, + ); + this.callback(null, result[0], map); + } catch (error: unknown) { + this.callback(error as Error); + } +} + +export default codeFenceLoader; + +export type Loader = RuleSetRule & { options: CodeFenceLoaderOptions }; + +export function getCodeFenceLoader(features: FeatureLabels): Loader { + return { + loader: __filename, + options: { features }, + }; +} diff --git a/development/webpack/utils/loaders/swcLoader.ts b/development/webpack/utils/loaders/swcLoader.ts new file mode 100644 index 000000000000..b6976ff8d66d --- /dev/null +++ b/development/webpack/utils/loaders/swcLoader.ts @@ -0,0 +1,208 @@ +import type { LoaderContext } from 'webpack'; +import type { JSONSchema7 } from 'schema-utils/declarations/validate'; +import type { FromSchema } from 'json-schema-to-ts'; +import { validate } from 'schema-utils'; +import { transform, type Options } from '@swc/core'; +import { type Args } from '../cli'; +import { __HMR_READY__ } from '../helpers'; + +// the schema here is limited to only the options we actually use +// there are loads more options available to SWC we could add. +const schema = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + env: { + type: 'object', + properties: { + targets: { + description: 'The browsers to target (browserslist format).', + type: 'string', + }, + }, + additionalProperties: false, + }, + jsc: { + type: 'object', + properties: { + externalHelpers: { + type: 'boolean', + default: false, + }, + transform: { + type: 'object', + properties: { + optimizer: { + type: 'object', + properties: { + globals: { + description: '', + type: 'object', + properties: { + envs: { + description: + 'Replaces environment variables (`if (process.env.DEBUG) `) with specified values/expressions at compile time.', + anyOf: [ + { + type: 'array', + items: { + type: 'string', + }, + }, + { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + ], + }, + vars: { + description: + 'Replaces variables `if(__DEBUG__){}` with specified values/expressions at compile time.', + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + react: { + description: 'Effective only if `syntax` supports ƒ.', + type: 'object', + properties: { + development: { + description: + 'Toggles plugins that aid in development, such as @swc/plugin-transform-react-jsx-self and @swc/plugin-transform-react-jsx-source. Defaults to `false`.', + type: 'boolean', + }, + refresh: { + description: 'Enable fast refresh feature for React app', + type: 'boolean', + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + parser: { + description: 'Defaults to EsParserConfig (syntax: ecmascript)', + type: 'object', + properties: { + syntax: { + type: 'string', + default: 'ecmascript', + enum: ['ecmascript', 'typescript'], + }, + }, + oneOf: [ + { + properties: { + syntax: { + const: 'typescript', + }, + tsx: { + default: false, + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['syntax'], + }, + { + properties: { + syntax: { + const: 'ecmascript', + }, + jsx: { + default: false, + type: 'boolean', + }, + }, + additionalProperties: false, + required: ['syntax'], + }, + ], + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, +} as const satisfies JSONSchema7; + +type SchemaOptions = { keepDefaultedPropertiesOptional: true }; +export type SwcLoaderOptions = FromSchema; + +type Context = LoaderContext; +export default function swcLoader(this: Context, src: string, srcMap?: string) { + const pluginOptions = this.getOptions(); + validate(schema, pluginOptions, { name: 'swcLoader' }); + + const options: Options = { + ...pluginOptions, + envName: this.mode, + filename: this.resourcePath, + inputSourceMap: srcMap, + sourceFileName: this.resourcePath, + sourceMaps: this.sourceMap, + swcrc: false, + }; + + const cb = this.async(); + transform(src, options).then(({ code, map }) => cb(null, code, map), cb); +} + +export type SwcConfig = { + args: Pick; + safeVariables: Record; + browsersListQuery: string; + isDevelopment: boolean; +}; + +/** + * Gets the Speedy Web Compiler (SWC) loader for the given syntax. + * + * @param syntax + * @param enableJsx + * @param swcConfig + * @returns + */ +export function getSwcLoader( + syntax: 'typescript' | 'ecmascript', + enableJsx: boolean, + swcConfig: SwcConfig, +) { + return { + loader: __filename, + options: { + env: { + targets: swcConfig.browsersListQuery, + }, + jsc: { + externalHelpers: true, + transform: { + react: { + development: swcConfig.isDevelopment, + refresh: + __HMR_READY__ && swcConfig.isDevelopment && swcConfig.args.watch, + }, + optimizer: { + globals: { + envs: swcConfig.safeVariables, + }, + }, + }, + parser: { + syntax, + [syntax === 'typescript' ? 'tsx' : 'jsx']: enableJsx, + }, + }, + } as const satisfies SwcLoaderOptions, + }; +} diff --git a/development/webpack/utils/plugins/ManifestPlugin/helpers.ts b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts new file mode 100644 index 000000000000..82efa9acf253 --- /dev/null +++ b/development/webpack/utils/plugins/ManifestPlugin/helpers.ts @@ -0,0 +1,56 @@ +/** + * Returns a function that will transform a manifest JSON object based on the + * given build args. + * + * Applies the following transformations: + * - If `lockdown` is `false`, removes lockdown scripts from content_scripts + * - If `test` is `true`, adds the "tabs" permission to the manifest + * + * @param args + * @param args.lockdown + * @param args.test + * @returns a function that will transform the manifest JSON object + * @throws an error if the manifest already contains the "tabs" permission and + * `test` is `true` + */ +export function transformManifest(args: { lockdown: boolean; test: boolean }) { + const transforms: ((manifest: chrome.runtime.Manifest) => void)[] = []; + + function removeLockdown(browserManifest: chrome.runtime.Manifest) { + const mainScripts = browserManifest.content_scripts?.[0]; + if (mainScripts) { + const keep = ['scripts/contentscript.js', 'scripts/inpage.js']; + mainScripts.js = mainScripts.js?.filter((js) => keep.includes(js)); + } + } + + if (!args.lockdown) { + // remove lockdown scripts from content_scripts + transforms.push(removeLockdown); + } + + function addTabsPermission(browserManifest: chrome.runtime.Manifest) { + if (browserManifest.permissions) { + if (browserManifest.permissions.includes('tabs')) { + throw new Error( + "manifest contains 'tabs' already; this transform should be removed.", + ); + } + browserManifest.permissions.push('tabs'); + } else { + browserManifest.permissions = ['tabs']; + } + } + if (args.test) { + // test builds need "tabs" permission for switchToWindowWithTitle + transforms.push(addTabsPermission); + } + + return transforms.length + ? (browserManifest: chrome.runtime.Manifest, _browser: string) => { + const clone = structuredClone(browserManifest); + transforms.forEach((transform) => transform(clone)); + return clone; + } + : undefined; +} diff --git a/development/webpack/utils/plugins/ManifestPlugin/index.ts b/development/webpack/utils/plugins/ManifestPlugin/index.ts new file mode 100644 index 000000000000..c08cfd7ba6e6 --- /dev/null +++ b/development/webpack/utils/plugins/ManifestPlugin/index.ts @@ -0,0 +1,351 @@ +import { extname, join } from 'node:path/posix'; +import { readFileSync } from 'node:fs'; +import { + sources, + ProgressPlugin, + type Compilation, + type Compiler, + type Asset, +} from 'webpack'; +import { validate } from 'schema-utils'; +import { + type DeflateOptions, + Zip, + AsyncZipDeflate, + ZipPassThrough, +} from 'fflate'; +import { noop, type Manifest, Browser } from '../../helpers'; +import { schema } from './schema'; +import type { ManifestPluginOptions } from './types'; + +const { RawSource, ConcatSource } = sources; + +type Assets = Compilation['assets']; + +const NAME = 'ManifestPlugin'; +const BROWSER_TEMPLATE_RE = /\[browser\]/gu; + +/** + * Clones a Buffer or Uint8Array and returns it + * + * @param data + * @returns + */ +function clone(data: Buffer | Uint8Array): Buffer { + return Buffer.from(data); +} + +/** + * Adds the given asset to the zip file + * + * @param asset - The asset to add + * @param assetName - The name of the asset + * @param compress - Whether to compress the asset + * @param compressionOptions - The options to use for compression + * @param mtime - The modification time of the asset + * @param zip - The zip file to add the asset to + */ +function addAssetToZip( + asset: Buffer, + assetName: string, + compress: boolean, + compressionOptions: DeflateOptions | undefined, + mtime: number, + zip: Zip, +): void { + const zipFile = compress + ? new AsyncZipDeflate(assetName, compressionOptions) + : new ZipPassThrough(assetName); + zipFile.mtime = mtime; + zip.add(zipFile); + // use a copy of the Buffer, as Zip will consume it + zipFile.push(asset, true); +} + +/** + * A webpack plugin that generates extension manifests for browsers and organizes + * assets into browser-specific directories and optionally zips them. + * + * TODO: it'd be great if the logic to find entry points was also in this plugin + * instead of in helpers.ts. Moving that here would allow us to utilize the + * this.options.transform function to modify the manifest before collecting the + * entry points. + */ +export class ManifestPlugin { + /** + * File types that can be compressed well using DEFLATE compression, used when + * zipping assets. + */ + static compressibleFileTypes = new Set([ + '.bmp', + '.cjs', + '.css', + '.csv', + '.eot', + '.html', + '.js', + '.json', + '.log', + '.map', + '.md', + '.mjs', + '.svg', + '.txt', + '.wasm', + '.vtt', // very slow to process? + // ttf is disabled as some were getting corrupted during compression. You + // can test this by uncommenting it, running with --zip, and then unzipping + // the resulting zip file. If it is still broken the unzip operation will + // show an error. + // '.ttf', + '.wav', + '.xml', + ]); + + options: ManifestPluginOptions; + + manifests: Map = new Map(); + + constructor(options: ManifestPluginOptions) { + validate(schema, options, { name: NAME }); + this.options = options; + this.manifests = new Map(); + } + + apply(compiler: Compiler) { + compiler.hooks.compilation.tap(NAME, this.hookIntoPipelines.bind(this)); + } + + private async zipAssets( + compilation: Compilation, + assets: Assets, // an object of asset names to assets + options: ManifestPluginOptions, + ): Promise { + // TODO(perf): this zips (and compresses) every file individually for each + // browser. Can we share the compression and crc steps to save time? + const { browsers, zipOptions } = options; + const { excludeExtensions, level, outFilePath, mtime } = zipOptions; + const compressionOptions: DeflateOptions = { level }; + const assetsArray = Object.entries(assets); + + let filesProcessed = 0; + const numAssetsPerBrowser = assetsArray.length + 1; + const totalWork = numAssetsPerBrowser * browsers.length; // +1 for each browser's manifest.json + const reportProgress = + ProgressPlugin.getReporter(compilation.compiler) || noop; + // TODO(perf): run this in parallel. If you try without carefully optimizing the + // process will run out of memory pretty quickly, and crash. Fun! + for (const browser of browsers) { + const manifest = this.manifests.get(browser) as sources.Source; + const source = await new Promise((resolve, reject) => { + // since Zipping is async, a past chunk could cause an error after we've + // started processing additional chunks. We'll use this errored flag to + // short-circuit the rest of the processing if that happens. + let errored = false; + const zipSource = new ConcatSource(); + const zip = new Zip((error, data, final) => { + if (errored) return; // ignore additional errors + if (error) { + // set error flag to prevent additional processing + errored = true; + reject(error); + } else { + zipSource.add(new RawSource(clone(data))); + // we've received our final bit of data, return the zipSource + if (final) resolve(zipSource); + } + }); + + // add the browser's manifest.json file to the zip + addAssetToZip( + manifest.buffer(), + 'manifest.json', + true, + compressionOptions, + mtime, + zip, + ); + + const message = `${++filesProcessed}/${totalWork} assets zipped for ${browser}`; + reportProgress(0, message, 'manifest.json'); + + for (const [assetName, asset] of assetsArray) { + if (errored) return; + + const extName = extname(assetName); + if (excludeExtensions.includes(extName)) continue; + + addAssetToZip( + // make a copy of the asset Buffer as Zipping will *consume* it, + // which breaks things if we are compiling for multiple browsers. + clone(asset.buffer()), + assetName, + ManifestPlugin.compressibleFileTypes.has(extName), + compressionOptions, + mtime, + zip, + ); + reportProgress( + 0, + `${++filesProcessed}/${totalWork} assets zipped for ${browser}`, + assetName, + ); + } + + zip.end(); + }); + + // add the zip file to webpack's assets. + const zipFilePath = outFilePath.replace(BROWSER_TEMPLATE_RE, browser); + compilation.emitAsset(zipFilePath, source, { + javascriptModule: false, + compressed: true, + contentType: 'application/zip', + development: true, + }); + } + } + + /** + * Moves the assets to the correct browser locations and adds each browser's + * extension manifest.json file to the list of assets. + * + * @param compilation + * @param assets + * @param options + */ + private moveAssets( + compilation: Compilation, + assets: Assets, + options: ManifestPluginOptions, + ): void { + // we need to wait to delete assets until after we've zipped them all + const assetDeletions = new Set(); + const { browsers } = options; + const assetEntries = Object.entries(assets); + browsers.forEach((browser) => { + const manifest = this.manifests.get(browser) as sources.Source; + compilation.emitAsset(join(browser, 'manifest.json'), manifest, { + javascriptModule: false, + contentType: 'application/json', + }); + for (const [name, asset] of assetEntries) { + // move the assets to their final browser-relative locations + const assetDetails = compilation.getAsset(name) as Readonly; + compilation.emitAsset(join(browser, name), asset, assetDetails.info); + assetDeletions.add(name); + } + }); + // delete the assets after we've zipped them all + assetDeletions.forEach((assetName) => compilation.deleteAsset(assetName)); + } + + private prepareManifests(compilation: Compilation): void { + const context = compilation.options.context as string; + const manifestPath = join( + context, + `manifest/v${this.options.manifest_version}`, + ); + // Load the base manifest + const basePath = join(manifestPath, `_base.json`); + const baseManifest: Manifest = JSON.parse(readFileSync(basePath, 'utf8')); + + const { transform } = this.options; + const resources = this.options.web_accessible_resources; + const description = this.options.description + ? `${baseManifest.description} – ${this.options.description}` + : baseManifest.description; + const { version } = this.options; + + this.options.browsers.forEach((browser) => { + let manifest: Manifest = { ...baseManifest, description, version }; + + if (browser !== 'firefox') { + // version_name isn't used by FireFox, but is by Chrome, et al. + manifest.version_name = this.options.versionName; + } + + try { + const browserManifestPath = join(manifestPath, `${browser}.json`); + // merge browser-specific overrides into the browser manifest + manifest = { + ...manifest, + ...require(browserManifestPath), + }; + } catch { + // ignore if the file doesn't exist, as some browsers might not need overrides + } + + // merge provided `web_accessible_resources` + if (resources && resources.length > 0) { + if (manifest.manifest_version === 3) { + manifest.web_accessible_resources = + manifest.web_accessible_resources || []; + const war = manifest.web_accessible_resources.find((resource) => + resource.matches.includes(''), + ); + if (war) { + // merge the resources into the existing resource, ensure uniqueness using `Set` + war.resources = [...new Set([...war.resources, ...resources])]; + } else { + // add a new resource + manifest.web_accessible_resources.push({ + matches: [''], + resources: [...resources], + }); + } + } else { + manifest.web_accessible_resources = [ + ...(manifest.web_accessible_resources || []), + ...resources, + ]; + } + } + + // allow the user to `transform` the manifest. Use a copy of the manifest + // so modifications for one browser don't affect other browsers. + if (transform) { + manifest = transform?.(JSON.parse(JSON.stringify(manifest)), browser); + } + + // Add the manifest file to the assets + const source = new RawSource(JSON.stringify(manifest, null, 2)); + this.manifests.set(browser, source); + }); + } + + private hookIntoPipelines(compilation: Compilation): void { + // prepare manifests early so we can catch errors early instead of waiting + // until the end of the compilation. + this.prepareManifests(compilation); + + // TODO: MV3 needs to be handled differently. Specifically, it needs to + // load the files it needs via a function call to `importScripts`, plus some + // other shenanigans. + + // hook into the processAssets hook to move/zip assets + const tapOptions = { + name: NAME, + stage: Infinity, + }; + if (this.options.zip) { + const options = this.options as ManifestPluginOptions; + compilation.hooks.processAssets.tapPromise( + tapOptions, + async (assets: Assets) => { + await this.zipAssets(compilation, assets, options); + this.moveAssets( + compilation, + assets, + this.options as ManifestPluginOptions, + ); + }, + ); + } else { + const options = this.options as ManifestPluginOptions; + compilation.hooks.processAssets.tap(tapOptions, (assets: Assets) => { + this.moveAssets(compilation, assets, options); + }); + } + } +} diff --git a/development/webpack/utils/plugins/ManifestPlugin/schema.ts b/development/webpack/utils/plugins/ManifestPlugin/schema.ts new file mode 100644 index 000000000000..e6d5efac7ff6 --- /dev/null +++ b/development/webpack/utils/plugins/ManifestPlugin/schema.ts @@ -0,0 +1,115 @@ +import { ExtendedJSONSchema } from 'json-schema-to-ts'; +import { Browsers } from '../../helpers'; + +type Writeable = { -readonly [P in keyof T]: T[P] }; + +export const schema = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + required: ['browsers', 'description', 'manifest_version', 'version', 'zip'], + properties: { + browsers: { + description: 'The browsers to build for.', + type: 'array', + items: { + type: 'string', + enum: Browsers as Writeable, + }, + minItems: 1, + maxItems: Browsers.length, + uniqueItems: true, + }, + version: { + description: + 'One to four dot-separated integers identifying the version of this extension.', + type: 'string', + }, + versionName: { + description: + 'A Semantic Versioning-compliant version number for the extension.', + type: 'string', + }, + description: { + description: 'A plain text string that describes the extension.', + type: ['string', 'null'], + maxLength: 132, + }, + manifest_version: { + description: + 'An integer specifying the version of the manifest file format your package requires.', + type: 'number', + enum: [2, 3], + }, + web_accessible_resources: { + description: + 'An array of strings specifying the paths of additional web-accessible resources.', + type: 'array', + items: { + type: 'string', + }, + }, + transform: { + description: 'Function to transform the manifest file.', + instanceof: 'Function', + tsType: '((manifest: Manifest, browser: Browser) => Manifest)', + }, + zip: { + description: 'Whether or not to zip the individual browser builds.', + type: 'boolean', + }, + zipOptions: { + required: ['outFilePath'], + properties: { + level: { + description: + 'Compression level for compressible assets. 0 is no compression, 9 is maximum compression. 6 is default.', + type: 'number', + default: 6, + minimum: 0, + maximum: 9, + }, + mtime: { + description: + 'Modification time for all files in the zip, specified as a UNIX timestamp (milliseconds since 1 January 1970 UTC). This property sets a uniform modification time for the contents of the zip file. Note: Zip files use FAT file timestamps, which have a limited range. Therefore, datetimes before 1980-01-01 (timestamp value of 315532800000) are invalid in standard Zip files, and datetimes on or after 2100-01-01 (timestamp value of 4102444800000) are also invalid. Values must fall within this range.', + type: 'number', + // Zip files use FAT file timestamps, which have a limited range. + // Datetimes before 1980-01-01 are invalid in standard Zip files. + minimum: Date.UTC(1980, 0, 1), + // datetimes after 2099-12-31 are invalid in zip files + exclusiveMaximum: Date.UTC(2100, 0, 1), + get default() { + return Date.now(); + }, + }, + excludeExtensions: { + description: 'File extensions to exclude from zip.', + type: 'array', + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\.[a-zA-Z0-9]+$', + }, + default: [], + }, + outFilePath: { + description: + 'File path template for zip file relative to webpack output directory. You must include `[browser]` in the file path template, which will be replaced with the browser name. For example, `builds/[browser].zip`.', + type: 'string', + pattern: '.*\\[browser\\].*', + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + if: { + properties: { + zip: { + const: true, + }, + }, + }, + then: { + required: ['zipOptions'], + }, +} satisfies ExtendedJSONSchema>; diff --git a/development/webpack/utils/plugins/ManifestPlugin/types.ts b/development/webpack/utils/plugins/ManifestPlugin/types.ts new file mode 100644 index 000000000000..174d09d4f6b6 --- /dev/null +++ b/development/webpack/utils/plugins/ManifestPlugin/types.ts @@ -0,0 +1,94 @@ +import type { Browser, Manifest } from '../../helpers'; + +export type BaseManifestPluginOptions = { + /** + * The browsers to build for. + */ + browsers: readonly [Browser, ...Browser[]]; + + /** + * An array of strings specifying the paths of additional web-accessible resources. + */ + web_accessible_resources?: readonly string[]; + + /** + * An integer specifying the version of the manifest file format your package requires + */ + manifest_version: 2 | 3; + + /** + * One to four dot-separated integers identifying the version of this extension. A couple of rules apply to the integers: + * + * * The integers must be between 0 and 65535, inclusive. + * * Non-zero integers can't start with 0. For example, 032 is invalid because it begins with a zero. + * * They must not be all zero. For example, 0 and 0.0.0.0 are invalid while 0.1.0.0 is valid. + * + * Here are some examples of valid versions: + * + * * "version": "1" + * * "version": "1.0" + * * "version": "2.10.2" + * * "version": "3.1.2.4567" + * + * If the published extension has a newer version string than the installed extension, then the extension is automatically updated. + * + * The comparison starts with the leftmost integers. Then, if those integers are equal, the integers to the right are compared, and so on. For example, 1.2.0 is a newer version than 1.1.9.9999. + * + * A missing integer is equal to zero. For example, 1.1.9.9999 is newer than 1.1, and 1.1.9.9999 is older than 1.2. + */ + version: string; + + /** + * A Semantic Versioning-compliant version number for the extension. Not used in Firefox builds since Firefox doesn't currently support it. + */ + versionName: string; + + /** + * A plain text string (no HTML or other formatting; no more than 132 characters) that describes the extension. + * + * The description should be suitable for both the browser's Extensions page, e.g., chrome://extensions, and extension web stores. You can specify locale-specific strings for this field. + */ + description: string | null; + + /** + * Function to transform the manifest file. + * + * @param manifest + * @param browser + * @returns + */ + transform?: (manifest: Manifest, browser: Browser) => Manifest; + + /** + * Whether or not to zip the individual browser builds. + */ + zip: Zip; +}; + +export type ZipOptions = { + /** + * Options for the zip. + */ + zipOptions: { + /** + * Compression level for compressible assets. 0 is no compression, 9 is maximum compression. 6 is default. + */ + level: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + /** + * Modification time for all files in the zip, specified as a UNIX timestamp (milliseconds since 1 January 1970 UTC). This property sets a uniform modification time for the contents of the zip file. Note: Zip files use FAT file timestamps, which have a limited range. Therefore, datetimes before 1980-01-01 (timestamp value of 315532800000) are invalid in standard Zip files, and datetimes on or after 2100-01-01 (timestamp value of 4102444800000) are also invalid. Values must fall within this range. + */ + mtime: number; + /** + * File extensions to exclude from zip; should include the `.`, e.g., [`.map`]. + */ + excludeExtensions: string[]; + + /** + * File path template for zip file relative to webpack output directory. You must include `[browser]` in the file path template, which will be replaced with the browser name. For example, `builds/[browser].zip`. + */ + outFilePath: string; + }; +}; + +export type ManifestPluginOptions = + BaseManifestPluginOptions & (Zip extends true ? ZipOptions : object); diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts new file mode 100644 index 000000000000..b80f6102ab75 --- /dev/null +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -0,0 +1,175 @@ +import { dirname, relative } from 'node:path'; +import { ModuleFilenameHelpers, Compilation, sources } from 'webpack'; +import { validate } from 'schema-utils'; +import { schema } from './schema'; +import type { SelfInjectPluginOptions, Source, Compiler } from './types'; + +export { type SelfInjectPluginOptions } from './types'; + +/** + * Default options for the SelfInjectPlugin. + */ +const defaultOptions = { + // The default `sourceUrlExpression` is configured for browser extensions. + // It generates the absolute url of the given file as an extension url. + // e.g., `chrome-extension:///scripts/inpage.js` + sourceUrlExpression: (filename: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`, +} satisfies SelfInjectPluginOptions; + +/** + * Modifies processed assets to inject a script tag that will execute the asset + * as an inline script. Primarily used in Chromium extensions that need to + * access a tab's `window` object from a `content_script`. + * + * @example + * Input: + * ```js + * // webpack.config.js + * module.exports = {plugins: [new SelfInjectPlugin({ test: /\.js$/ })]}; + * ``` + * + * ```js + * // src/index.js + * console.log("hello world"); + * ``` + * Output: + * ```js + * // dist/main.js + * {let d=document,s=d.createElement('script');s.textContent="console.log(\"hello world\");\n//# sourceMappingURL=main.js.map"+`\n//# sourceURL=${(globalThis.browser||chrome).runtime.getURL("main.js")};`;d.documentElement.appendChild(s).remove()} + * ``` + * ```json + * // dist/main.js.map (example) + * {"version":3,"file":"x","mappings":"AAAAA,QAAQC,IAAI","sources":["webpack://./src/index.js"],"sourcesContent":["console.log(\"hello world\");"],"names":["console","log"]} + * ``` + */ +export class SelfInjectPlugin { + private options: SelfInjectPluginOptions & typeof defaultOptions; + + constructor(options: SelfInjectPluginOptions) { + validate(schema, options, { name: SelfInjectPlugin.name }); + + this.options = { ...defaultOptions, ...options }; + } + + apply(compiler: Compiler): void { + compiler.hooks.compilation.tap(SelfInjectPlugin.name, (compilation) => { + this.processAssets(compilation); + }); + } + + /** + * Hooks into the compilation process to modify assets. + * + * @param compilation + */ + processAssets(compilation: Compilation): void { + const opts = { + name: SelfInjectPlugin.name, + stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING, + }; + compilation.hooks.processAssets.tap(opts, () => this.process(compilation)); + } + + /** + * Processes compilation assets to inject a script tag that will execute the + * asset as an inline script. + * + * @param compilation + */ + process(compilation: Compilation): void { + const { test } = this.options; + const match = ModuleFilenameHelpers.matchObject.bind(null, { test }); + + for (const chunk of compilation.chunks) { + for (const file of chunk.files) { + if (match(file)) { + compilation.updateAsset(file, (asset: Source) => { + return this.updateAsset(compilation, file, asset); + }); + } + } + } + } + + /** + * Updates the given asset to inject a script tag that will execute the asset + * as an inline script. + * + * @param compilation + * @param file + * @param asset + */ + updateAsset(compilation: Compilation, file: string, asset: Source): Source { + const { ConcatSource, RawSource } = sources; + const { map, source } = asset.sourceAndMap(); + + let sourceMappingURLComment = ''; + // emit a separate source map file (if this asset already has one) + if (map /* `map` can be `null`; webpack's types are wrong */) { + const { devtool } = compilation.options; + const sourceMapPath = `${file}.map`; + + // we're removing the source map from the original webpack asset, since + // it's now a different file that isn't mappable, so we need to re-add it + // as a new asset: + const mapSource = new RawSource(JSON.stringify(map)); + compilation.emitAsset(sourceMapPath, mapSource); + + // we must "hide" the `sourceMappingURL` from the file when `hidden` + // source maps are requested by omitting the reference from the source + if (devtool && !devtool.startsWith('hidden-')) { + // `sourceMappingURL` needs to be relative to the file so that the + // browser's dev tools can find it. + const sourceMappingURL = relative(dirname(file), sourceMapPath); + sourceMappingURLComment = `\n//# sourceMappingURL=${sourceMappingURL}`; + } + } + + // generate the new self-injecting source code: + const newSource = new ConcatSource(); + // wrapped in a new lexical scope so we don't pollute the global namespace + newSource.add(`{`); + newSource.add(`let d=document,s=d.createElement('script');`); + newSource.add(`s.textContent=`); + newSource.add(this.escapeJs(source + sourceMappingURLComment)); + newSource.add(`+`); + // The browser's dev tools can't map our inline javascript back to its + // source. We add a sourceURL directive to help with that. It also helps + // organize the Sources panel in browser dev tools by separating the inline + // script into its own origin. + newSource.add( + `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, + ); + newSource.add(`;`); + // add and immediately remove the script to avoid modifying the DOM. + newSource.add(`d.documentElement.appendChild(s).remove()`); + newSource.add(`}`); + + return newSource; + } + + /** + * Escapes the given JavaScript source as a JavaScript string. + * + * Replaces line separators and paragraph separators with their unicode escape + * sequences. + * + * @example + * ```js + * escapeJs(`console.log('hello world');`); + * // => "\"console.log('hello world');\"" + * ``` + * @param source + * @returns + */ + private escapeJs(source: string): string { + return ( + JSON.stringify(source) + // replace line separators + .replace(/\u2028/gu, '\\u2028') + // and paragraph separators + .replace(/\u2029/gu, '\\u2029') + ); + } +} diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/schema.ts b/development/webpack/utils/plugins/SelfInjectPlugin/schema.ts new file mode 100644 index 000000000000..2b674ba93f31 --- /dev/null +++ b/development/webpack/utils/plugins/SelfInjectPlugin/schema.ts @@ -0,0 +1,58 @@ +import { ExtendedJSONSchema } from 'json-schema-to-ts'; + +export const schema = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + exclude: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp', tsType: 'RegExp' }, + { + type: 'array', + items: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp', tsType: 'RegExp' }, + ], + }, + }, + ], + }, + include: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp' }, + { + type: 'array', + items: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp', tsType: 'RegExp' }, + ], + }, + }, + ], + }, + test: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp', tsType: 'RegExp' }, + { + type: 'array', + items: { + oneOf: [ + { type: 'string' }, + { instanceof: 'RegExp', tsType: 'RegExp' }, + ], + }, + }, + ], + }, + sourceUrlExpression: { + instanceof: 'Function', + tsType: '((filename: string) => string)', + }, + }, + additionalProperties: false, +} satisfies ExtendedJSONSchema>; diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts new file mode 100644 index 000000000000..2240227bd76a --- /dev/null +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -0,0 +1,49 @@ +import type { Asset } from 'webpack'; + +export type { Compiler } from 'webpack'; +export type Source = Asset['source']; + +/** + * Options for the SelfInjectPlugin. + */ +export type SelfInjectPluginOptions = { + /** + * Specify which chunks to apply the transformation to. + * + * @example + * ```js + * { + * test: /inpage/, + * } + * ``` + */ + test?: string | RegExp | (string | RegExp)[]; + /** + * A function that returns a JavaScript expression escaped as a string which + * will be injected into matched file to provide a sourceURL for the self + * injected script. + * + * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * + * @example Custom + * ```js + * Appends a runtime URL for a website, e.g., + * `// //# sourceURL=https://google.com/scripts/myfile.js` + * { + * sourceUrlExpression: (filename) => `document.location.origin/${filename}` + * } + * ``` + * @example Default + * Appends a runtime URL for a browser extension, e.g., + * `//# sourceURL=chrome-extension:///scripts/inpage.js` + * + * ```js + * { + * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * } + * ``` + * @param filename - the chunk's relative filename as it will exist in the output directory + * @returns + */ + sourceUrlExpression?: (filename: string) => string; +}; diff --git a/development/webpack/utils/version.ts b/development/webpack/utils/version.ts new file mode 100644 index 000000000000..ea9cbe7b6e24 --- /dev/null +++ b/development/webpack/utils/version.ts @@ -0,0 +1,70 @@ +import type { BuildType } from './config'; + +/** + * Computes the version number for use in the extension manifest. Uses the + * `version` field in the project's `package.json`. + * + * @param type + * @param options + * @param options.id + * @param options.isPrerelease + * @param releaseVersion + * @returns Returns the version and version_name values for the extension. + */ +export const getExtensionVersion = ( + type: string, + { id, isPrerelease }: Pick, + releaseVersion: number, +): { version: string; versionName: string } => { + const { version } = require('../../../package.json') as { version: string }; + + if (id < 10 || id > 64 || releaseVersion < 0 || releaseVersion > 999) { + throw new Error( + `Build id must be 10-64 and release version must be 0-999 +(inclusive). Received an id of '${id}' and a release version of +'${releaseVersion}'. + +Wait, but that seems so arbitrary? +================================== + +We encode the build id and the release version into the extension version by +concatenating the two numbers together. The maximum value for the concatenated +number is 65535 (a Chromium limitation). The value cannot start with a '0'. We +utilize 2 digits for the build id and 3 for the release version. This affords us +55 release types and 1000 releases per 'version' + build type. + +Okay, so how do I fix it? +========================= + +You'll need to adjust the build 'id' (in builds.yml) or the release version to +fit within these limits or bump the version number in package.json and start the +release version number over from 0. If you can't do that you'll need to come up +with a new way of encoding this information, or re-evaluate the need for this +metadata. + +Good luck on your endeavors.`, + ); + } + + if (!isPrerelease) { + if (releaseVersion !== 0) { + throw new Error( + `A '${type}' build's release version must always be '0'. Got '${releaseVersion}' instead.`, + ); + } + // main build (non-prerelease) version_name is just a plain version number + // the version field needs the `.0` because some runtime code freaks out + // if it's missing. + return { + version: `${version}.0`, + versionName: version, + }; + } + return { + // if version=18.7.25, id=10, releaseVersion=12 we get 18.7.25.1012 + version: `${version}.${id}${releaseVersion}`, + // The manifest.json's `version_name` field can be anything we want, so we + // make it human readable, e.g., `18.7.25-beta.123`. + versionName: `${version}-${type}.${releaseVersion}`, + }; +}; diff --git a/development/webpack/webpack.config.ts b/development/webpack/webpack.config.ts new file mode 100644 index 000000000000..d207b3ae741c --- /dev/null +++ b/development/webpack/webpack.config.ts @@ -0,0 +1,395 @@ +/** + * @file The main webpack configuration file for the browser extension. + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { argv, exit } from 'node:process'; +import { + ProvidePlugin, + type Configuration, + type WebpackPluginInstance, + type Chunk, + type MemoryCacheOptions, + type FileCacheOptions, +} from 'webpack'; +import CopyPlugin from 'copy-webpack-plugin'; +import HtmlBundlerPlugin from 'html-bundler-webpack-plugin'; +import rtlCss from 'postcss-rtlcss'; +import autoprefixer from 'autoprefixer'; +import type ReactRefreshPluginType from '@pmmmwh/react-refresh-webpack-plugin'; +import { SelfInjectPlugin } from './utils/plugins/SelfInjectPlugin'; +import { + type Manifest, + collectEntries, + getMinimizers, + NODE_MODULES_RE, + __HMR_READY__, +} from './utils/helpers'; +import { transformManifest } from './utils/plugins/ManifestPlugin/helpers'; +import { parseArgv, getDryRunMessage } from './utils/cli'; +import { getCodeFenceLoader } from './utils/loaders/codeFenceLoader'; +import { getSwcLoader } from './utils/loaders/swcLoader'; +import { getBuildTypes, getVariables } from './utils/config'; +import { ManifestPlugin } from './utils/plugins/ManifestPlugin'; +import { getLatestCommit } from './utils/git'; + +const buildTypes = getBuildTypes(); +const { args, cacheKey, features } = parseArgv(argv.slice(2), buildTypes); +if (args.dryRun) { + console.error(getDryRunMessage(args, features)); + exit(0); +} + +// #region short circuit for unsupported build configurations +if (args.lavamoat) { + throw new Error("The webpack build doesn't support LavaMoat yet. So sorry."); +} +if (args.manifest_version === 3) { + throw new Error( + "The webpack build doesn't support manifest_version version 3 yet. So sorry.", + ); +} +// #endregion short circuit for unsupported build configurations + +const context = join(__dirname, '../../app'); +const isDevelopment = args.env === 'development'; +const MANIFEST_VERSION = args.manifest_version; +const manifestPath = join(context, `manifest/v${MANIFEST_VERSION}/_base.json`); +const manifest: Manifest = require(manifestPath); +const { entry, canBeChunked } = collectEntries(manifest, context); +const codeFenceLoader = getCodeFenceLoader(features); +const browsersListPath = join(context, '../.browserslistrc'); +// read .browserslist now to stop it from searching for the file over and over +const browsersListQuery = readFileSync(browsersListPath, 'utf8'); +const { variables, safeVariables, version } = getVariables(args, buildTypes); +const webAccessibleResources = + args.devtool === 'source-map' + ? ['scripts/inpage.js.map', 'scripts/contentscript.js.map'] + : []; + +// #region cache +const cache = args.cache + ? ({ + type: 'filesystem', + name: `MetaMask—${args.env}`, + version: cacheKey, + idleTimeout: 0, + idleTimeoutForInitialStore: 0, + idleTimeoutAfterLargeChanges: 0, + // small performance gain by increase memory generations + maxMemoryGenerations: Infinity, + // Disable allowCollectingMemory because it can slow the build by 10%! + allowCollectingMemory: false, + buildDependencies: { + defaultConfig: [__filename], + // Invalidates the build cache when the listed files change. + // `__filename` makes all `require`d dependencies of *this* file + // `buildDependencies` + config: [ + __filename, + join(context, '../.metamaskrc'), + join(context, '../builds.yml'), + browsersListPath, + ], + }, + } as const satisfies FileCacheOptions) + : ({ type: 'memory' } as const satisfies MemoryCacheOptions); +// #endregion cache + +// #region plugins +const commitHash = isDevelopment ? getLatestCommit().hash() : null; +const plugins: WebpackPluginInstance[] = [ + new SelfInjectPlugin({ test: /^scripts\/inpage\.js$/u }), + // HtmlBundlerPlugin treats HTML files as entry points + new HtmlBundlerPlugin({ + preprocessorOptions: { useWith: false }, + minify: args.minify, + integrity: 'auto', + test: /\.html$/u, // default is eta/html, we only want html + data: { + isMMI: args.type === 'mmi', + isTest: args.test, + shouldIncludeSnow: args.snow, + }, + }), + new ManifestPlugin({ + web_accessible_resources: webAccessibleResources, + manifest_version: MANIFEST_VERSION, + description: commitHash + ? `${args.env} build from git id: ${commitHash.substring(0, 8)}` + : null, + version: version.version, + versionName: version.versionName, + browsers: args.browser, + transform: transformManifest(args), + zip: args.zip, + ...(args.zip + ? { + zipOptions: { + outFilePath: `../../builds/metamask-[browser]-${version.versionName}.zip`, // relative to output.path + mtime: getLatestCommit().timestamp(), + excludeExtensions: ['.map'], + // `level: 9` is the highest; it may increase build time by ~5% over level 1 + level: 9, + }, + } + : {}), + }), + // use ProvidePlugin to polyfill *global* node variables + new ProvidePlugin({ + // Make a global `Buffer` variable that points to the `buffer` package. + Buffer: ['buffer', 'Buffer'], + // Make a global `process` variable that points to the `process` package. + process: 'process/browser', + // polyfill usages of `setImmediate`, ideally this would be automatically + // handled by `swcLoader`'s `env.usage = 'entry'` option, but that setting + // results in a compilation error: `Module parse failed: 'import' and + // 'export' may appear only with 'sourceType: module' (2:0)`. I spent a few + // hours trying to figure it out but couldn't. So, this is the workaround. + // Note: we should probably remove usages of `setImmediate` from our + // codebase so we don't have to polyfill it. + setImmediate: 'core-js-pure/actual/set-immediate', + }), + new CopyPlugin({ + patterns: [ + { from: join(context, '_locales'), to: '_locales' }, // translations + // misc images + // TODO: fix overlap between this folder and automatically bundled assets + { from: join(context, 'images'), to: 'images' }, + ], + }), +]; +// enable React Refresh in 'development' mode when `watch` is enabled +if (__HMR_READY__ && isDevelopment && args.watch) { + const ReactRefreshWebpackPlugin: typeof ReactRefreshPluginType = require('@pmmmwh/react-refresh-webpack-plugin'); + plugins.push(new ReactRefreshWebpackPlugin()); +} +if (args.progress) { + const { ProgressPlugin } = require('webpack'); + plugins.push(new ProgressPlugin()); +} +// #endregion plugins + +const swcConfig = { args, safeVariables, browsersListQuery, isDevelopment }; +const tsxLoader = getSwcLoader('typescript', true, swcConfig); +const jsxLoader = getSwcLoader('ecmascript', true, swcConfig); +const ecmaLoader = getSwcLoader('ecmascript', false, swcConfig); + +const config = { + entry, + cache, + plugins, + context, + mode: args.env, + stats: args.stats ? 'normal' : 'none', + name: `MetaMask – ${args.env}`, + // use the `.browserlistrc` file directly to avoid browserslist searching + target: `browserslist:${browsersListPath}:defaults`, + // TODO: look into using SourceMapDevToolPlugin and its exclude option to speed up the build + // TODO: put source maps in an upper level directory (like the gulp build does now) + // see: https://webpack.js.org/plugins/source-map-dev-tool-plugin/#host-source-maps-externally + devtool: args.devtool === 'none' ? false : args.devtool, + output: { + wasmLoading: 'fetch', + // required for `integrity` to work in the browser + crossOriginLoading: 'anonymous', + // filenames for *initial* files (essentially JS entry points) + filename: '[name].[contenthash].js', + path: join(context, '..', 'dist'), + // Clean the output directory before emit, so that only the latest build + // files remain. Nearly 0 performance penalty for this clean up step. + clean: true, + // relative to HTML page. This value is essentially prepended to asset URLs + // in the output HTML, i.e., ` + diff --git a/offscreen/scripts/offscreen.ts b/offscreen/scripts/offscreen.ts index 654c99151c28..9f611333b581 100644 --- a/offscreen/scripts/offscreen.ts +++ b/offscreen/scripts/offscreen.ts @@ -1,6 +1,10 @@ import { BrowserRuntimePostMessageStream } from '@metamask/post-message-stream'; import { ProxySnapExecutor } from '@metamask/snaps-execution-environments'; -import { OffscreenCommunicationTarget } from '../../shared/constants/offscreen-communication'; +import { + OffscreenCommunicationEvents, + OffscreenCommunicationTarget, +} from '../../shared/constants/offscreen-communication'; + import initLedger from './ledger'; import initTrezor from './trezor'; import initLattice from './lattice'; @@ -22,7 +26,24 @@ const parentStream = new BrowserRuntimePostMessageStream({ ProxySnapExecutor.initialize(parentStream, './snaps/index.html'); +if (process.env.IN_TEST) { + chrome.runtime.onMessage.addListener((message) => { + if ( + message && + typeof message === 'object' && + message.event === OffscreenCommunicationEvents.metamaskBackgroundReady && + message.target === OffscreenCommunicationTarget.extension + ) { + window.document?.documentElement?.classList?.add('controller-loaded'); + } + }); +} + chrome.runtime.sendMessage({ target: OffscreenCommunicationTarget.extensionMain, isBooted: true, + + // This message is being sent from the Offscreen Document to the Service Worker. + // The Service Worker has no way to query `navigator.webdriver`, so we send it here. + webdriverPresent: navigator.webdriver === true, }); diff --git a/package.json b/package.json index 2b890b689cad..f9fafd91540d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "metamask-crx", - "version": "12.1.0", + "version": "12.4.0", "private": true, "repository": { "type": "git", "url": "https://github.com/MetaMask/metamask-extension.git" }, "scripts": { + "webpack": "tsx ./development/webpack/launch.ts", + "webpack:clearcache": "rimraf node_modules/.cache/webpack", + "postinstall": "yarn webpack:clearcache", "env:e2e": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' SENTRY_DSN_DEV=https://fake@sentry.io/0000000 yarn", "start": "yarn build:dev dev --apply-lavamoat=false --snow=false", "start:skip-onboarding": "SKIP_ONBOARDING=true yarn start", @@ -20,8 +23,8 @@ "dist:mmi:debug": "yarn dist --build-type mmi --apply-lavamoat=false --snow=false", "build": "yarn lavamoat:build", "build:dev": "node development/build/index.js", - "start:test": "yarn env:e2e build:dev testDev", - "start:test:flask": "yarn start:test --build-type flask", + "start:test": "yarn env:e2e build:dev testDev --apply-lavamoat=false", + "start:test:flask": "yarn start:test --build-type flask --apply-lavamoat=false", "start:test:mv2:flask": "ENABLE_MV3=false yarn start:test:flask --apply-lavamoat=false --snow=false", "start:test:mv2": "ENABLE_MV3=false BLOCKAID_FILE_CDN=static.cx.metamask.io/api/v1/confirmations/ppom yarn start:test --apply-lavamoat=false --snow=false", "benchmark:chrome": "SELENIUM_BROWSER=chrome ts-node test/e2e/benchmark.js", @@ -34,35 +37,37 @@ "build:test:flask:mv2": "ENABLE_MV3=false yarn build:test:flask", "build:test:mmi": "yarn build:test --build-type mmi", "build:test:mv2": "ENABLE_MV3=false BLOCKAID_FILE_CDN=static.cx.metamask.io/api/v1/confirmations/ppom yarn build:test", - "test": "yarn lint && yarn test:unit && yarn test:unit:jest", + "build:test:webpack": "BLOCKAID_FILE_CDN=static.cx.metamask.io/api/v1/confirmations/ppom yarn env:e2e webpack --test --browser=chrome --no-cache --lockdown --sentry --snow", + "test": "yarn lint && yarn test:unit", "dapp": "node development/static-server.js node_modules/@metamask/test-dapp/dist --port 8080", "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && yarn dapp'", "forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010", "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", - "test:unit": "node ./test/run-unit-tests.js --jestGlobal --jestDev", - "test:unit:jest": "node ./test/run-unit-tests.js --jestGlobal --jestDev", - "test:unit:jest:watch": "node --inspect ./node_modules/.bin/jest --watch", - "test:unit:global": "mocha test/unit-global/*.test.js", + "test:unit": "jest", + "test:unit:watch": "jest --watch", + "test:unit:coverage": "jest --coverage", + "test:unit:webpack": "tsx --test development/webpack/test/*.test.ts", + "test:unit:webpack:coverage": "nyc --reporter=html --reporter=json --reporter=text tsx --test development/webpack/test/*.test.ts", + "test:integration": "jest --config jest.integration.config.js", + "test:integration:coverage": "jest --config jest.integration.config.js --coverage", "test:e2e:chrome": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:mmi": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --mmi", "test:e2e:chrome:flask": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --build-type flask", + "test:e2e:chrome:webpack": "ENABLE_MV3=false SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test: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:ci": "yarn playwright test --project=mmi --project=mmi.visual", "test:e2e:mmi:all": "yarn playwright test --project=mmi && yarn test:e2e:mmi:visual", "test:e2e:mmi:regular": "yarn playwright test --project=mmi", - "test:e2e:mmi:visual": "./test/e2e/mmi/scripts/run-visual-test.sh check", - "test:e2e:mmi:visual:update": "./test/e2e/mmi/scripts/run-visual-test.sh update", + "test:e2e:mmi:visual": "./test/e2e/playwright/mmi/scripts/run-visual-test.sh check", + "test:e2e:mmi:visual:update": "./test/e2e/playwright/mmi/scripts/run-visual-test.sh update", + "test:e2e:swap": "yarn playwright test --project=swap", + "test:e2e:global": "yarn playwright test --project=global", "test:e2e:pw:report": "yarn playwright show-report public/playwright/playwright-reports/html", - "test:e2e:chrome:confirmation-redesign": "ENABLE_CONFIRMATION_REDESIGN=true SELENIUM_BROWSER=chrome node test/e2e/run-all.js", "test:e2e:chrome:rpc": "SELENIUM_BROWSER=chrome node test/e2e/run-all.js --rpc", "test:e2e:chrome:multi-provider": "MULTIPROVIDER=true SELENIUM_BROWSER=chrome node test/e2e/run-all.js --multi-provider", "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", "test:e2e:firefox:flask": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js --build-type flask", "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": "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", "lint": "yarn lint:prettier && yarn lint:eslint && yarn lint:tsc && yarn lint:styles", @@ -113,7 +118,6 @@ "generate-beta-commit": "node ./development/generate-beta-commit.js", "validate-branch-name": "validate-branch-name", "add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts", - "add-team-label-to-pr": "ts-node ./.github/scripts/add-team-label-to-pr.ts", "check-pr-has-required-labels": "ts-node ./.github/scripts/check-pr-has-required-labels.ts", "close-release-bug-report-issue": "ts-node ./.github/scripts/close-release-bug-report-issue.ts", "check-template-and-add-labels": "ts-node ./.github/scripts/check-template-and-add-labels.ts", @@ -223,8 +227,8 @@ "semver@7.3.8": "^7.5.4", "@trezor/schema-utils@npm:1.0.2": "patch:@trezor/schema-utils@npm%3A1.0.2#~/.yarn/patches/@trezor-schema-utils-npm-1.0.2-7dd48689b2.patch", "lavamoat-core@npm:^15.1.1": "patch:lavamoat-core@npm%3A15.1.1#~/.yarn/patches/lavamoat-core-npm-15.1.1-51fbe39988.patch", - "@metamask/snaps-sdk": "^6.0.0", - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A32.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-32.0.0-e23c2c3443.patch", + "@metamask/snaps-sdk": "^6.3.0", + "@swc/types@0.1.5": "^0.1.6", "@babel/runtime@npm:^7.7.6": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.9.2": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@babel/runtime@npm:^7.12.5": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", @@ -252,25 +256,25 @@ "sucrase@npm:3.34.0": "^3.35.0", "@expo/config/glob": "^10.3.10", "@expo/config-plugins/glob": "^10.3.10", - "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A19.0.0#~/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch", + "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A20.2.0#~/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch", "@solana/web3.js/rpc-websockets": "^8.0.1", - "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A19.0.0#~/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch", - "@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch", + "@metamask/network-controller@npm:^19.0.0": "patch:@metamask/network-controller@npm%3A20.2.0#~/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch", "@metamask/gas-fee-controller@npm:^15.1.1": "patch:@metamask/gas-fee-controller@npm%3A15.1.2#~/.yarn/patches/@metamask-gas-fee-controller-npm-15.1.2-db4d2976aa.patch", - "@metamask/snaps-utils@npm:^7.7.0": "patch:@metamask/snaps-utils@npm%3A7.7.0#~/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch", - "@metamask/snaps-utils@npm:^7.4.0": "patch:@metamask/snaps-utils@npm%3A7.7.0#~/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch", - "@metamask/snaps-utils@npm:^7.5.0": "patch:@metamask/snaps-utils@npm%3A7.7.0#~/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch", - "@metamask/keyring-controller@npm:^16.0.0": "patch:@metamask/keyring-controller@npm%3A16.1.0#~/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch", - "@metamask/keyring-controller@npm:^17.1.0": "patch:@metamask/keyring-controller@npm%3A16.1.0#~/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch" + "@metamask/snaps-controllers@npm:^8.1.1": "patch:@metamask/snaps-controllers@npm%3A9.5.0#~/.yarn/patches/@metamask-snaps-controllers-npm-9.5.0-8b21e3c072.patch", + "@metamask/snaps-controllers@npm:^9.5.0": "patch:@metamask/snaps-controllers@npm%3A9.5.0#~/.yarn/patches/@metamask-snaps-controllers-npm-9.5.0-8b21e3c072.patch", + "@metamask/nonce-tracker@npm:^5.0.0": "patch:@metamask/nonce-tracker@npm%3A5.0.0#~/.yarn/patches/@metamask-nonce-tracker-npm-5.0.0-d81478218e.patch", + "@metamask/keyring-controller@npm:^16.0.0": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", + "@metamask/keyring-controller@npm:^17.1.0": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", + "@trezor/connect-web@npm:^9.1.11": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch" }, "dependencies": { "@babel/runtime": "patch:@babel/runtime@npm%3A7.24.0#~/.yarn/patches/@babel-runtime-npm-7.24.0-7eb1dd11a2.patch", "@blockaid/ppom_release": "^1.5.2", - "@contentful/rich-text-html-renderer": "^16.3.5", "@ensdomains/content-hash": "^2.5.7", "@ethereumjs/tx": "^4.1.1", "@ethersproject/abi": "^5.6.4", "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/hash": "^5.7.0", "@ethersproject/hdnode": "^5.6.2", @@ -290,21 +294,24 @@ "@metamask-institutional/rpc-allowlist": "^1.0.3", "@metamask-institutional/sdk": "^0.1.30", "@metamask-institutional/transaction-update": "^0.2.5", + "@metamask-institutional/types": "^1.1.0", "@metamask/abi-utils": "^2.0.2", - "@metamask/accounts-controller": "^17.0.0", + "@metamask/account-watcher": "^4.1.0", + "@metamask/accounts-controller": "^18.1.0", "@metamask/address-book-controller": "^4.0.1", "@metamask/announcement-controller": "^6.1.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A33.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-33.0.0-3e7448c4cd.patch", + "@metamask/assets-controllers": "^36.0.0", "@metamask/base-controller": "^5.0.1", + "@metamask/bitcoin-wallet-snap": "^0.5.0", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", - "@metamask/controller-utils": "^10.0.0", + "@metamask/controller-utils": "^11.1.0", "@metamask/design-tokens": "^4.0.0", - "@metamask/ens-controller": "^10.0.1", + "@metamask/ens-controller": "^13.0.0", "@metamask/eth-json-rpc-filters": "^7.0.0", - "@metamask/eth-json-rpc-middleware": "^12.1.1", - "@metamask/eth-ledger-bridge-keyring": "^2.0.1", + "@metamask/eth-json-rpc-middleware": "^14.0.0", + "@metamask/eth-ledger-bridge-keyring": "patch:@metamask/eth-ledger-bridge-keyring@npm%3A2.0.1#~/.yarn/patches/@metamask-eth-ledger-bridge-keyring-npm-2.0.1-7a5d815b2d.patch", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^7.0.1", "@metamask/eth-snap-keyring": "^4.3.1", @@ -314,25 +321,27 @@ "@metamask/ethjs": "^0.6.0", "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", - "@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/gas-fee-controller": "^18.0.0", "@metamask/jazzicon": "^2.0.0", "@metamask/keyring-api": "^8.0.0", - "@metamask/keyring-controller": "patch:@metamask/keyring-controller@npm%3A16.1.0#~/.yarn/patches/@metamask-keyring-controller-npm-16.1.0-7043d2dc62.patch", - "@metamask/logging-controller": "^3.0.1", + "@metamask/keyring-controller": "patch:@metamask/keyring-controller@npm%3A17.1.1#~/.yarn/patches/@metamask-keyring-controller-npm-17.1.1-098cb41930.patch", + "@metamask/logging-controller": "^5.0.0", "@metamask/logo": "^3.1.2", "@metamask/message-manager": "^7.3.0", "@metamask/message-signing-snap": "^0.3.3", "@metamask/metamask-eth-abis": "^3.1.1", "@metamask/name-controller": "^8.0.0", - "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A19.0.0#~/.yarn/patches/@metamask-network-controller-npm-19.0.0-a5e0d1fe14.patch", + "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A20.2.0#~/.yarn/patches/@metamask-network-controller-npm-20.2.0-98b1a5ae59.patch", "@metamask/notification-controller": "^5.0.1", + "@metamask/notification-services-controller": "^0.2.1", "@metamask/object-multiplex": "^2.0.0", "@metamask/obs-store": "^9.0.0", "@metamask/permission-controller": "^10.0.0", "@metamask/permission-log-controller": "^2.0.1", - "@metamask/phishing-controller": "^9.0.3", + "@metamask/phishing-controller": "^12.0.1", "@metamask/post-message-stream": "^8.0.0", - "@metamask/ppom-validator": "^0.31.0", + "@metamask/ppom-validator": "patch:@metamask/ppom-validator@npm%3A0.32.0#~/.yarn/patches/@metamask-ppom-validator-npm-0.32.0-f677deea54.patch", + "@metamask/profile-sync-controller": "^0.2.1", "@metamask/providers": "^14.0.2", "@metamask/queued-request-controller": "^2.0.0", "@metamask/rate-limit-controller": "^5.0.1", @@ -340,18 +349,17 @@ "@metamask/safe-event-emitter": "^3.1.1", "@metamask/scure-bip39": "^2.0.3", "@metamask/selected-network-controller": "^15.0.2", - "@metamask/signature-controller": "^16.0.0", - "@metamask/smart-transactions-controller": "^10.1.2", - "@metamask/snaps-controllers": "^9.2.0", - "@metamask/snaps-execution-environments": "^6.5.0", - "@metamask/snaps-rpc-methods": "^9.1.4", - "@metamask/snaps-sdk": "^6.0.0", - "@metamask/snaps-utils": "patch:@metamask/snaps-utils@npm%3A7.7.0#~/.yarn/patches/@metamask-snaps-utils-npm-7.7.0-2cc1f044af.patch", - "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A32.0.0#~/.yarn/patches/@metamask-transaction-controller-npm-32.0.0-e23c2c3443.patch", - "@metamask/user-operation-controller": "^10.0.0", + "@metamask/signature-controller": "^18.1.0", + "@metamask/smart-transactions-controller": "12.0.1", + "@metamask/snaps-controllers": "^9.5.0", + "@metamask/snaps-execution-environments": "^6.6.2", + "@metamask/snaps-rpc-methods": "^11.0.0", + "@metamask/snaps-sdk": "^6.3.0", + "@metamask/snaps-utils": "^8.0.1", + "@metamask/transaction-controller": "^35.2.0", + "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^8.2.1", "@ngraveio/bc-ur": "^1.1.12", - "@noble/ciphers": "^0.5.2", "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.4.0", "@reduxjs/toolkit": "patch:@reduxjs/toolkit@npm%3A1.9.7#~/.yarn/patches/@reduxjs-toolkit-npm-1.9.7-b14925495c.patch", @@ -359,7 +367,8 @@ "@sentry/browser": "^8.19.0", "@sentry/types": "^8.19.0", "@sentry/utils": "^8.19.0", - "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.2.2#~/.yarn/patches/@trezor-connect-web-npm-9.2.2-a4de8e45fc.patch", + "@swc/core": "1.4.11", + "@trezor/connect-web": "patch:@trezor/connect-web@npm%3A9.3.0#~/.yarn/patches/@trezor-connect-web-npm-9.3.0-040ab10d9a.patch", "@zxing/browser": "^0.1.4", "@zxing/library": "0.20.0", "await-semaphore": "^0.1.1", @@ -383,7 +392,6 @@ "ethereumjs-util": "^7.0.10", "extension-port-stream": "^3.0.0", "fast-json-patch": "^3.1.1", - "firebase": "^10.11.0", "fuse.js": "^3.2.0", "he": "^1.2.0", "human-standard-token-abi": "^2.0.0", @@ -450,6 +458,7 @@ "@lavamoat/lavadome-core": "0.0.10", "@lavamoat/lavapack": "^6.1.0", "@lgbot/madge": "^6.2.0", + "@lydell/node-pty": "^1.0.1", "@metamask/api-specs": "^0.9.3", "@metamask/auto-changelog": "^2.1.0", "@metamask/build-utils": "^1.0.0", @@ -469,24 +478,26 @@ "@open-rpc/schema-utils-js": "^1.16.2", "@open-rpc/test-coverage": "^2.2.2", "@playwright/test": "^1.39.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@sentry/cli": "^2.19.4", - "@storybook/addon-a11y": "^7.6.19", - "@storybook/addon-actions": "^7.6.19", + "@storybook/addon-a11y": "^7.6.20", + "@storybook/addon-actions": "^7.6.20", "@storybook/addon-designs": "^7.0.9", - "@storybook/addon-docs": "^7.6.19", - "@storybook/addon-essentials": "^7.6.19", + "@storybook/addon-docs": "^7.6.20", + "@storybook/addon-essentials": "^7.6.20", "@storybook/addon-knobs": "^7.0.2", - "@storybook/addon-mdx-gfm": "^7.6.19", - "@storybook/addons": "^7.6.19", - "@storybook/api": "^7.6.19", - "@storybook/client-api": "^7.6.19", - "@storybook/components": "^7.6.19", - "@storybook/core-events": "^7.6.19", - "@storybook/react": "^7.6.19", - "@storybook/react-webpack5": "^7.6.19", + "@storybook/addon-mdx-gfm": "^7.6.20", + "@storybook/addons": "^7.6.20", + "@storybook/api": "^7.6.20", + "@storybook/client-api": "^7.6.20", + "@storybook/components": "^7.6.20", + "@storybook/core-events": "^7.6.20", + "@storybook/react": "^7.6.20", + "@storybook/react-webpack5": "^7.6.20", "@storybook/storybook-deployer": "^2.8.16", "@storybook/test-runner": "^0.14.1", - "@storybook/theming": "^7.6.19", + "@storybook/theming": "^7.6.20", + "@swc/helpers": "^0.5.7", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^10.4.8", "@testing-library/react-hooks": "^8.0.1", @@ -494,6 +505,7 @@ "@tsconfig/node20": "^20.1.2", "@types/babelify": "^7.3.7", "@types/browserify": "^12.0.37", + "@types/chrome": "^0.0.268", "@types/currency-formatter": "^1.5.1", "@types/fs-extra": "^9.0.13", "@types/gulp": "^4.0.9", @@ -505,6 +517,7 @@ "@types/luxon": "^3.4.2", "@types/mocha": "^10.0.3", "@types/node": "^20", + "@types/path-browserify": "^1.0.2", "@types/pify": "^5.0.1", "@types/react": "^16.9.53", "@types/react-beautiful-dnd": "^13", @@ -522,9 +535,12 @@ "@types/w3c-web-hid": "^1.0.3", "@types/watchify": "^3.11.1", "@types/webextension-polyfill": "^0.10.4", + "@types/ws": "^8.5.10", "@types/yargs": "^17.0.32", + "@types/yargs-parser": "^21.0.3", "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", + "@welldone-software/why-did-you-render": "^8.0.3", "@whitespace/storybook-addon-html": "^5.1.6", "addons-linter": "^6.28.0", "autoprefixer": "^10.4.19", @@ -532,12 +548,15 @@ "babelify": "^10.0.0", "bify-module-groups": "^2.0.0", "browserify": "^17.0.0", + "browserslist": "^4.23.0", + "buffer": "^6.0.3", "chalk": "^4.1.2", "chokidar": "^3.6.0", "concurrently": "^8.2.2", - "contentful": "^10.8.7", "copy-webpack-plugin": "^12.0.2", + "core-js-pure": "^3.38.0", "cross-spawn": "^7.0.3", + "crypto-browserify": "^3.12.0", "css-loader": "^6.10.0", "css-to-xpath": "^0.1.0", "csstype": "^3.0.11", @@ -564,6 +583,7 @@ "fake-indexeddb": "^4.0.1", "fancy-log": "^1.3.3", "fast-glob": "^3.2.2", + "fflate": "^0.8.2", "fs-extra": "^8.1.0", "ganache": "patch:ganache@npm%3A7.9.2#~/.yarn/patches/ganache-npm-7.9.2-a70dc8da34.patch", "gh-pages": "^5.0.0", @@ -579,12 +599,15 @@ "gulp-watch": "^5.0.1", "gulp-zip": "^5.1.0", "history": "^5.0.0", + "html-bundler-webpack-plugin": "^3.17.3", + "https-browserify": "^1.0.0", "husky": "^8.0.3", "ini": "^3.0.0", "jest": "^29.7.0", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.7.0", "jsdom": "^16.7.0", + "json-schema-to-ts": "^3.0.1", "koa": "^2.7.0", "lavamoat": "^8.0.2", "lavamoat-browserify": "^17.0.4", @@ -596,11 +619,15 @@ "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", + "path-browserify": "^1.0.1", "postcss": "^8.4.32", + "postcss-loader": "^8.1.1", "postcss-rtlcss": "^4.0.9", "prettier": "^2.7.1", - "prettier-eslint": "^16.3.0", + "prettier-eslint": "^15.0.1", "prettier-plugin-sort-json": "^1.0.0", + "process": "^0.11.10", "pumpify": "^2.0.1", "randomcolor": "^0.5.4", "react-devtools": "^4.11.0", @@ -611,8 +638,10 @@ "redux-mock-store": "^1.5.4", "remote-redux-devtools": "^0.5.16", "resolve-url-loader": "^3.1.5", + "rimraf": "^5.0.5", "sass-embedded": "^1.71.0", - "sass-loader": "^10.1.1", + "sass-loader": "^14.1.1", + "schema-utils": "^4.2.0", "selenium-webdriver": "^4.15.0", "semver": "^7.5.4", "serve-handler": "^6.1.2", @@ -620,14 +649,16 @@ "source-map": "^0.7.4", "source-map-explorer": "^2.4.2", "sprintf-js": "^1.1.3", - "storybook": "^7.6.19", - "storybook-dark-mode": "^3.0.3", + "storybook": "^7.6.20", + "storybook-dark-mode": "^4.0.2", "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", "string.prototype.matchall": "^4.0.2", "style-loader": "^0.21.0", "stylelint": "^13.6.1", "superstruct": "^1.0.3", "terser": "^5.7.0", + "terser-webpack-plugin": "^5.3.10", "through2": "^4.0.2", "ts-node": "^10.9.2", "tsx": "^4.7.1", @@ -641,12 +672,15 @@ "watchify": "^4.0.0", "webextension-polyfill": "^0.8.0", "webpack": "^5.91.0", + "webpack-dev-server": "^5.0.3", + "ws": "^8.17.1", "yaml": "^2.4.1", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" }, "engines": { "node": ">= 20", - "yarn": "^4.0.2" + "yarn": "^4.2.2" }, "lavamoat": { "allowScripts": { @@ -697,6 +731,7 @@ "@storybook/test-runner>playwright": true, "@storybook/addon-knobs>core-js": false, "@storybook/manager-webpack5>core-js": false, + "@storybook/react-webpack5>@storybook/builder-webpack5>@swc/core": false, "@storybook/react-webpack5>@storybook/preset-react-webpack>@pmmmwh/react-refresh-webpack-plugin>core-js-pure": false, "ts-node>@swc/core": false, "jsdom>ws>bufferutil": false, @@ -708,10 +743,19 @@ "@trezor/connect-web>@trezor/connect>@trezor/utxo-lib>tiny-secp256k1": false, "@storybook/test-runner>@swc/core": false, "@lavamoat/lavadome-react>@lavamoat/preinstall-always-fail": false, - "tsx>esbuild": false, + "ws>bufferutil": false, + "ws>utf-8-validate": false, "@metamask/eth-trezor-keyring>@trezor/connect-web>@trezor/connect>@trezor/protobuf>protobufjs": false, "firebase>@firebase/firestore>@grpc/proto-loader>protobufjs": false, - "@lavamoat/lavadome-core>@lavamoat/preinstall-always-fail": false + "@lavamoat/lavadome-core>@lavamoat/preinstall-always-fail": false, + "tsx>esbuild": true, + "@pmmmwh/react-refresh-webpack-plugin>core-js-pure": true, + "@swc/core": true, + "@lydell/node-pty": true, + "$root$": true, + "core-js-pure": true, + "resolve-url-loader>es6-iterator>d>es5-ext": false, + "resolve-url-loader>es6-iterator>d>es5-ext>esniff>es5-ext": false } }, "packageManager": "yarn@4.2.2" diff --git a/playwright.config.ts b/playwright.config.ts index 6937e6faf8b6..6632e1983e79 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,16 +3,16 @@ import { devices } from '@playwright/test'; import dotenv from 'dotenv'; import { isHeadless } from './test/helpers/env'; -dotenv.config({ path: './test/e2e/mmi/.env' }); +dotenv.config({ path: './test/e2e/playwright/mmi/.env' }); const logOutputFolder = './public/playwright/playwright-reports'; /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { - testDir: 'test/e2e/mmi/specs', + testDir: 'test/e2e/playwright', /* Maximum time one test can run for. */ - timeout: 70 * 1000, + timeout: 300 * 1000, expect: { timeout: 30 * 1000, }, @@ -51,19 +51,36 @@ const config: PlaywrightTestConfig = { projects: [ { name: 'mmi', - testMatch: '**/*.spec.ts', - testIgnore: '**/*visual.spec.ts', + testMatch: '/mmi/specs/**.spec.ts', + testIgnore: '/mmi/specs/visual.spec.ts', use: { ...devices['Desktop Chrome'], }, }, { name: 'mmi.visual', - testMatch: '**/*visual.spec.ts', + testMatch: '/mmi/**/*visual.spec.ts', use: { ...devices['Desktop Chrome'], }, }, + { + name: 'swap', + testMatch: '/swap/specs/*swap.spec.ts', + use: { + ...devices['Desktop Chrome'], + headless: true, + }, + }, + // Global: universal, common, shared, and non feature related tests + { + name: 'global', + testMatch: '/global/specs/**.spec.ts', + use: { + ...devices['Desktop Chrome'], + headless: true, + }, + }, ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ diff --git a/privacy-snapshot.json b/privacy-snapshot.json index fe6579bfab73..3b4ded67481c 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -1,11 +1,14 @@ [ "acl.execution.metamask.io", + "api.blockchair.com", "api.lens.dev", "api.segment.io", "api.web3modal.com", + "app.ens.domains", "arbitrum-mainnet.infura.io", "bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link", "bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link", + "bridge.api.cx.metamask.io", "cdn.segment.com", "cdn.segment.io", "cdnjs.cloudflare.com", @@ -24,6 +27,7 @@ "localhost:8000", "localhost:8545", "mainnet.infura.io", + "metamask.eth", "metamask.github.io", "min-api.cryptocompare.com", "nft.api.cx.metamask.io", @@ -38,6 +42,7 @@ "responsive-rpc.test", "sentry.io", "snaps.metamask.io", + "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", "swap.api.cx.metamask.io", @@ -52,5 +57,6 @@ "authentication.api.cx.metamask.io", "oidc.api.cx.metamask.io", "price.api.cx.metamask.io", - "token.api.cx.metamask.io" + "token.api.cx.metamask.io", + "client-side-detection.api.cx.metamask.io" ] diff --git a/shared/constants/app.ts b/shared/constants/app.ts index a38ac274d301..70a393626b58 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -1,4 +1,4 @@ -import { DialogType } from '@metamask/snaps-sdk'; +import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import { RestrictedMethods } from './permissions'; /** @@ -34,7 +34,6 @@ export const MESSAGE_TYPE = { ETH_REQUEST_ACCOUNTS: 'eth_requestAccounts', ETH_SEND_TRANSACTION: 'eth_sendTransaction', ETH_SEND_RAW_TRANSACTION: 'eth_sendRawTransaction', - ETH_SIGN: 'eth_sign', ETH_SIGN_TRANSACTION: 'eth_signTransaction', ETH_SIGN_TYPED_DATA: 'eth_signTypedData', ETH_SIGN_TYPED_DATA_V1: 'eth_signTypedData_v1', @@ -49,9 +48,10 @@ export const MESSAGE_TYPE = { WALLET_REQUEST_PERMISSIONS: 'wallet_requestPermissions', WATCH_ASSET: 'wallet_watchAsset', WATCH_ASSET_LEGACY: 'metamask_watchAsset', - SNAP_DIALOG_ALERT: `${RestrictedMethods.snap_dialog}:alert`, - SNAP_DIALOG_CONFIRMATION: `${RestrictedMethods.snap_dialog}:confirmation`, - SNAP_DIALOG_PROMPT: `${RestrictedMethods.snap_dialog}:prompt`, + SNAP_DIALOG_ALERT: DIALOG_APPROVAL_TYPES.alert, + SNAP_DIALOG_CONFIRMATION: DIALOG_APPROVAL_TYPES.confirmation, + SNAP_DIALOG_PROMPT: DIALOG_APPROVAL_TYPES.prompt, + SNAP_DIALOG_DEFAULT: DIALOG_APPROVAL_TYPES.default, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) MMI_AUTHENTICATE: 'metamaskinstitutional_authenticate', MMI_REAUTHENTICATE: 'metamaskinstitutional_reauthenticate', @@ -65,17 +65,12 @@ export const MESSAGE_TYPE = { ///: END:ONLY_INCLUDE_IF } as const; -export const SNAP_DIALOG_TYPES = { - [DialogType.Alert]: MESSAGE_TYPE.SNAP_DIALOG_ALERT, - [DialogType.Confirmation]: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION, - [DialogType.Prompt]: MESSAGE_TYPE.SNAP_DIALOG_PROMPT, -}; - ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = { confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation', confirmAccountRemoval: 'snap_manageAccounts:confirmAccountRemoval', - showSnapAccountRedirect: 'showSnapAccountRedirect', + showSnapAccountRedirect: 'snap_manageAccounts:showSnapAccountRedirect', + showNameSnapAccount: 'snap_manageAccounts:showNameSnapAccount', }; ///: END:ONLY_INCLUDE_IF diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index e02c992bbbba..e87b0689777f 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,5 +1,6 @@ import { CHAIN_IDS } from './network'; +// TODO read from feature flags export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MAINNET, CHAIN_IDS.BSC, @@ -11,3 +12,11 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, ]; + +export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; +export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; +export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS + ? BRIDGE_DEV_API_BASE_URL + : BRIDGE_PROD_API_BASE_URL; + +export const BRIDGE_CLIENT_ID = 'extension'; diff --git a/shared/constants/custody.ts b/shared/constants/custody.ts new file mode 100644 index 000000000000..112b59c9e011 --- /dev/null +++ b/shared/constants/custody.ts @@ -0,0 +1,8 @@ +export enum AccountType { + CUSTODY = 'custody', +} + +export enum CustodyStatus { + CREATED = 'created', + ABORTED = 'aborted', +} diff --git a/shared/constants/eth-methods.ts b/shared/constants/eth-methods.ts index 3eea6c662594..8aa47f1bcd00 100644 --- a/shared/constants/eth-methods.ts +++ b/shared/constants/eth-methods.ts @@ -2,7 +2,6 @@ import { EthMethod } from '@metamask/keyring-api'; export const ETH_EOA_METHODS = [ EthMethod.PersonalSign, - EthMethod.Sign, EthMethod.SignTransaction, EthMethod.SignTypedDataV1, EthMethod.SignTypedDataV3, diff --git a/shared/constants/hardware-wallets.ts b/shared/constants/hardware-wallets.ts index a5decda5ac32..96e50ed7c17e 100644 --- a/shared/constants/hardware-wallets.ts +++ b/shared/constants/hardware-wallets.ts @@ -37,6 +37,7 @@ export enum HardwareAffiliateLinks { airgap = 'https://airgap.it/', coolwallet = 'https://www.coolwallet.io/', dcent = 'https://dcentwallet.com/', + ngrave = 'https://shop.ngrave.io/', imtoken = 'https://token.im/', onekey = 'https://onekey.so/products/onekey-pro-hardware-wallet/', } @@ -49,6 +50,7 @@ export enum HardwareAffiliateTutorialLinks { airgap = 'https://support.airgap.it/guides/metamask/', coolwallet = 'https://www.coolwallet.io/metamask-step-by-step-guides/', dcent = 'https://medium.com/dcentwallet/dcent-wallet-now-supports-qr-based-protocol-to-link-with-metamask-57555f02603f', + ngrave = 'https://ngrave.io/zero', imtoken = 'https://support.token.im/hc/en-us/articles/24652624775961/', onekey = 'https://help.onekey.so/hc/articles/9426592069903', } diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index fcb08858be8a..7cbbefe23e64 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -512,6 +512,8 @@ export enum MetaMetricsEventName { AppLocked = 'App Locked', AppWindowExpanded = 'App Window Expanded', BridgeLinkClicked = 'Bridge Link Clicked', + BitcoinSupportToggled = 'Bitcoin Support Toggled', + BitcoinTestnetSupportToggled = 'Bitcoin Testnet Support Toggled', DappViewed = 'Dapp Viewed', DecryptionApproved = 'Decryption Approved', DecryptionRejected = 'Decryption Rejected', @@ -568,7 +570,6 @@ export enum MetaMetricsEventName { OnboardingWalletCreationComplete = 'Wallet Created', OnboardingWalletSetupComplete = 'Application Opened', OnboardingWalletAdvancedSettings = 'Settings Updated', - OnboardingWalletAdvancedSettingsTurnOnProfileSyncing = 'Turn On Profile Syncing', OnboardingWalletImportAttempted = 'Wallet Import Attempted', OnboardingWalletVideoPlay = 'SRP Intro Video Played', OnboardingTwitterClick = 'External Link Clicked', @@ -626,6 +627,7 @@ export enum MetaMetricsEventName { WalletSetupCanceled = 'Wallet Setup Canceled', WalletSetupFailed = 'Wallet Setup Failed', WalletCreated = 'Wallet Created', + WatchEthereumAccountsToggled = 'Watch Ethereum Accounts Toggled', ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) DeeplinkClicked = 'Deeplink Clicked', ConnectCustodialAccountClicked = 'Connect Custodial Account Clicked', @@ -667,6 +669,7 @@ export enum MetaMetricsEventName { TransactionApproved = 'Transaction Approved', SwapCompleted = 'Swap Completed', TransactionFinalized = 'Transaction Finalized', + ConfirmationQueued = 'Confirmation Queued', ExitedSwaps = 'Exited Swaps', SwapError = 'Swap Error', SnapInstallStarted = 'Snap Install Started', @@ -697,8 +700,6 @@ export enum MetaMetricsEventName { SnapAccountTransactionFinalizeRedirectGoToSiteClicked = 'Snap Account Transaction Finalize Redirect "Go To Site" Clicked', SnapAccountTransactionFinalizeRedirectSnapUrlClicked = 'Snap Account Transaction Finalize Redirect "Snap URL" Clicked', SnapAccountTransactionFinalizeClosed = 'Snap Account Transaction Finalize Closed', - TokenAutoDetectionEnableModal = 'Token Autodetection Enabled from modal', - TokenAutoDetectionDisableModal = 'Token Autodetection Disabled from modal', ///: END:ONLY_INCLUDE_IF TurnOnMetaMetrics = 'MetaMetrics Turned On', TurnOffMetaMetrics = 'MetaMetrics Turned Off', @@ -708,8 +709,6 @@ export enum MetaMetricsEventName { NotificationClicked = 'Notification Clicked', NotificationMenuOpened = 'Notification Menu Opened', - NftAutoDetectionEnableModal = 'Nft Autodetection Enabled from modal', - NftAutoDetectionDisableModal = 'Nft Autodetection Disabled from modal', // Send sendAssetSelected = 'Send Asset Selected', sendFlowExited = 'Send Flow Exited', @@ -728,6 +727,11 @@ export enum MetaMetricsEventAccountType { ///: END:ONLY_INCLUDE_IF } +export enum QueueType { + NavigationHeader = 'navigation_header', + QueueController = 'queue_controller', +} + export enum MetaMetricsEventAccountImportType { Json = 'json', PrivateKey = 'private_key', @@ -812,6 +816,8 @@ export enum MetaMetricsTransactionEventSource { } export enum MetaMetricsEventLocation { + AlertFrictionModal = 'alert_friction_modal', + Confirmation = 'confirmation', SignatureConfirmation = 'signature_confirmation', TokenDetails = 'token_details', TokenDetection = 'token_detection', @@ -824,6 +830,7 @@ export enum MetaMetricsEventUiCustomization { FlaggedAsSafetyUnknown = 'flagged_as_safety_unknown', FlaggedAsWarning = 'flagged_as_warning', GasEstimationFailed = 'gas_estimation_failed', + Order = 'order', RedesignedConfirmation = 'redesigned_confirmation', SecurityAlertError = 'security_alert_error', Siwe = 'sign_in_with_ethereum', diff --git a/shared/constants/methods-tags.ts b/shared/constants/methods-tags.ts index 89cce5f67c8d..651109dcedcf 100644 --- a/shared/constants/methods-tags.ts +++ b/shared/constants/methods-tags.ts @@ -28,7 +28,6 @@ export const methodsWithConfirmation = [ 'wallet_requestPermissions', 'wallet_requestSnaps', 'eth_decrypt', - 'eth_sign', 'eth_requestAccounts', 'eth_getEncryptionPublicKey', ]; diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index 7f629ca49c6b..5217394a5415 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -1,9 +1,13 @@ -import { ProviderConfig } from '@metamask/network-controller'; import { CaipChainId } from '@metamask/utils'; import { isBtcMainnetAddress, isBtcTestnetAddress } from '../../lib/multichain'; -export type ProviderConfigWithImageUrl = Omit & { - rpcPrefs?: { imageUrl?: string }; +export type ProviderConfigWithImageUrl = { + rpcUrl?: string; + type: string; + ticker: string; + nickname?: string; + rpcPrefs?: { blockExplorerUrl?: string; imageUrl?: string }; + id?: string; }; export type MultichainProviderConfig = ProviderConfigWithImageUrl & { @@ -21,6 +25,12 @@ export enum MultichainNetworks { export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg'; +export const MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP = { + [MultichainNetworks.BITCOIN]: 'https://blockstream.info/address', + [MultichainNetworks.BITCOIN_TESTNET]: + 'https://blockstream.info/testnet/address', +} as const; + export const MULTICHAIN_TOKEN_IMAGE_MAP = { [MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL, } as const; @@ -38,6 +48,8 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< type: 'rpc', rpcPrefs: { imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[MultichainNetworks.BITCOIN], }, isAddressCompatible: isBtcMainnetAddress, }, @@ -50,6 +62,10 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< type: 'rpc', rpcPrefs: { imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], + blockExplorerUrl: + MULTICHAIN_NETWORK_BLOCK_EXPLORER_URL_MAP[ + MultichainNetworks.BITCOIN_TESTNET + ], }, isAddressCompatible: isBtcTestnetAddress, }, diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 955c9b2decc2..de8429d8ba6d 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -1,7 +1,6 @@ import { capitalize, pick } from 'lodash'; /** - * A type representing any valid value for 'type' for setProviderType and other - * methods that add or manipulate networks in MetaMask state. + * A type representing built-in network types, used as an identifier. */ export type NetworkType = (typeof NETWORK_TYPES)[keyof typeof NETWORK_TYPES]; @@ -142,6 +141,10 @@ export const CHAIN_IDS = { CHZ: '0x15b38', NUMBERS: '0x290b', SEI: '0x531', + BERACHAIN: '0x138d5', + METACHAIN_ONE: '0x1b6e6', + NEAR: '0x18d', + NEAR_TESTNET: '0x18e', } as const; export const CHAINLIST_CHAIN_IDS_MAP = { @@ -172,6 +175,7 @@ export const CHAINLIST_CHAIN_IDS_MAP = { FLARE_MAINNET: '0xe', FUSE_GOLD_MAINNET: '0x7a', HAQQ_NETWORK: '0x2be3', + IOTEX_MAINNET: '0x1251', KCC_MAINNET: '0x141', KLAYTN_MAINNET_CYPRESS: '0x2019', KROMA_MAINNET: '0xff', @@ -243,6 +247,8 @@ export const MOONRIVER_DISPLAY_NAME = 'Moonriver'; export const SCROLL_DISPLAY_NAME = 'Scroll'; export const SCROLL_SEPOLIA_DISPLAY_NAME = 'Scroll Sepolia'; export const OP_BNB_DISPLAY_NAME = 'opBNB'; +export const BERACHAIN_DISPLAY_NAME = 'Berachain Artio'; +export const METACHAIN_ONE_DISPLAY_NAME = 'Metachain One Mainnet'; export const infuraProjectId = process.env.INFURA_PROJECT_ID; export const getRpcUrl = ({ @@ -289,6 +295,7 @@ export const CURRENCY_SYMBOLS = { HARMONY: 'ONE', PALM: 'PALM', MATIC: 'MATIC', + POL: 'POL', TEST_ETH: 'TESTETH', USDC: 'USDC', USDT: 'USDT', @@ -356,11 +363,13 @@ const CHAINLIST_CURRENCY_SYMBOLS_MAP = { OASYS_MAINNET: 'OAS', HUOBI_ECO_CHAIN_MAINNET: 'HT', ACALA_NETWORK: 'ACA', + IOTEX_MAINNET: 'IOTX', } as const; export const CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION = { WETHIO: 'ZYN', CHZ: 'CHZ', + MATIC: 'POL', }; export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg'; @@ -369,7 +378,7 @@ export const LINEA_SEPOLIA_TOKEN_IMAGE_URL = './images/linea-logo-testnet.png'; export const LINEA_MAINNET_TOKEN_IMAGE_URL = './images/linea-logo-mainnet.svg'; export const TEST_ETH_TOKEN_IMAGE_URL = './images/black-eth-logo.svg'; export const BNB_TOKEN_IMAGE_URL = './images/bnb.svg'; -export const MATIC_TOKEN_IMAGE_URL = './images/matic-token.svg'; +export const POL_TOKEN_IMAGE_URL = './images/pol-token.svg'; export const AVAX_TOKEN_IMAGE_URL = './images/avax-token.svg'; export const AETH_TOKEN_IMAGE_URL = './images/arbitrum.svg'; export const FTM_TOKEN_IMAGE_URL = './images/fantom-opera.svg'; @@ -399,6 +408,8 @@ export const EVMOS_IMAGE_URL = './images/evmos.svg'; export const FLARE_MAINNET_IMAGE_URL = './images/flare-mainnet.svg'; export const FUSE_GOLD_MAINNET_IMAGE_URL = './images/fuse-mainnet.jpg'; export const HAQQ_NETWORK_IMAGE_URL = './images/haqq.svg'; +export const IOTEX_MAINNET_IMAGE_URL = './images/iotex.svg'; +export const IOTEX_TOKEN_IMAGE_URL = './images/iotex-token.svg'; export const KCC_MAINNET_IMAGE_URL = './images/kcc-mainnet.svg'; export const KLAYTN_MAINNET_IMAGE_URL = './images/klaytn.svg'; export const KROMA_MAINNET_IMAGE_URL = './images/kroma.svg'; @@ -407,6 +418,8 @@ export const MANTA_PACIFIC_MAINNET_IMAGE_URL = './images/manta.svg'; export const MANTLE_MAINNET_IMAGE_URL = './images/mantle.svg'; export const MOONBEAM_IMAGE_URL = './images/moonbeam.svg'; export const MOONRIVER_IMAGE_URL = './images/moonriver.svg'; +export const MOONBEAM_TOKEN_IMAGE_URL = './images/moonbeam-token.svg'; +export const MOONRIVER_TOKEN_IMAGE_URL = './images/moonriver-token.svg'; export const NEAR_AURORA_MAINNET_IMAGE_URL = './images/near-aurora.svg'; export const NEBULA_MAINNET_IMAGE_URL = './images/nebula.svg'; export const OASYS_MAINNET_IMAGE_URL = './images/oasys.svg'; @@ -429,6 +442,7 @@ export const SCROLL_IMAGE_URL = './images/scroll.svg'; export const NUMBERS_MAINNET_IMAGE_URL = './images/numbers-mainnet.svg'; export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png'; export const SEI_IMAGE_URL = './images/sei.svg'; +export const NEAR_IMAGE_URL = './images/near.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -484,10 +498,12 @@ export const BUILT_IN_NETWORKS = { [NETWORK_TYPES.MAINNET]: { chainId: CHAIN_IDS.MAINNET, blockExplorerUrl: `https://etherscan.io`, + ticker: CURRENCY_SYMBOLS.ETH, }, [NETWORK_TYPES.LINEA_MAINNET]: { chainId: CHAIN_IDS.LINEA_MAINNET, blockExplorerUrl: 'https://lineascan.build', + ticker: CURRENCY_SYMBOLS.ETH, }, [NETWORK_TYPES.LOCALHOST]: { chainId: CHAIN_IDS.LOCALHOST, @@ -534,6 +550,9 @@ export const NETWORK_TO_NAME_MAP = { [CHAIN_IDS.SCROLL_SEPOLIA]: SCROLL_SEPOLIA_DISPLAY_NAME, [CHAIN_IDS.SEPOLIA]: SEPOLIA_DISPLAY_NAME, [CHAIN_IDS.OPBNB]: OP_BNB_DISPLAY_NAME, + [CHAIN_IDS.ZKSYNC_ERA]: ZK_SYNC_ERA_DISPLAY_NAME, + [CHAIN_IDS.BERACHAIN]: BERACHAIN_DISPLAY_NAME, + [CHAIN_IDS.METACHAIN_ONE]: METACHAIN_ONE_DISPLAY_NAME, } as const; export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { @@ -546,7 +565,7 @@ export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.MAINNET]: CHAINLIST_CURRENCY_SYMBOLS_MAP.ETH, [CHAINLIST_CHAIN_IDS_MAP.OPBNB]: CHAINLIST_CURRENCY_SYMBOLS_MAP.OPBNB, [CHAINLIST_CHAIN_IDS_MAP.OPTIMISM]: CHAINLIST_CURRENCY_SYMBOLS_MAP.OPTIMISM, - [CHAINLIST_CHAIN_IDS_MAP.POLYGON]: CHAINLIST_CURRENCY_SYMBOLS_MAP.MATIC, + [CHAINLIST_CHAIN_IDS_MAP.POLYGON]: CHAINLIST_CURRENCY_SYMBOLS_MAP.POL, [CHAINLIST_CHAIN_IDS_MAP.ZKSYNC_ERA]: CHAINLIST_CURRENCY_SYMBOLS_MAP.ZKSYNC_ERA, [CHAINLIST_CHAIN_IDS_MAP.GOERLI]: @@ -648,6 +667,8 @@ export const CHAIN_ID_TO_CURRENCY_SYMBOL_MAP = { CHAINLIST_CURRENCY_SYMBOLS_MAP.HUOBI_ECO_CHAIN_MAINNET, [CHAINLIST_CHAIN_IDS_MAP.ACALA_NETWORK]: CHAINLIST_CURRENCY_SYMBOLS_MAP.ACALA_NETWORK, + [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: + CHAINLIST_CURRENCY_SYMBOLS_MAP.IOTEX_MAINNET, } as const; /** @@ -694,7 +715,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAIN_IDS.LINEA_MAINNET]: LINEA_MAINNET_TOKEN_IMAGE_URL, [CHAIN_IDS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL, [CHAIN_IDS.BSC]: BNB_TOKEN_IMAGE_URL, - [CHAIN_IDS.POLYGON]: MATIC_TOKEN_IMAGE_URL, + [CHAIN_IDS.POLYGON]: POL_TOKEN_IMAGE_URL, [CHAIN_IDS.ARBITRUM]: AETH_TOKEN_IMAGE_URL, [CHAIN_IDS.FANTOM]: FTM_TOKEN_IMAGE_URL, [CHAIN_IDS.HARMONY]: HARMONY_ONE_TOKEN_IMAGE_URL, @@ -702,6 +723,8 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAIN_IDS.PALM]: PALM_TOKEN_IMAGE_URL, [CHAIN_IDS.CELO]: CELO_TOKEN_IMAGE_URL, [CHAIN_IDS.GNOSIS]: GNOSIS_TOKEN_IMAGE_URL, + [CHAIN_IDS.NEAR]: NEAR_IMAGE_URL, + [CHAIN_IDS.NEAR_TESTNET]: NEAR_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ACALA_NETWORK]: ACALA_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ARBITRUM_NOVA]: ARBITRUM_NOVA_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.ASTAR]: ASTAR_IMAGE_URL, @@ -722,6 +745,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.EVMOS]: EVMOS_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.FLARE_MAINNET]: FLARE_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.FUSE_GOLD_MAINNET]: FUSE_GOLD_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: IOTEX_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.HAQQ_NETWORK]: HAQQ_NETWORK_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.KCC_MAINNET]: KCC_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.KLAYTN_MAINNET_CYPRESS]: KLAYTN_MAINNET_IMAGE_URL, @@ -768,7 +792,7 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.MAINNET]: ETH_TOKEN_IMAGE_URL, [CHAIN_IDS.TEST_ETH]: TEST_ETH_TOKEN_IMAGE_URL, [CHAIN_IDS.BSC]: BNB_TOKEN_IMAGE_URL, - [CHAIN_IDS.POLYGON]: MATIC_TOKEN_IMAGE_URL, + [CHAIN_IDS.POLYGON]: POL_TOKEN_IMAGE_URL, [CHAIN_IDS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL, [CHAIN_IDS.OPTIMISM]: ETH_TOKEN_IMAGE_URL, [CHAIN_IDS.CELO]: CELO_TOKEN_IMAGE_URL, @@ -779,6 +803,11 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.SCROLL_SEPOLIA]: SCROLL_IMAGE_URL, [CHAIN_IDS.NUMBERS]: NUMBERS_TOKEN_IMAGE_URL, [CHAIN_IDS.SEI]: SEI_IMAGE_URL, + [CHAIN_IDS.NEAR]: NEAR_IMAGE_URL, + [CHAIN_IDS.NEAR_TESTNET]: NEAR_IMAGE_URL, + [CHAIN_IDS.MOONRIVER]: MOONRIVER_TOKEN_IMAGE_URL, + [CHAIN_IDS.MOONBEAM]: MOONBEAM_TOKEN_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: IOTEX_TOKEN_IMAGE_URL, } as const; export const INFURA_BLOCKED_KEY = 'countryBlocked'; @@ -938,10 +967,10 @@ export const FEATURED_RPCS: RPCDefinition[] = [ chainId: CHAIN_IDS.POLYGON, nickname: `${POLYGON_DISPLAY_NAME} ${capitalize(NETWORK_TYPES.MAINNET)}`, rpcUrl: `https://polygon-mainnet.infura.io/v3/${infuraProjectId}`, - ticker: CURRENCY_SYMBOLS.MATIC, + ticker: CURRENCY_SYMBOLS.POL, rpcPrefs: { blockExplorerUrl: 'https://polygonscan.com/', - imageUrl: MATIC_TOKEN_IMAGE_URL, + imageUrl: POL_TOKEN_IMAGE_URL, }, }, { diff --git a/shared/constants/offscreen-communication.ts b/shared/constants/offscreen-communication.ts index 618239609956..77c657943842 100644 --- a/shared/constants/offscreen-communication.ts +++ b/shared/constants/offscreen-communication.ts @@ -17,6 +17,7 @@ export enum OffscreenCommunicationTarget { export enum OffscreenCommunicationEvents { trezorDeviceConnect = 'trezor-device-connect', ledgerDeviceConnect = 'ledger-device-connect', + metamaskBackgroundReady = 'metamask-background-ready', } /** diff --git a/shared/constants/permissions.test.js b/shared/constants/permissions.test.js index b63014359bb7..7a1fb0a57500 100644 --- a/shared/constants/permissions.test.js +++ b/shared/constants/permissions.test.js @@ -10,16 +10,13 @@ import { describe('EndowmentPermissions', () => { it('has the expected permission keys', () => { - // Since some permissions are fenced out, this causes problems with the - // test, so we re-add them here. expect(Object.keys(EndowmentPermissions).sort()).toStrictEqual( - [ - 'endowment:name-lookup', - ...Object.keys(endowmentPermissionBuilders).filter( + Object.keys(endowmentPermissionBuilders) + .filter( (targetName) => !Object.keys(ExcludedSnapEndowments).includes(targetName), - ), - ].sort(), + ) + .sort(), ); }); }); diff --git a/shared/constants/permissions.ts b/shared/constants/permissions.ts index 57cccfc81efb..0829d772e854 100644 --- a/shared/constants/permissions.ts +++ b/shared/constants/permissions.ts @@ -17,6 +17,7 @@ export const RestrictedMethods = Object.freeze({ snap_getBip44Entropy: 'snap_getBip44Entropy', snap_getEntropy: 'snap_getEntropy', snap_getLocale: 'snap_getLocale', + snap_getPreferences: 'snap_getPreferences', wallet_snap: 'wallet_snap', ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) snap_manageAccounts: 'snap_manageAccounts', @@ -29,4 +30,45 @@ export const ConnectionPermission = Object.freeze({ connection_permission: 'connection_permission', }); +// This configuration specifies permission weight thresholds used to determine which +// permissions to show or hide on certain Snap-related flows (Install, Update, etc.) +export const PermissionWeightThreshold = Object.freeze({ + snapInstall: 3 as const, + snapUpdateApprovedPermissions: 3 as const, +}); + +// Specify minimum number of permissions to be shown, when abstraction is applied +export const MinPermissionAbstractionDisplayCount = 3; + +// Specify number of permissions used as threshold for permission abstraction logic to be applied +export const PermissionsAbstractionThreshold = 3; + +export const PermissionWeight = Object.freeze({ + eth_accounts: 3, + permittedChains: 3, + snap_dialog: 4, + snap_notify: 4, + snap_getBip32PublicKey: 3, + snap_getBip32Entropy: 1, + snap_getBip44Entropy: 1, + snap_getEntropy: 3, + snap_manageState: 4, + snap_getLocale: 4, + wallet_snap: 4, + endowment_networkAccess: 3, + endowment_webassembly: 4, + endowment_transactionInsight: 4, + endowment_cronjob: 4, + endowment_ethereumProvider: 4, + endowment_rpc: 4, + endowment_lifecycleHooks: 4, + endowment_pageHome: 4, + snap_manageAccounts: 3, + endowment_keyring: 4, + endowment_nameLookup: 3, + endowment_signatureInsight: 4, + connection_permission: 3, + unknown_permission: 3, +}); + export * from './snaps/permissions'; diff --git a/shared/constants/security-provider.ts b/shared/constants/security-provider.ts index 1c8dd6433fac..e6fff53ee28a 100644 --- a/shared/constants/security-provider.ts +++ b/shared/constants/security-provider.ts @@ -14,7 +14,7 @@ type SecurityProviderConfig = Record< { /** translation key for security provider name */ readonly tKeyName: string; - /** URL to securty provider website */ + /** URL to security provider website */ readonly url: string; } >; @@ -100,6 +100,10 @@ export const SECURITY_PROVIDER_SUPPORTED_CHAIN_IDS: Hex[] = [ CHAIN_IDS.OPTIMISM, CHAIN_IDS.POLYGON, CHAIN_IDS.SEPOLIA, + CHAIN_IDS.ZKSYNC_ERA, + CHAIN_IDS.SCROLL, + CHAIN_IDS.BERACHAIN, + CHAIN_IDS.METACHAIN_ONE, ]; export const SECURITY_PROVIDER_EXCLUDED_TRANSACTION_TYPES = [ @@ -112,3 +116,11 @@ export const LOADING_SECURITY_ALERT_RESPONSE: SecurityAlertResponse = { result_type: BlockaidResultType.Loading, reason: BlockaidReason.inProgress, }; + +export enum SecurityAlertSource { + /** Validation performed remotely using the Security Alerts API. */ + API = 'api', + + /** Validation performed locally using the PPOM. */ + Local = 'local', +} diff --git a/shared/constants/signatures.ts b/shared/constants/signatures.ts new file mode 100644 index 000000000000..3ecc56be8630 --- /dev/null +++ b/shared/constants/signatures.ts @@ -0,0 +1,29 @@ +export enum PrimaryTypeOrder { + Order = 'Order', + OrderComponents = 'OrderComponents', +} + +export enum PrimaryTypePermit { + Permit = 'Permit', + PermitBatch = 'PermitBatch', + PermitBatchTransferFrom = 'PermitBatchTransferFrom', + PermitSingle = 'PermitSingle', + PermitTransferFrom = 'PermitTransferFrom', +} + +/** + * EIP-712 Permit PrimaryTypes + */ +export const PrimaryType = { + ...PrimaryTypeOrder, + ...PrimaryTypePermit, +} as const; + +// Create a type from the const object +export type PrimaryType = (typeof PrimaryType)[keyof typeof PrimaryType]; + +export const PRIMARY_TYPES_ORDER: PrimaryTypeOrder[] = + Object.values(PrimaryTypeOrder); +export const PRIMARY_TYPES_PERMIT: PrimaryTypePermit[] = + Object.values(PrimaryTypePermit); +export const PRIMARY_TYPES: PrimaryType[] = Object.values(PrimaryType); diff --git a/shared/constants/snaps/permissions.ts b/shared/constants/snaps/permissions.ts index aca6f5ef8023..837beea4d9ff 100644 --- a/shared/constants/snaps/permissions.ts +++ b/shared/constants/snaps/permissions.ts @@ -8,9 +8,7 @@ export const EndowmentPermissions = Object.freeze({ 'endowment:lifecycle-hooks': 'endowment:lifecycle-hooks', 'endowment:page-home': 'endowment:page-home', 'endowment:signature-insight': 'endowment:signature-insight', - ///: BEGIN:ONLY_INCLUDE_IF(build-flask) 'endowment:name-lookup': 'endowment:name-lookup', - ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) 'endowment:keyring': 'endowment:keyring', ///: END:ONLY_INCLUDE_IF @@ -22,11 +20,6 @@ export const ExcludedSnapPermissions = Object.freeze({ 'eth_accounts is disabled. For more information please see https://github.com/MetaMask/snaps/issues/990.', }); -export const ExcludedSnapEndowments = Object.freeze({ - ///: BEGIN:ONLY_INCLUDE_IF(build-main) - 'endowment:name-lookup': - 'This endowment is experimental and therefore not available.', - ///: END:ONLY_INCLUDE_IF -}); +export const ExcludedSnapEndowments = Object.freeze({}); export const DynamicSnapPermissions = Object.freeze(['eth_accounts']); diff --git a/shared/constants/swaps.ts b/shared/constants/swaps.ts index bd3db9d94a2f..53e1243acaa0 100644 --- a/shared/constants/swaps.ts +++ b/shared/constants/swaps.ts @@ -2,7 +2,7 @@ import { ETH_TOKEN_IMAGE_URL, TEST_ETH_TOKEN_IMAGE_URL, BNB_TOKEN_IMAGE_URL, - MATIC_TOKEN_IMAGE_URL, + POL_TOKEN_IMAGE_URL, AVAX_TOKEN_IMAGE_URL, CURRENCY_SYMBOLS, CHAIN_IDS, @@ -74,7 +74,7 @@ export const MATIC_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { name: 'Matic', address: DEFAULT_TOKEN_ADDRESS, decimals: 18, - iconUrl: MATIC_TOKEN_IMAGE_URL, + iconUrl: POL_TOKEN_IMAGE_URL, } as const; export const AVAX_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { diff --git a/shared/constants/transaction.ts b/shared/constants/transaction.ts index 726148865512..24b89140f941 100644 --- a/shared/constants/transaction.ts +++ b/shared/constants/transaction.ts @@ -11,7 +11,6 @@ export const IN_PROGRESS_TRANSACTION_STATUSES = [ ]; export const SIGNING_METHODS = Object.freeze([ - 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -195,5 +194,3 @@ export enum TokenStandard { /** Not a token, but rather the base asset of the selected chain. */ none = 'NONE', } - -export const EIP712_PRIMARY_TYPE_PERMIT = 'Permit'; diff --git a/shared/constants/urls.ts b/shared/constants/urls.ts index a44d7d2573c8..66ce3e1c9438 100644 --- a/shared/constants/urls.ts +++ b/shared/constants/urls.ts @@ -1,3 +1,7 @@ export enum BaseUrl { Portfolio = 'https://portfolio.metamask.io', } + +export enum SurveyUrl { + BtcSupport = 'https://www.getfeedback.com/r/yG6FbiW5', +} diff --git a/shared/lib/four-byte.test.ts b/shared/lib/four-byte.test.ts new file mode 100644 index 000000000000..e25235e6ae54 --- /dev/null +++ b/shared/lib/four-byte.test.ts @@ -0,0 +1,65 @@ +import { HttpProvider } from '@metamask/ethjs'; +import nock from 'nock'; + +import { + FOUR_BYTE_RESPONSE, + TRANSACTION_DATA_FOUR_BYTE, +} from '../../test/data/confirmations/transaction-decode'; +import { getMethodDataAsync, getMethodFrom4Byte } from './four-byte'; + +const FOUR_BYTE_MOCK = TRANSACTION_DATA_FOUR_BYTE.slice(0, 10); + +describe('Four Byte', () => { + const fetchMock = jest.fn(); + + describe('getMethodFrom4Byte', () => { + it('returns signature with earliest creation date', async () => { + jest.spyOn(global, 'fetch').mockImplementation(fetchMock); + + fetchMock.mockResolvedValue({ + ok: true, + json: async () => FOUR_BYTE_RESPONSE, + }); + + const result = await getMethodFrom4Byte(FOUR_BYTE_MOCK); + + expect(result).toStrictEqual('someOtherFunction(address,uint256)'); + }); + }); + + describe('getMethodDataAsync', () => { + global.ethereumProvider = new HttpProvider( + 'https://mainnet.infura.io/v3/341eacb578dd44a1a049cbc5f6fd4035', + ); + it('returns a valid signature for setApprovalForAll when use4ByteResolution privacy setting is ON', async () => { + nock('https://www.4byte.directory:443', { encodedQueryParams: true }) + .get('/api/v1/signatures/') + .query({ hex_signature: '0xa22cb465' }) + .reply(200, { + count: 2, + next: null, + previous: null, + results: [ + { + id: 841519, + created_at: '2022-06-12T00:50:19.305588Z', + text_signature: 'niceFunctionHerePlzClick943230089(address,bool)', + hex_signature: '0xa22cb465', + bytes_signature: '¢,´e', + }, + { + id: 29659, + created_at: '2018-04-11T21:47:39.980645Z', + text_signature: 'setApprovalForAll(address,bool)', + hex_signature: '0xa22cb465', + bytes_signature: '¢,´e', + }, + ], + }); + expect(await getMethodDataAsync('0xa22cb465', true)).toStrictEqual({ + name: 'Set Approval For All', + params: [{ type: 'address' }, { type: 'bool' }], + }); + }); + }); +}); diff --git a/shared/lib/four-byte.ts b/shared/lib/four-byte.ts new file mode 100644 index 000000000000..167dfc563b73 --- /dev/null +++ b/shared/lib/four-byte.ts @@ -0,0 +1,83 @@ +import { MethodRegistry } from 'eth-method-registry'; +import fetchWithCache from './fetch-with-cache'; + +type FourByteResult = { + created_at: string; + text_signature: string; +}; + +type FourByteResponse = { + results: FourByteResult[]; +}; + +export async function getMethodFrom4Byte( + fourBytePrefix: string, +): Promise { + const fourByteResponse = (await fetchWithCache({ + url: `https://www.4byte.directory/api/v1/signatures/?hex_signature=${fourBytePrefix}`, + fetchOptions: { + referrerPolicy: 'no-referrer-when-downgrade', + body: null, + method: 'GET', + mode: 'cors', + }, + functionName: 'getMethodFrom4Byte', + })) as FourByteResponse; + + fourByteResponse.results.sort((a, b) => { + return new Date(a.created_at).getTime() < new Date(b.created_at).getTime() + ? -1 + : 1; + }); + + return fourByteResponse.results[0].text_signature; +} + +let registry: MethodRegistry | undefined; + +type HttpProvider = { + host: string; + timeout: number; +}; + +type MethodRegistryArgs = { + network: string; + provider: HttpProvider; +}; + +export async function getMethodDataAsync( + fourBytePrefix: string, + allow4ByteRequests: boolean, + provider?: unknown, +) { + try { + let fourByteSig = null; + if (allow4ByteRequests) { + fourByteSig = await getMethodFrom4Byte(fourBytePrefix).catch((e) => { + console.error(e); + return null; + }); + console.log('fourByteSig = ', fourByteSig); + } + + if (!registry) { + registry = new MethodRegistry({ + provider: provider ?? global.ethereumProvider, + } as MethodRegistryArgs); + } + + if (!fourByteSig) { + return {}; + } + + const parsedResult = registry.parse(fourByteSig); + + return { + name: parsedResult.name, + params: parsedResult.args, + }; + } catch (error) { + console.error(error); + return {}; + } +} diff --git a/shared/lib/index.d.ts b/shared/lib/index.d.ts new file mode 100644 index 000000000000..03952d5a6c3d --- /dev/null +++ b/shared/lib/index.d.ts @@ -0,0 +1 @@ +declare module '@metamask/ethjs'; diff --git a/shared/lib/trace.test.ts b/shared/lib/trace.test.ts index a1845cb2fbd7..fc3294924698 100644 --- a/shared/lib/trace.test.ts +++ b/shared/lib/trace.test.ts @@ -1,12 +1,19 @@ -import { Span, startSpan, withIsolationScope } from '@sentry/browser'; -import { trace } from './trace'; +import { + Span, + startSpan, + startSpanManual, + withIsolationScope, +} from '@sentry/browser'; +import { endTrace, trace, TraceName } from './trace'; jest.mock('@sentry/browser', () => ({ withIsolationScope: jest.fn(), startSpan: jest.fn(), + startSpanManual: jest.fn(), })); -const NAME_MOCK = 'testTransaction'; +const NAME_MOCK = TraceName.Transaction; +const ID_MOCK = 'testId'; const PARENT_CONTEXT_MOCK = {} as Span; const TAGS_MOCK = { @@ -21,14 +28,9 @@ const DATA_MOCK = { data3: 123, }; -function mockGetMetaMetricsEnabled(enabled: boolean) { - global.sentry = { - getMetaMetricsEnabled: () => Promise.resolve(enabled), - }; -} - describe('Trace', () => { const startSpanMock = jest.mocked(startSpan); + const startSpanManualMock = jest.mocked(startSpanManual); const withIsolationScopeMock = jest.mocked(withIsolationScope); const setTagsMock = jest.fn(); @@ -44,53 +46,30 @@ describe('Trace', () => { }); describe('trace', () => { - // @ts-expect-error This function is missing from the Mocha type definitions - it.each([ - ['enabled', true], - ['disabled', false], - ])( - 'executes callback if Sentry is %s', - async (_: string, sentryEnabled: boolean) => { - let callbackExecuted = false; - - mockGetMetaMetricsEnabled(sentryEnabled); - - await trace({ name: NAME_MOCK }, async () => { - callbackExecuted = true; - }); - - expect(callbackExecuted).toBe(true); - }, - ); + it('executes callback', () => { + let callbackExecuted = false; - // @ts-expect-error This function is missing from the Mocha type definitions - it.each([ - ['enabled', true], - ['disabled', false], - ])( - 'returns value from callback if Sentry is %s', - async (_: string, sentryEnabled: boolean) => { - mockGetMetaMetricsEnabled(sentryEnabled); - - const result = await trace({ name: NAME_MOCK }, async () => { - return true; - }); - - expect(result).toBe(true); - }, - ); + trace({ name: NAME_MOCK }, () => { + callbackExecuted = true; + }); - it('invokes Sentry if enabled', async () => { - mockGetMetaMetricsEnabled(true); + expect(callbackExecuted).toBe(true); + }); - await trace( + it('returns value from callback', () => { + const result = trace({ name: NAME_MOCK }, () => true); + expect(result).toBe(true); + }); + + it('invokes Sentry if callback provided', () => { + trace( { name: NAME_MOCK, tags: TAGS_MOCK, data: DATA_MOCK, parentContext: PARENT_CONTEXT_MOCK, }, - async () => Promise.resolve(), + () => true, ); expect(withIsolationScopeMock).toHaveBeenCalledTimes(1); @@ -101,6 +80,7 @@ describe('Trace', () => { name: NAME_MOCK, parentSpan: PARENT_CONTEXT_MOCK, attributes: DATA_MOCK, + op: 'custom', }, expect.any(Function), ); @@ -108,5 +88,153 @@ describe('Trace', () => { expect(setTagsMock).toHaveBeenCalledTimes(1); expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); }); + + it('invokes Sentry if no callback provided', () => { + trace({ + id: ID_MOCK, + name: NAME_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + expect(withIsolationScopeMock).toHaveBeenCalledTimes(1); + + expect(startSpanManualMock).toHaveBeenCalledTimes(1); + expect(startSpanManualMock).toHaveBeenCalledWith( + { + name: NAME_MOCK, + parentSpan: PARENT_CONTEXT_MOCK, + attributes: DATA_MOCK, + op: 'custom', + }, + expect.any(Function), + ); + + expect(setTagsMock).toHaveBeenCalledTimes(1); + expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + }); + + it('invokes Sentry if no callback provided with custom start time', () => { + trace({ + id: ID_MOCK, + name: NAME_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + startTime: 123, + }); + + expect(withIsolationScopeMock).toHaveBeenCalledTimes(1); + + expect(startSpanManualMock).toHaveBeenCalledTimes(1); + expect(startSpanManualMock).toHaveBeenCalledWith( + { + name: NAME_MOCK, + parentSpan: PARENT_CONTEXT_MOCK, + attributes: DATA_MOCK, + op: 'custom', + startTime: 123, + }, + expect.any(Function), + ); + + expect(setTagsMock).toHaveBeenCalledTimes(1); + expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + }); + }); + + describe('endTrace', () => { + it('ends Sentry span matching name and specified ID', () => { + const spanEndMock = jest.fn(); + const spanMock = { end: spanEndMock } as unknown as Span; + + startSpanManualMock.mockImplementationOnce((_, fn) => + fn(spanMock, () => { + // Intentionally empty + }), + ); + + trace({ + name: NAME_MOCK, + id: ID_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK, id: ID_MOCK }); + + expect(spanEndMock).toHaveBeenCalledTimes(1); + }); + + it('ends Sentry span matching name and default ID', () => { + const spanEndMock = jest.fn(); + const spanMock = { end: spanEndMock } as unknown as Span; + + startSpanManualMock.mockImplementationOnce((_, fn) => + fn(spanMock, () => { + // Intentionally empty + }), + ); + + trace({ + name: NAME_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK }); + + expect(spanEndMock).toHaveBeenCalledTimes(1); + }); + + it('ends Sentry span with custom timestamp', () => { + const spanEndMock = jest.fn(); + const spanMock = { end: spanEndMock } as unknown as Span; + + startSpanManualMock.mockImplementationOnce((_, fn) => + fn(spanMock, () => { + // Intentionally empty + }), + ); + + trace({ + name: NAME_MOCK, + id: ID_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK, id: ID_MOCK, timestamp: 123 }); + + expect(spanEndMock).toHaveBeenCalledTimes(1); + expect(spanEndMock).toHaveBeenCalledWith(123); + }); + + it('does not end Sentry span if name and ID does not match', () => { + const spanEndMock = jest.fn(); + const spanMock = { end: spanEndMock } as unknown as Span; + + startSpanManualMock.mockImplementationOnce((_, fn) => + fn(spanMock, () => { + // Intentionally empty + }), + ); + + trace({ + name: NAME_MOCK, + id: ID_MOCK, + tags: TAGS_MOCK, + data: DATA_MOCK, + parentContext: PARENT_CONTEXT_MOCK, + }); + + endTrace({ name: NAME_MOCK, id: 'invalidId' }); + + expect(spanEndMock).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index c585edb981ba..b9f59a2f2353 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -1,54 +1,209 @@ import * as Sentry from '@sentry/browser'; -import { Primitive } from '@sentry/types'; +import { Primitive, StartSpanOptions } from '@sentry/types'; import { createModuleLogger } from '@metamask/utils'; import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; +export enum TraceName { + BackgroundConnect = 'Background Connect', + DeveloperTest = 'Developer Test', + FirstRender = 'First Render', + GetState = 'Get State', + InitialActions = 'Initial Actions', + LoadScripts = 'Load Scripts', + Middleware = 'Middleware', + NestedTest1 = 'Nested Test 1', + NestedTest2 = 'Nested Test 2', + NotificationDisplay = 'Notification Display', + PPOMValidation = 'PPOM Validation', + SetupStore = 'Setup Store', + Transaction = 'Transaction', + UIStartup = 'UI Startup', +} + const log = createModuleLogger(sentryLogger, 'trace'); +const ID_DEFAULT = 'default'; +const OP_DEFAULT = 'custom'; + +const tracesByKey: Map = new Map(); + +type PendingTrace = { + end: (timestamp?: number) => void; + request: TraceRequest; + startTime: number; +}; + +export type TraceContext = unknown; + +export type TraceCallback = (context?: TraceContext) => T; + export type TraceRequest = { data?: Record; - name: string; - parentContext?: unknown; + id?: string; + name: TraceName; + parentContext?: TraceContext; + startTime?: number; tags?: Record; }; -export async function trace( +export type EndTraceRequest = { + id?: string; + name: TraceName; + timestamp?: number; +}; + +export function trace(request: TraceRequest, fn: TraceCallback): T; + +export function trace(request: TraceRequest): TraceContext; + +export function trace( request: TraceRequest, - fn: (context?: unknown) => Promise, -): Promise { - const { data: attributes, name, parentContext, tags } = request; - const parentSpan = (parentContext ?? null) as Sentry.Span | null; + fn?: TraceCallback, +): T | TraceContext { + if (!fn) { + return startTrace(request); + } + + return traceCallback(request, fn); +} + +export function endTrace(request: EndTraceRequest) { + const { name, timestamp } = request; + const id = getTraceId(request); + const key = getTraceKey(request); + const pendingTrace = tracesByKey.get(key); + + if (!pendingTrace) { + log('No pending trace found', name, id); + return; + } - const isSentryEnabled = - (await globalThis.sentry.getMetaMetricsEnabled()) as boolean; + pendingTrace.end(timestamp); + + tracesByKey.delete(key); + + const { request: pendingRequest, startTime } = pendingTrace; + const endTime = timestamp ?? getPerformanceTimestamp(); + const duration = endTime - startTime; + + log('Finished trace', name, id, duration, { request: pendingRequest }); +} - const callback = async (span: Sentry.Span | null) => { +function traceCallback(request: TraceRequest, fn: TraceCallback): T { + const { name } = request; + + const callback = (span: Sentry.Span | null) => { log('Starting trace', name, request); const start = Date.now(); - let error; - - try { - return await fn(span); - } catch (currentError) { - error = currentError; - throw currentError; - } finally { - const end = Date.now(); - const duration = end - start; - - log('Finished trace', name, duration, { error, request }); - } + let error: unknown; + + return tryCatchMaybePromise( + () => fn(span), + (currentError) => { + error = currentError; + throw currentError; + }, + () => { + const end = Date.now(); + const duration = end - start; + + log('Finished trace', name, duration, { error, request }); + }, + ) as T; }; - if (!isSentryEnabled) { - log('Skipping Sentry trace as metrics disabled', name, request); - return callback(null); - } + return startSpan(request, (spanOptions) => + Sentry.startSpan(spanOptions, callback), + ); +} + +function startTrace(request: TraceRequest): TraceContext { + const { name, startTime: requestStartTime } = request; + const startTime = requestStartTime ?? getPerformanceTimestamp(); + const id = getTraceId(request); - return await Sentry.withIsolationScope(async (scope) => { + const callback = (span: Sentry.Span | null) => { + const end = (timestamp?: number) => { + span?.end(timestamp); + }; + + const pendingTrace = { end, request, startTime }; + const key = getTraceKey(request); + tracesByKey.set(key, pendingTrace); + + log('Started trace', name, id, request); + + return span; + }; + + return startSpan(request, (spanOptions) => + Sentry.startSpanManual(spanOptions, callback), + ); +} + +function startSpan( + request: TraceRequest, + callback: (spanOptions: StartSpanOptions) => T, +) { + const { data: attributes, name, parentContext, startTime, tags } = request; + const parentSpan = (parentContext ?? null) as Sentry.Span | null; + + const spanOptions: StartSpanOptions = { + attributes, + name, + op: OP_DEFAULT, + parentSpan, + startTime, + }; + + return Sentry.withIsolationScope((scope) => { scope.setTags(tags as Record); - return await Sentry.startSpan({ name, parentSpan, attributes }, callback); + return callback(spanOptions); }); } + +function getTraceId(request: TraceRequest) { + return request.id ?? ID_DEFAULT; +} + +function getTraceKey(request: TraceRequest) { + const { name } = request; + const id = getTraceId(request); + + return [name, id].join(':'); +} + +function getPerformanceTimestamp(): number { + return performance.timeOrigin + performance.now(); +} + +function tryCatchMaybePromise( + tryFn: () => T, + catchFn: (error: unknown) => void, + finallyFn: () => void, +): T | undefined { + let isPromise = false; + + try { + const result = tryFn() as T; + + if (result instanceof Promise) { + isPromise = true; + return result.catch(catchFn).finally(finallyFn) as T; + } + + return result; + } catch (error) { + if (!isPromise) { + catchFn(error); + } + } finally { + if (!isPromise) { + finallyFn(); + } + } + + return undefined; +} diff --git a/shared/lib/transaction-controller-utils.test.js b/shared/lib/transaction-controller-utils.test.js index c7be714d00c5..7cc7bc75b22f 100644 --- a/shared/lib/transaction-controller-utils.test.js +++ b/shared/lib/transaction-controller-utils.test.js @@ -1,4 +1,5 @@ import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import BigNumber from 'bignumber.js'; import { EtherDenomination } from '../constants/common'; import { CHAIN_IDS } from '../constants/network'; import { Numeric } from '../modules/Numeric'; @@ -17,6 +18,53 @@ describe('transaction controller utils', () => { }); }); + describe('calcTokenAmount()', () => { + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + // number values + [0, 5, '0'], + [123456, undefined, '123456'], + [123456, 5, '1.23456'], + [123456, 6, '0.123456'], + // Do not delete the following test. Testing decimal = 36 is important because it has broken + // BigNumber#div in the past when the value that was passed into it was not a BigNumber. + [123456, 36, '1.23456e-31'], + [3000123456789678, 6, '3000123456.789678'], + // eslint-disable-next-line no-loss-of-precision + [3000123456789123456789123456789, 3, '3.0001234567891233e+27'], // expected precision lost + // eslint-disable-next-line no-loss-of-precision + [3000123456789123456789123456789, 6, '3.0001234567891233e+24'], // expected precision lost + // string values + ['0', 5, '0'], + ['123456', undefined, '123456'], + ['123456', 5, '1.23456'], + ['123456', 6, '0.123456'], + ['3000123456789678', 6, '3000123456.789678'], + [ + '3000123456789123456789123456789', + 3, + '3.000123456789123456789123456789e+27', + ], + [ + '3000123456789123456789123456789', + 6, + '3.000123456789123456789123456789e+24', + ], + // BigNumber values + [new BigNumber('3000123456789678'), 6, '3000123456.789678'], + [ + new BigNumber('3000123456789123456789123456789'), + 6, + '3.000123456789123456789123456789e+24', + ], + ])( + 'returns the value %s divided by 10^%s = %s', + (value, decimals, expected) => { + expect(calcTokenAmount(value, decimals).toString()).toBe(expected); + }, + ); + }); + describe('getSwapsTokensReceivedFromTxMeta', () => { it('returns null if txMeta is not well formed', () => { expect(getSwapsTokensReceivedFromTxMeta('ETH', {}, '0x00')).toBe(null); @@ -230,4 +278,72 @@ describe('transaction controller utils', () => { ).toBe(calcTokenAmount(logs[0].data, 8).toString(10), 6); }); }); + + it('respects the precision argument for the default token', () => { + const preTxBalance = new Numeric('5000000', 10, EtherDenomination.WEI); + const postTxBalance = new Numeric( + '592624562452462456762123343', + 10, + EtherDenomination.WEI, + ); + const gasUsed = new Numeric('28000', 10).toPrefixedHexString(); + const effectiveGasPrice = new Numeric('21', 10).toPrefixedHexString(); + const gasCost = calcGasTotal(gasUsed, effectiveGasPrice); + const ethReceived = postTxBalance + .minus(preTxBalance.minus(gasCost, 16)) + .toDenomination(EtherDenomination.ETH); + + const get = (precision) => + getSwapsTokensReceivedFromTxMeta( + 'ETH', + { + txReceipt: { + gasUsed, + effectiveGasPrice, + type: TransactionEnvelopeType.feeMarket, + }, + preTxBalance: preTxBalance.toPrefixedHexString(), + postTxBalance: postTxBalance.toPrefixedHexString(), + swapMetaData: { token_to_amount: '0x1' }, + }, + '0x00', + '0x00', + '8', + {}, + CHAIN_IDS.MAINNET, + precision, + ); + + expect(get(null)).toBe(ethReceived.toString()); + for (let precision = 1; precision < 10; precision++) { + expect(get(precision)).toBe(ethReceived.round(precision).toString()); + } + }); + + it('respects the precision argument for a non-default token', () => { + const logs = [ + { + topics: [TOKEN_TRANSFER_LOG_TOPIC_HASH, '', '0x00'], + address: '0x00', + data: new Numeric('123456789', 10).toPrefixedHexString(), + }, + ]; + const fullPrecision = calcTokenAmount(logs[0].data, 8); + const get = (precision) => + getSwapsTokensReceivedFromTxMeta( + 'USDC', + { txReceipt: { logs, status: '0x1' } }, + '0x00', + '0x00', + '8', + { txReceipt: {} }, + CHAIN_IDS.MAINNET, + precision, + ); + + expect(get(null)).toBe(fullPrecision.toString()); + for (let precision = 1; precision < 10; precision++) { + expect(get(precision)).toBe(fullPrecision.toPrecision(precision)); + } + }); }); diff --git a/shared/lib/transactions-controller-utils.js b/shared/lib/transactions-controller-utils.js index 073ff922af67..88b7015b2090 100644 --- a/shared/lib/transactions-controller-utils.js +++ b/shared/lib/transactions-controller-utils.js @@ -1,5 +1,6 @@ import BigNumber from 'bignumber.js'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; + import { EtherDenomination } from '../constants/common'; import { Numeric } from '../modules/Numeric'; import { isSwapsDefaultTokenSymbol } from '../modules/swaps.utils'; @@ -33,9 +34,14 @@ export function toPrecisionWithoutTrailingZeros(n, precision) { .replace(/(\.[0-9]*[1-9])0*|(\.0*)/u, '$1'); } -export function calcTokenAmount(value, decimals) { - const multiplier = Math.pow(10, Number(decimals || 0)); - return new BigNumber(String(value)).div(multiplier); +/** + * @param {number|string|BigNumber} value + * @param {number} decimals + * @returns {BigNumber} + */ +export function calcTokenAmount(value, decimals = 0) { + const divisor = new BigNumber(10).pow(decimals); + return new BigNumber(String(value)).div(divisor); } export function getSwapsTokensReceivedFromTxMeta( @@ -46,6 +52,7 @@ export function getSwapsTokensReceivedFromTxMeta( tokenDecimals, approvalTxMeta, chainId, + precision = 6, ) { const accountAddress = txMeta?.swapAndSendRecipient ?? senderAddress; @@ -100,9 +107,11 @@ export function getSwapsTokensReceivedFromTxMeta( ) .minus(preTxBalanceLessGasCost) .toDenomination(EtherDenomination.ETH) - .toBase(10) - .round(6); - return ethReceived.toString(); + .toBase(10); + + return ( + precision === null ? ethReceived : ethReceived.round(precision) + ).toFixed(); } const txReceiptLogs = txReceipt?.logs; if (txReceiptLogs && txReceipt?.status !== '0x0') { @@ -121,12 +130,14 @@ export function getSwapsTokensReceivedFromTxMeta( isTransferFromGivenAddress ); }); - return tokenTransferLog - ? toPrecisionWithoutTrailingZeros( - calcTokenAmount(tokenTransferLog.data, tokenDecimals).toString(10), - 6, - ) - : ''; + + if (tokenTransferLog) { + const tokenAmount = calcTokenAmount(tokenTransferLog.data, tokenDecimals); + return precision === null + ? tokenAmount.toFixed() + : toPrecisionWithoutTrailingZeros(tokenAmount, precision); + } + return ''; } return null; } diff --git a/shared/modules/conversion.utils.ts b/shared/modules/conversion.utils.ts index 0f3a35dde774..d9fd2262f843 100644 --- a/shared/modules/conversion.utils.ts +++ b/shared/modules/conversion.utils.ts @@ -1,5 +1,5 @@ +import { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; - import { addHexPrefix, BN } from 'ethereumjs-util'; import { EtherDenomination } from '../constants/common'; import { Numeric, NumericValue } from './Numeric'; @@ -25,6 +25,13 @@ export function addHexes(aHexWEI: string, bHexWEI: string) { .toString(); } +export function multiplyHexes(aHexWEI: Hex, bHexWEI: Hex): Hex { + return new Numeric(aHexWEI, 16) + .times(new Numeric(bHexWEI, 16)) + .round(6, BigNumber.ROUND_HALF_DOWN) + .toString() as Hex; +} + export function decWEIToDecETH(decWEI: string) { return new Numeric(decWEI, 10, EtherDenomination.WEI) .toDenomination(EtherDenomination.ETH) diff --git a/shared/modules/metametrics.test.ts b/shared/modules/metametrics.test.ts index 1227be64db6f..29434cfb1687 100644 --- a/shared/modules/metametrics.test.ts +++ b/shared/modules/metametrics.test.ts @@ -39,6 +39,10 @@ const createTransactionMetricsRequest = (customProps = {}) => { trackEvent: jest.fn(), getIsSmartTransaction: jest.fn(), getSmartTransactionByMinedTxHash: jest.fn(), + getRedesignedTransactionsEnabled: jest.fn(), + getMethodData: jest.fn(), + getIsRedesignedConfirmationsDeveloperEnabled: jest.fn(), + getIsConfirmationAdvancedDetailsOpen: jest.fn(), ...customProps, } as TransactionMetricsRequest; }; diff --git a/shared/modules/mv3.utils.js b/shared/modules/mv3.utils.js index e969c5b74228..417484c46de6 100644 --- a/shared/modules/mv3.utils.js +++ b/shared/modules/mv3.utils.js @@ -1,3 +1,5 @@ +/* eslint-disable import/unambiguous -- Not an external module and not of concern */ + const runtimeManifest = global.chrome?.runtime.getManifest() || global.browser?.runtime.getManifest(); @@ -8,7 +10,7 @@ const runtimeManifest = * unavailable. That's why we have a fallback using the ENABLE_MV3 constant. The fallback is also * used in unit tests. */ -export const isManifestV3 = runtimeManifest +const isManifestV3 = runtimeManifest ? runtimeManifest.manifest_version === 3 : // Our build system sets this as a boolean, but in a Node.js context (e.g. unit tests) it will // always be a string @@ -21,7 +23,7 @@ export const isManifestV3 = runtimeManifest * This is only available in when the manifest is version 3, and only in chromium * versions 109 and higher. As of June 7, 2024, it is not available in firefox. */ -export const isOffscreenAvailable = Boolean(global.chrome?.offscreen); +const isOffscreenAvailable = Boolean(global.chrome?.offscreen); /** * A boolean indicating whether the current extension's manifest is version 3 @@ -29,5 +31,10 @@ export const isOffscreenAvailable = Boolean(global.chrome?.offscreen); * happen to users on MetaMask versions 11.16.7 and higher, who are using a * chromium browser with a version below 109. */ -export const isMv3ButOffscreenDocIsMissing = - isManifestV3 && !isOffscreenAvailable; +const isMv3ButOffscreenDocIsMissing = isManifestV3 && !isOffscreenAvailable; + +module.exports = { + isManifestV3, + isOffscreenAvailable, + isMv3ButOffscreenDocIsMissing, +}; diff --git a/shared/modules/object.utils.js b/shared/modules/object.utils.js index 111db13c4a92..3da08956cb02 100644 --- a/shared/modules/object.utils.js +++ b/shared/modules/object.utils.js @@ -35,7 +35,8 @@ export function maskObject(object, mask) { } else if (maskKey && typeof maskKey === 'object') { state[key] = maskObject(object[key], maskKey); } else if (maskKey === undefined || maskKey === false) { - state[key] = typeof object[key]; + // As typeof null (misleadingly) returns “object,” it would be more readable to display “null” instead of “object.” + state[key] = object[key] === null ? null : typeof object[key]; } else { throw new Error(`Unsupported mask entry: ${maskKey}`); } diff --git a/shared/modules/selectors/account.ts b/shared/modules/selectors/account.ts new file mode 100644 index 000000000000..e124ed6f0458 --- /dev/null +++ b/shared/modules/selectors/account.ts @@ -0,0 +1,3 @@ +import { isHardwareWallet } from '../../../ui/selectors/selectors'; + +export { isHardwareWallet }; diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts index 4bdacaacd93f..f1dc4fee5ec2 100644 --- a/shared/modules/selectors/index.test.ts +++ b/shared/modules/selectors/index.test.ts @@ -1,5 +1,6 @@ import { createSwapsMockStore } from '../../../test/jest'; -import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../constants/network'; +import { CHAIN_IDS } from '../../constants/network'; +import { mockNetworkState } from '../../../test/stub/networks'; import { getSmartTransactionsOptInStatus, getCurrentChainSupportsSmartTransactions, @@ -14,7 +15,6 @@ describe('Selectors', () => { metamask: { preferences: { smartTransactionsOptInStatus: true, - showTokenAutodetectModal: true, }, internalAccounts: { selectedAccount: 'account1', @@ -36,9 +36,6 @@ describe('Selectors', () => { balance: '0x15f6f0b9d4f8d000', }, }, - providerConfig: { - chainId: CHAIN_IDS.MAINNET, - }, swapsState: { swapsFeatureFlags: { ethereum: { @@ -59,13 +56,11 @@ describe('Selectors', () => { smartTransactionsState: { liveness: true, }, - networkConfigurations: { - 'network-configuration-id-1': { - chainId: CHAIN_IDS.MAINNET, - ticker: CURRENCY_SYMBOLS.ETH, - rpcUrl: 'https://mainnet.infura.io/v3/', - }, - }, + ...mockNetworkState({ + id: 'network-configuration-id-1', + chainId: CHAIN_IDS.MAINNET, + rpcUrl: 'https://mainnet.infura.io/v3/', + }), }, }; }; @@ -78,14 +73,6 @@ describe('Selectors', () => { }); }); - describe('getShowTokenAutodetectModal', () => { - it('should return show autodetection token modal status', () => { - const state = createMockState(); - const result = getSmartTransactionsOptInStatus(state); - expect(result).toBe(true); - }); - }); - describe('getCurrentChainSupportsSmartTransactions', () => { it('should return true if the chain ID is allowed for smart transactions', () => { const state = createMockState(); @@ -99,10 +86,7 @@ describe('Selectors', () => { ...state, metamask: { ...state.metamask, - providerConfig: { - ...state.metamask.providerConfig, - chainId: CHAIN_IDS.POLYGON, - }, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), }, }; const result = getCurrentChainSupportsSmartTransactions(newState); @@ -129,7 +113,7 @@ describe('Selectors', () => { expect(getSmartTransactionsEnabled(state)).toBe(false); }); - it('returns false if feature flag is enabled, is a HW and is Ethereum network', () => { + it('returns true if feature flag is enabled, is a HW and is Ethereum network', () => { const state = createSwapsMockStore(); const newState = { ...state, @@ -150,7 +134,7 @@ describe('Selectors', () => { }, }, }; - expect(getSmartTransactionsEnabled(newState)).toBe(false); + expect(getSmartTransactionsEnabled(newState)).toBe(true); }); it('returns false if feature flag is enabled, not a HW and is Polygon network', () => { @@ -159,10 +143,7 @@ describe('Selectors', () => { ...state, metamask: { ...state.metamask, - providerConfig: { - ...state.metamask.providerConfig, - chainId: CHAIN_IDS.POLYGON, - }, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), }, }; expect(getSmartTransactionsEnabled(newState)).toBe(false); @@ -174,10 +155,7 @@ describe('Selectors', () => { ...state, metamask: { ...state.metamask, - providerConfig: { - ...state.metamask.providerConfig, - chainId: CHAIN_IDS.BSC, - }, + ...mockNetworkState({ chainId: CHAIN_IDS.BSC }), }, }; expect(getSmartTransactionsEnabled(newState)).toBe(false); @@ -189,10 +167,7 @@ describe('Selectors', () => { ...state, metamask: { ...state.metamask, - providerConfig: { - ...state.metamask.providerConfig, - chainId: CHAIN_IDS.LINEA_MAINNET, - }, + ...mockNetworkState({ chainId: CHAIN_IDS.LINEA_MAINNET }), }, }; expect(getSmartTransactionsEnabled(newState)).toBe(false); @@ -286,10 +261,7 @@ describe('Selectors', () => { ...state.metamask.preferences, smartTransactionsOptInStatus: null, }, - providerConfig: { - ...state.metamask.providerConfig, - chainId: CHAIN_IDS.POLYGON, - }, + ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), }, }; expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); @@ -305,13 +277,10 @@ describe('Selectors', () => { ...state.metamask.preferences, smartTransactionsOptInStatus: null, }, - networkConfigurations: { - 'network-configuration-id-1': { - chainId: CHAIN_IDS.MAINNET, - ticker: CURRENCY_SYMBOLS.ETH, - rpcUrl: 'https://mainnet.quiknode.pro/', - }, - }, + ...mockNetworkState({ + chainId: CHAIN_IDS.MAINNET, + rpcUrl: 'https://mainnet.quiknode.pro/', + }), }, }; expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); diff --git a/shared/modules/selectors/index.ts b/shared/modules/selectors/index.ts index 2274b562629b..d53296394c26 100644 --- a/shared/modules/selectors/index.ts +++ b/shared/modules/selectors/index.ts @@ -1,4 +1,7 @@ +import { getHardwareWalletType } from '../../../ui/selectors/selectors'; + export * from './smart-transactions'; export * from './feature-flags'; -export * from './token-auto-detect'; -export * from './nft-auto-detect'; +export * from './account'; + +export { getHardwareWalletType }; diff --git a/shared/modules/selectors/nft-auto-detect.ts b/shared/modules/selectors/nft-auto-detect.ts deleted file mode 100644 index 889a0348f179..000000000000 --- a/shared/modules/selectors/nft-auto-detect.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - getIsMainnet, - getUseNftDetection, -} from '../../../ui/selectors/selectors'; - -type NftAutoDetectionMetaMaskState = { - metamask: { - preferences: { - showNftAutodetectModal: boolean | null; - }; - }; -}; - -export const getShowNftAutodetectModal = ( - state: NftAutoDetectionMetaMaskState, -): boolean | null => { - return state.metamask.preferences?.showNftAutodetectModal; -}; - -export const getIsShowNftAutodetectModal = ( - state: NftAutoDetectionMetaMaskState, -) => { - return ( - !getUseNftDetection(state) && - getIsMainnet(state) && - (getShowNftAutodetectModal(state) === null || - getShowNftAutodetectModal(state) === undefined) - ); -}; diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index 2ee51f652f5d..81c480f94977 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -11,6 +11,8 @@ import { } from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file. import { isProduction } from '../environment'; +import { MultichainState } from '../../../ui/selectors/multichain'; + type SmartTransactionsMetaMaskState = { metamask: { preferences: { @@ -28,9 +30,6 @@ type SmartTransactionsMetaMaskState = { }; }; }; - providerConfig: { - chainId: Hex; - }; swapsState: { swapsFeatureFlags: { ethereum: { @@ -51,7 +50,8 @@ type SmartTransactionsMetaMaskState = { smartTransactionsState: { liveness: boolean; }; - networkConfigurations: { + selectedNetworkClientId: string; + networkConfigurations?: { [key: string]: { chainId: Hex; rpcUrl: string; @@ -97,7 +97,9 @@ const getIsAllowedRpcUrlForSmartTransactions = ( * @returns true if the selected account has a non-zero balance, otherwise false. */ const hasNonZeroBalance = (state: SmartTransactionsMetaMaskState) => { - const selectedAccount = getSelectedAccount(state); + const selectedAccount = getSelectedAccount( + state as unknown as MultichainState, + ); return BigInt(selectedAccount?.balance || '0x0') > 0n; }; diff --git a/shared/modules/selectors/token-auto-detect.ts b/shared/modules/selectors/token-auto-detect.ts deleted file mode 100644 index a6680a47af8d..000000000000 --- a/shared/modules/selectors/token-auto-detect.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getUseTokenDetection } from '../../../ui/selectors/selectors'; - -type TokenAutoDetectionMetaMaskState = { - metamask: { - preferences: { - showTokenAutodetectModal: boolean | null; - }; - showTokenAutodetectModalOnUpgrade: boolean | null; - }; -}; - -export const getShowTokenAutodetectModal = ( - state: TokenAutoDetectionMetaMaskState, -): boolean | null => { - return state.metamask.preferences?.showTokenAutodetectModal; -}; - -export const getIsShowTokenAutodetectModal = ( - state: TokenAutoDetectionMetaMaskState, -) => { - // Upgrade case - if (state.metamask.showTokenAutodetectModalOnUpgrade === null) { - return ( - !getUseTokenDetection(state) && - state.metamask.showTokenAutodetectModalOnUpgrade === null - ); - } - - return ( - !getUseTokenDetection(state) && getShowTokenAutodetectModal(state) === null - ); -}; diff --git a/shared/types/confirm.ts b/shared/types/confirm.ts new file mode 100644 index 000000000000..4dc62234f089 --- /dev/null +++ b/shared/types/confirm.ts @@ -0,0 +1,5 @@ +export type LastInteractedConfirmationInfo = { + id: string; + timestamp: number; + chainId: string; +}; diff --git a/shared/types/transaction-decode.ts b/shared/types/transaction-decode.ts new file mode 100644 index 000000000000..d76c03b1a878 --- /dev/null +++ b/shared/types/transaction-decode.ts @@ -0,0 +1,25 @@ +export enum DecodedTransactionDataSource { + Uniswap = 'Uniswap', + Sourcify = 'Sourcify', + FourByte = 'FourByte', +} + +export type DecodedTransactionDataResponse = { + data: DecodedTransactionDataMethod[]; + source: DecodedTransactionDataSource; +}; + +export type DecodedTransactionDataMethod = { + name: string; + description?: string; + params: DecodedTransactionDataParam[]; +}; + +export type DecodedTransactionDataParam = { + name?: string; + description?: string; + type: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; + children?: DecodedTransactionDataParam[]; +}; diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 55bcb4b26ad0..935a95f849db 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -2,6 +2,7 @@ import { TransactionStatus, TransactionType, } from '@metamask/transaction-controller'; +import { Hex } from '@metamask/utils'; import { Confirmation } from '../../../ui/pages/confirmations/types/confirm'; export const PAYMASTER_AND_DATA = @@ -12,12 +13,13 @@ export const CONTRACT_INTERACTION_SENDER_ADDRESS = export const DEPOSIT_METHOD_DATA = '0xd0e30db0'; -export const genUnapprovedContractInteractionConfirmation = ( - { address, txData } = { - address: CONTRACT_INTERACTION_SENDER_ADDRESS, - txData: DEPOSIT_METHOD_DATA, - }, -): Confirmation => ({ +export const genUnapprovedContractInteractionConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, + txData = DEPOSIT_METHOD_DATA, +}: { + address?: Hex; + txData?: Hex; +} = {}): Confirmation => ({ actionId: String(400855682), chainId: '0xaa36a7', dappSuggestedGasFees: { @@ -151,3 +153,21 @@ export const genUnapprovedContractInteractionConfirmation = ( userFeeLevel: 'medium', verifiedOnBlockchain: false, }); + +export const genUnapprovedApproveConfirmation = ({ + address = CONTRACT_INTERACTION_SENDER_ADDRESS, +}: { + address?: Hex; +} = {}) => ({ + ...genUnapprovedContractInteractionConfirmation(), + txParams: { + from: address, + data: '0x095ea7b30000000000000000000000002e0d7e8c45221fca00d74a3609a0f7097035d09b0000000000000000000000000000000000000000000000000000000000000001', + gas: '0x16a92', + to: '0x076146c765189d51be3160a2140cf80bfc73ad68', + value: '0x0', + maxFeePerGas: '0x5b06b0c0d', + maxPriorityFeePerGas: '0x59682f00', + }, + type: TransactionType.tokenMethodApprove, +}); diff --git a/test/data/confirmations/helper.ts b/test/data/confirmations/helper.ts new file mode 100644 index 000000000000..e2d0a79f95ae --- /dev/null +++ b/test/data/confirmations/helper.ts @@ -0,0 +1,95 @@ +import { ApprovalType } from '@metamask/controller-utils'; +import { + TransactionStatus, + TransactionType, +} from '@metamask/transaction-controller'; + +import mockState from '../mock-state.json'; + +type RootState = { metamask: Record } & Record< + string, + unknown +>; + +export const getExampleMockSignatureConfirmState = ( + args: RootState = { metamask: {} }, +) => ({ + ...mockState, + ...args, + metamask: { + ...mockState.metamask, + preferences: { + redesignedTransactionsEnabled: true, + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + pendingApprovals: { + '123': { + id: '123', + type: ApprovalType.EthSignTypedData, + }, + }, + unapprovedTypedMessages: { + '123': { + id: '123', + chainId: + mockState.metamask.networkConfigurations.testNetworkConfigurationId + .chainId, + type: TransactionType.signTypedData, + status: TransactionStatus.unapproved, + txParams: { from: Object.keys(mockState.metamask.identities)[0] }, + msgParams: { + signatureMethod: 'eth_signTypedData_v4', + }, + }, + }, + ...args.metamask, + }, +}); + +export const getExampleMockContractInteractionConfirmState = ( + args: RootState = { metamask: {} }, +) => ({ + ...mockState, + ...args, + metamask: { + ...mockState.metamask, + preferences: { + redesignedTransactionsEnabled: true, + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + pendingApprovals: { + '123': { + id: '123', + type: ApprovalType.Transaction, + }, + }, + transactions: [ + { + id: '123', + type: TransactionType.contractInteraction, + chainId: + mockState.metamask.networkConfigurations.testNetworkConfigurationId + .chainId, + status: TransactionStatus.unapproved, + txParams: { from: Object.keys(mockState.metamask.identities)[0] }, + }, + ], + ...args.metamask, + }, +}); + +export const getMockConfirmState = (args: RootState = { metamask: {} }) => ({ + ...mockState, + ...args, + metamask: { + ...mockState.metamask, + preferences: { + redesignedTransactionsEnabled: true, + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + ...args.metamask, + }, +}); diff --git a/test/data/confirmations/transaction-decode.ts b/test/data/confirmations/transaction-decode.ts new file mode 100644 index 000000000000..e9700ababff3 --- /dev/null +++ b/test/data/confirmations/transaction-decode.ts @@ -0,0 +1,604 @@ +import { DecodedTransactionDataSource } from '../../../shared/types/transaction-decode'; + +export const CONTRACT_ADDRESS_UNISWAP = '0x1'; +export const CONTRACT_ADDRESS_SOURCIFY = '0x2'; +export const CONTRACT_ADDRESS_FOUR_BYTE = '0x3'; + +export const TRANSACTION_DATA_UNISWAP = + '0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006679b4bb00000000000000000000000000000000000000000000000000000000000000040b000604000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000005af3107a40000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000004c41800000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000027213e28d7fda5c57fe9e5dd923818dbccf71c4700000000000000000000000000000000000000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000004c418'; + +export const TRANSACTION_DATA_SOURCIFY = + '0xa9059cbb000000000000000000000000ec8507ecf7e946992294f06423a79835a32268460000000000000000000000000000000000000000000000000000000000000064'; + +export const TRANSACTION_DATA_SOURCIFY_NESTED = + '0x2a2d80d1000000000000000000000000be3be93ffad7d417c08124b43286f4476c006afe000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000cc97f2e548ab94f40e5adf473f596ced83b6ee0a0000000000000000000000000000000000000000000000000000019747f66fc300000000000000000000000000000000000000000000000000000000000000030000000000000000000000000305f515fa978cf87226cf8a9776d25bcfb2cc0b000000000000000000000000ffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000019747f66fc300000000000000000000000000000000000000000000000000000000000000000000000000000000000000004385328cc4d643ca98dfea734360c0f596c83449000000000000000000000000ffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000019747f66fc30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000ffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000019747f66fc30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004156e1fabfaf96d309b0039896d1f68b51e27f0e25b4481db8cad059b7e8db95d918bdc43ab1d03a0b6a84c7ea2219bf01133ceab4e7ca4e38055e4ed8af78a63b1b00000000000000000000000000000000000000000000000000000000000000'; + +export const TRANSACTION_DATA_FOUR_BYTE = + '0x12345678000000000000000000000000ec8507ecf7e946992294f06423a79835a32268460000000000000000000000000000000000000000000000000000000000000064'; + +export const TRANSACTION_DECODE_UNISWAP = { + source: DecodedTransactionDataSource.Uniswap, + data: [ + { + name: 'WRAP_ETH', + params: [ + { + name: 'recipient', + type: 'address', + value: '0x0000000000000000000000000000000000000002', + description: 'The recipient of the WETH', + children: undefined, + }, + { + name: 'amountMin', + type: 'uint256', + value: 123456, + description: 'The amount of ETH to wrap', + children: undefined, + }, + ], + }, + { + name: 'V3_SWAP_EXACT_IN', + params: [ + { + name: 'recipient', + type: 'address', + value: '0x0000000000000000000000000000000000000002', + description: 'The recipient of the output of the trade', + children: undefined, + }, + { + name: 'amountIn', + type: 'uint256', + value: 123456, + description: 'The amount of input tokens for the trade', + children: undefined, + }, + { + name: 'amountOutMin', + type: 'uint256', + value: 123456, + description: 'The minimum amount of output tokens the user wants', + children: undefined, + }, + { + name: 'path', + type: 'bytes', + value: [ + { + firstAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tickSpacing: 500, + secondAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }, + { + firstAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + tickSpacing: 100, + secondAddress: '0xd02aaa39b223fe8d0a0e5c4f27ead9083c756cc3', + }, + ], + description: 'The UniswapV3 encoded path to trade along', + children: undefined, + }, + { + name: 'payerIsUser', + type: 'bool', + value: false, + description: + 'A flag for whether the input tokens should come from the msg.sender (through Permit2) or whether the funds are already in the UniversalRouter', + children: undefined, + }, + ], + }, + { + name: 'PAY_PORTION', + params: [ + { + name: 'token', + type: 'address', + value: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + description: 'The ERC20 token to transfer (or Constants.ETH for ETH)', + children: undefined, + }, + { + name: 'recipient', + type: 'address', + value: '0x27213E28D7fDA5c57Fe9e5dD923818DBCcf71c47', + description: 'The recipient of the transfer', + children: undefined, + }, + { + name: 'bips', + type: 'uint256', + value: 123456, + description: + 'In basis points, the percentage of the contract’s balance to transfer', + children: undefined, + }, + ], + }, + { + name: 'SWEEP', + params: [ + { + name: 'token', + type: 'address', + value: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + description: 'The ERC20 token to sweep (or Constants.ETH for ETH)', + children: undefined, + }, + { + name: 'recipient', + type: 'address', + value: '0x0000000000000000000000000000000000000001', + description: 'The recipient of the sweep', + children: undefined, + }, + { + name: 'amountMin', + type: 'uint256', + value: 123456, + description: 'The minimum required tokens to receive from the sweep', + children: undefined, + }, + ], + }, + ], +}; + +export const TRANSACTION_DECODE_SOURCIFY = { + source: DecodedTransactionDataSource.Sourcify, + data: [ + { + name: 'cancelAuthorization', + description: 'Attempt to cancel an authorization', + params: [ + { + name: 'authorizer', + description: "Authorizer's address", + type: 'address', + value: '0xB0dA5965D43369968574D399dBe6374683773a65', + children: undefined, + }, + { + name: 'nonce', + description: 'Nonce of the authorization', + type: 'bytes32', + value: + '0x0000000000000000000000000000000000000000000000000000000000000123', + children: undefined, + }, + { + name: 'signature', + description: + 'Signature bytes signed by an EOA wallet or a contract wallet', + type: 'bytes', + value: '0x0456', + children: undefined, + }, + ], + }, + ], +}; + +export const TRANSACTION_DECODE_FOUR_BYTE = { + source: DecodedTransactionDataSource.FourByte, + data: [ + { + name: 'someFunction', + params: [ + { + children: undefined, + type: 'uint256', + value: 123456, + }, + { + children: undefined, + type: 'address', + value: '0x1234567890123456789012345678901234567890', + }, + { + children: undefined, + type: 'bytes', + value: '0x123', + }, + ], + }, + ], +}; + +export const TRANSACTION_DECODE_NESTED = { + source: DecodedTransactionDataSource.Sourcify, + data: [ + { + description: + "Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature", + name: 'permit', + params: [ + { + children: undefined, + description: 'The owner of the tokens being approved', + name: 'owner', + type: 'address', + value: '0xBe3be93fFAD7d417C08124B43286f4476C006AFe', + }, + { + children: [ + { + children: [ + { + children: [ + { + children: undefined, + description: undefined, + name: 'token', + type: 'address', + value: '0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B', + }, + { + children: undefined, + description: undefined, + name: 'amount', + type: 'uint160', + value: '0xffffffffffffffffffffffffffffffffffffffff', + }, + { + children: undefined, + description: undefined, + name: 'expiration', + type: 'uint48', + value: 1749259022275, + }, + { + children: undefined, + description: undefined, + name: 'nonce', + type: 'uint48', + value: 0, + }, + ], + description: undefined, + name: 'Item 1', + type: 'tuple', + value: [ + '0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + }, + { + children: [ + { + children: undefined, + description: undefined, + name: 'token', + type: 'address', + value: '0x4385328cc4D643Ca98DfEA734360C0F596C83449', + }, + { + children: undefined, + description: undefined, + name: 'amount', + type: 'uint160', + value: '0xffffffffffffffffffffffffffffffffffffffff', + }, + { + children: undefined, + description: undefined, + name: 'expiration', + type: 'uint48', + value: 1749259022275, + }, + { + children: undefined, + description: undefined, + name: 'nonce', + type: 'uint48', + value: 0, + }, + ], + description: undefined, + name: 'Item 2', + type: 'tuple', + value: [ + '0x4385328cc4D643Ca98DfEA734360C0F596C83449', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + }, + { + children: [ + { + children: undefined, + description: undefined, + name: 'token', + type: 'address', + value: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + }, + { + children: undefined, + description: undefined, + name: 'amount', + type: 'uint160', + value: '0xffffffffffffffffffffffffffffffffffffffff', + }, + { + children: undefined, + description: undefined, + name: 'expiration', + type: 'uint48', + value: 1749259022275, + }, + { + children: undefined, + description: undefined, + name: 'nonce', + type: 'uint48', + value: 0, + }, + ], + description: undefined, + name: 'Item 3', + type: 'tuple', + value: [ + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + }, + ], + description: undefined, + name: 'details', + type: 'tuple[]', + value: [ + [ + '0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + [ + '0x4385328cc4D643Ca98DfEA734360C0F596C83449', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + [ + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + ], + }, + { + children: undefined, + description: undefined, + name: 'spender', + type: 'address', + value: '0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a', + }, + { + children: undefined, + description: undefined, + name: 'sigDeadline', + type: 'uint256', + value: '0x019747f66fc3', + }, + ], + description: + 'Data signed over by the owner specifying the terms of approval', + name: 'permitBatch', + type: 'tuple', + value: [ + [ + [ + '0x0305f515fa978cf87226cf8A9776D25bcfb2Cc0B', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + [ + '0x4385328cc4D643Ca98DfEA734360C0F596C83449', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + [ + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + '0xffffffffffffffffffffffffffffffffffffffff', + 1749259022275, + 0, + ], + ], + '0xCC97F2E548ab94F40e5ADf473F596CEd83B6ee0a', + '0x019747f66fc3', + ], + }, + { + children: undefined, + description: "The owner's signature over the permit data", + name: 'signature', + type: 'bytes', + value: + '0x56e1fabfaf96d309b0039896d1f68b51e27f0e25b4481db8cad059b7e8db95d918bdc43ab1d03a0b6a84c7ea2219bf01133ceab4e7ca4e38055e4ed8af78a63b1b', + }, + ], + }, + ], +}; + +export const SOURCIFY_RESPONSE = { + files: [ + { + name: 'metadata.json', + content: JSON.stringify({ + output: { + abi: [ + { + constant: false, + inputs: [ + { + name: 'to', + type: 'address', + }, + { + name: 'value', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + name: 'success', + type: 'bool', + }, + ], + payable: false, + type: 'function', + }, + ], + userdoc: { + methods: { + 'transfer(address,uint256)': { + notice: 'Transfer tokens', + params: { + to: 'The address to transfer to', + value: 'The amount to transfer', + }, + }, + }, + }, + }, + }), + }, + ], +}; + +export const SOURCIFY_RESPONSE_NESTED = { + files: [ + { + name: 'metadata.json', + content: JSON.stringify({ + output: { + abi: [ + { + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { + internalType: 'uint160', + name: 'amount', + type: 'uint160', + }, + { + internalType: 'uint48', + name: 'expiration', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'nonce', + type: 'uint48', + }, + ], + internalType: 'struct IAllowanceTransfer.PermitDetails[]', + name: 'details', + type: 'tuple[]', + }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'sigDeadline', + type: 'uint256', + }, + ], + internalType: 'struct IAllowanceTransfer.PermitBatch', + name: 'permitBatch', + type: 'tuple', + }, + { + internalType: 'bytes', + name: 'signature', + type: 'bytes', + }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ], + devdoc: { + methods: { + 'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)': + { + details: + "May fail if the owner's nonce was invalidated in-flight by invalidateNonce", + params: { + owner: 'The owner of the tokens being approved', + permitBatch: + 'Data signed over by the owner specifying the terms of approval', + signature: "The owner's signature over the permit data", + }, + }, + }, + }, + userdoc: { + methods: { + 'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)': + { + notice: + "Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature", + }, + }, + notice: + 'Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer.', + }, + }, + }), + }, + ], +}; + +export const FOUR_BYTE_RESPONSE = { + results: [ + { + created_at: '2022-09-01T00:00:00.000Z', + text_signature: 'someFunction(address,uint256)', + }, + { + created_at: '2021-09-01T00:00:00.000Z', + text_signature: 'someOtherFunction(address,uint256)', + }, + ], +}; + +export const FOUR_BYTE_RESPONSE_NESTED = { + results: [ + { + created_at: '2022-09-01T00:00:00.000Z', + text_signature: + 'permit(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)', + }, + ], +}; diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index 61e6fa68713d..0a05a6716432 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -136,6 +136,25 @@ export const unapprovedTypedSignMsgV4 = { }, } as SignatureRequestType; +export const orderSignatureMsg = { + id: 'e5249ae0-4b6b-11ef-831f-65b48eb489ec', + securityAlertResponse: { + result_type: 'loading', + reason: 'validation_in_progress', + securityAlertId: 'dadfc03d-43f9-4515-9aa2-cb00715c3e07', + }, + status: 'unapproved', + time: 1722011224974, + type: 'eth_signTypedData', + msgParams: { + data: '{"types":{"Order":[{"type":"uint8","name":"direction"},{"type":"address","name":"maker"},{"type":"address","name":"taker"},{"type":"uint256","name":"expiry"},{"type":"uint256","name":"nonce"},{"type":"address","name":"erc20Token"},{"type":"uint256","name":"erc20TokenAmount"},{"type":"Fee[]","name":"fees"},{"type":"address","name":"erc721Token"},{"type":"uint256","name":"erc721TokenId"},{"type":"Property[]","name":"erc721TokenProperties"}],"Fee":[{"type":"address","name":"recipient"},{"type":"uint256","name":"amount"},{"type":"bytes","name":"feeData"}],"Property":[{"type":"address","name":"propertyValidator"},{"type":"bytes","name":"propertyData"}],"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}]},"domain":{"name":"ZeroEx","version":"1.0.0","chainId":"0x1","verifyingContract":"0xdef1c0ded9bec7f1a1670819833240f027b25eff"},"primaryType":"Order","message":{"direction":"0","maker":"0x8eeee1781fd885ff5ddef7789486676961873d12","taker":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","expiry":"2524604400","nonce":"100131415900000000000000000000000000000083840314483690155566137712510085002484","erc20Token":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","erc20TokenAmount":"42000000000000","fees":[],"erc721Token":"0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e","erc721TokenId":"2516","erc721TokenProperties":[]}}', + from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + signatureMethod: 'eth_signTypedData_v4', + origin: 'https://metamask.github.io', + }, +}; + export const permitSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', securityAlertResponse: { diff --git a/test/data/mock-data.js b/test/data/mock-data.js index b39b2305cfdd..f42327497b67 100644 --- a/test/data/mock-data.js +++ b/test/data/mock-data.js @@ -429,6 +429,291 @@ const TRADES_API_MOCK_RESULT = [ }, ]; +const SWAP_TEST_ETH_USDC_TRADES_MOCK = [ + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000001c616972737761704c696768743446656544796e616d696346697865640000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000190c020b8bd00000000000000000000000000000000000000000000000000000000669e592c00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000034ed3100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000000001b7e0912a9d133a8b79f7213ecb9c3754f4e7b5ed6afc5d3897451660ecfc64dd8645690a525c41c903efa55e3ff3a35e94e3e9d214481c26072c7bf22a09e38ee000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000001fe", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3468593", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 300000, + averageGas: 150000, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 845, + aggregator: "airswapV4", + aggType: "RFQ", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 1.008542852896115, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.468593, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.000995894893460576, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.000995894893460576 + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f6e65496e6368563546656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034a5600000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f1915000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c8e449022e0000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034a55f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000002c000000000000000000000006ce6d6d40a4c4088309293b0582372a2e6bb632e8000000000000000000000008592064903ef23d34e4d5aaaed40abf6d96af1867dcbea7c000000000000000000000000000000000000000000000000014e", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3520620", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 584, + aggregator: "oneInchV5", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.06, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9936715373533649, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.52062, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001010832772774198, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001010832772774198 + } + }, + { + trade: null, + hasRoute: false, + maxGas: 2750000, + averageGas: 637198, + estimatedRefund: 0, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + sourceAmount: "1000000000000000", + destinationAmount: null, + error: "Request failed with status code 403", + approvalNeeded: null, + fetchTime: 121, + aggregator: "paraswap", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.05, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: null, + hasRoute: false, + maxGas: 405000, + averageGas: 209421, + estimatedRefund: 0, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + sourceAmount: "1000000000000000", + destinationAmount: null, + error: "Cannot read properties of undefined (reading '0xv4order')", + approvalNeeded: null, + fetchTime: 108, + aggregator: "pmm", + aggType: "RFQ", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000c307846656544796e616d6963000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000033b4150000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f191500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000128d9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000033b41500000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48869584cd00000000000000000000000011ededebf63bef0ea2d2d071bdf88f71543ec6fb00000000000000000000000000000000000000004b3fbadd405c612525d4c81900000000000000000000000000000000000000000000000001f5", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3457589", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1650000, + averageGas: 411000, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 215, + aggregator: "zeroEx", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.2, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 1.0116882188836294, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.457589, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.000992735448865134, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.000992735448865134 + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136f70656e4f6365616e46656544796e616d69630000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000003472230000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f19150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000088490411a32000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a00000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000003858960223400000000000000000000000000000000000000000000000000000000000034722400000000000000000000000000000000000000000000000000000000003584240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ef53a4bd0e16ccc9116770a41c4bd3ad1147bd4f00000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000100000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000004e451a74316000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000001000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000064eb5625d9000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c452bbbe2900000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000a9c0cded336699547aac4f9de5a11ada979bc59a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000074de5d4fcbf63e00296fd95d33236b97940166310000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffff6f0ed6f346007563d3266de350d174a831bde0ca0001000000000000000005db0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000044000000000000000000000000000000000000000000000000000000000000016400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000182", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3507236", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 1832, + aggregator: "openOcean", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9974971987834718, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.507236, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001006989987744627, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001006989987744627 + } + }, + { + trade: null, + hasRoute: false, + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + sourceToken: 0x0000000000000000000000000000000000000000, + destinationToken: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, + sourceAmount: 1000000000000000, + destinationAmount: null, + error: "Request failed with status code 403", + approvalNeeded: null, + fetchTime: 33, + aggregator: "hashFlow", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: + { + ratio: null, + calculationError: "No trade data to calculate price slippage", + bucket: "high", + sourceAmountInUSD: null, + destinationAmountInUSD: null, + sourceAmountInNativeCurrency: null, + destinationAmountInNativeCurrency: null, + sourceAmountInETH: null, + destinationAmountInETH: null + } + }, + { + trade: { + data: "0x5f5755290000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000136b796265725377617046656544796e616d6963000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000385896022340000000000000000000000000000000000000000000000000000000000003860210000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000007f544a44c00000000000000000000000000f326e4de8f66a0bdc0970b79e0924e33c79f191500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b44e21fd0e90000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000008c00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000074de5d4fcbf63e00296fd95d33236b979401663100000000000000000000000000000000000000000000000000000000669e5d4100000000000000000000000000000000000000000000000000000000000005a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000000400e00deaa0000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000002aa3041fe813cfe572969216c6843c33f14f9194000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c59900000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000000040ca6182da00000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000007a819fa46734a49d0112796f9377e024c350fb260000000000000000000000000000000000000000669e58be8ddca21b4e2a8e860000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000807cf9a772d5a3f9cefbc1192e939d62f0d9bd380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000316439e530f4a3c0000000000000000000000000000000000000000000000000000000000000149500000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000001495000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd670000000000000000000000000000000000000000000000000000000000000041f8b567a2d7c3e67633821bc6d207b655cb8c65bb94a0ceda4e3cca76c040ae9015be7ee2c63c80506271283fa5be190f00ba64c10aa2abefc9b4fd012ad4654c1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400e00deaa000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080000000000000000000000000ec577a919fca1b682f584a50b1048331ef0f30dd0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000316439e530f4a3c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000003000000000000000000000000003986aa000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000074de5d4fcbf63e00296fd95d33236b979401663100000000000000000000000000000000000000000000000000038589602234000000000000000000000000000000000000000000000000000000000000386021000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002267b22536f75726365223a226d6574616d61736b222c22416d6f756e74496e555344223a22332e34363835383836343432363131323336222c22416d6f756e744f7574555344223a22332e37353438323837323633373238333935222c22526566657272616c223a22222c22466c616773223a302c22416d6f756e744f7574223a2233373730303236222c2254696d657374616d70223a313732313635333339332c22496e74656772697479496e666f223a7b224b65794944223a2231222c225369676e6174757265223a224779574558562f6454475354315036694a7347522f366d502f755953554d6e5669664e536e52657a505a546471734976366c5939345a564c45587952393442534359427376452f396a4f4c427a474d77564d32473248766f4e707a39644c2f2f646a534b576f4d354a4558694664524c73354277547137416b7656625653677155726e3930756d6950684854386857303448612f68626647584e59784a38474f445557726d4e366a2b714d5a4656434d59305a2b475666596646464830797575724a554e794d507a38434f2b524c5a374f586e594f4b3344555275784f366e76424248463451304c4577696450597a41365331416f4c69504a577a4e77366177594e644d486b687a74464644303431384c664430533630517031306947744470726e6c59725761633275466e356d696c546a735157442b4f4f6b797a4a78576b7450333545464266436f555449452f5a58524d3345513d3d227d7d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115", + from: "0x8fcd29a7887f82463e0ea7332cb1ce431a4430f7", + value: "1000000000000000", + to: "0x881D40237659C251811CEC9c364ef91dC08D300C" + }, + hasRoute: false, + sourceAmount: "1000000000000000", + destinationAmount: "3770026", + error: null, + sourceToken: "0x0000000000000000000000000000000000000000", + destinationToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + maxGas: 1000000, + averageGas: 560305, + estimatedRefund: 0, + approvalNeeded: null, + fetchTime: 824, + aggregator: "kyberSwap", + aggType: "AGG", + fee: 0.875, + quoteRefreshSeconds: 30, + gasMultiplier: 1.1, + sourceTokenRate: 1, + destinationTokenRate: 0.00028711782946588897, + priceSlippage: { + ratio: 0.9223817200612836, + calculationError: "", + bucket: "low", + sourceAmountInUSD: 3.49848, + destinationAmountInUSD: 3.770026, + sourceAmountInNativeCurrency: 0.001, + destinationAmountInNativeCurrency: 0.001082441682149968, + sourceAmountInETH: 0.001, + destinationAmountInETH: 0.001082441682149968 + } + } +] + const NETWORKS_2_API_MOCK_RESULT = { active: true, networkId: 1, @@ -482,5 +767,6 @@ module.exports = { GAS_PRICE_API_MOCK_RESULT, FEATURE_FLAGS_API_MOCK_RESULT, TRADES_API_MOCK_RESULT, + SWAP_TEST_ETH_USDC_TRADES_MOCK, NETWORKS_2_API_MOCK_RESULT, }; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 5beee30824ce..09b9cad15757 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -64,9 +64,6 @@ "metamask": { "ipfsGateway": "", "dismissSeedBackUpReminder": false, - "disabledRpcMethodPreferences": { - "eth_sign": false - }, "usePhishDetect": true, "participateInMetaMetrics": false, "gasEstimateType": "fee-market", @@ -147,17 +144,17 @@ "0x539": true }, "showTestnetMessageInDropdown": true, - "networkConfigurations": {}, + "networkConfigurations": { + "goerli": { + "id": "goerli", + "chainId": "0x5" + } + }, "alertEnabledness": { "unconnectedAccount": true }, "featureFlags": {}, "network": "5", - "providerConfig": { - "type": "rpc", - "chainId": "0x5", - "ticker": "ETH" - }, "internalAccounts": { "accounts": { "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3": { @@ -173,7 +170,6 @@ "methods": [ "personal_sign", "eth_sendTransaction", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v2", @@ -195,7 +191,6 @@ "methods": [ "personal_sign", "eth_sendTransaction", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v2", @@ -217,7 +212,6 @@ "methods": [ "personal_sign", "eth_sendTransaction", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v2", @@ -239,7 +233,6 @@ "methods": [ "personal_sign", "eth_sendTransaction", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v2", @@ -393,8 +386,6 @@ } } }, - "unapprovedMsgs": {}, - "unapprovedMsgCount": 0, "unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgCount": 0, "unapprovedDecryptMsgs": {}, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 9e595ad8f0e0..93af077c77b1 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -366,6 +366,7 @@ }, "preferences": { "hideZeroBalanceTokens": false, + "isRedesignedConfirmationsDeveloperEnabled": false, "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": true, @@ -380,13 +381,6 @@ "unconnectedAccount": true }, "featureFlags": {}, - "providerConfig": { - "type": "rpc", - "nickname": "goerli", - "chainId": "0x5", - "ticker": "ETH", - "id": "chain5" - }, "networkConfigurations": { "testNetworkConfigurationId": { "rpcUrl": "https://testrpc.com", @@ -395,12 +389,12 @@ "type": "rpc", "id": "testNetworkConfigurationId" }, - "chain5": { + "goerli": { "type": "rpc", "chainId": "0x5", "ticker": "ETH", "nickname": "Chain 5", - "id": "chain5" + "id": "goerli" } }, "internalAccounts": { @@ -417,7 +411,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -437,7 +430,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -457,7 +449,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -477,7 +468,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -497,7 +487,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -521,7 +510,6 @@ "options": {}, "methods": [ "personal_sign", - "eth_sign", "eth_signTransaction", "eth_signTypedData_v1", "eth_signTypedData_v3", @@ -532,6 +520,7 @@ }, "selectedAccount": "cf8dace4-9439-4bd4-b3a8-88c821c8fcb3" }, + "balances": {}, "keyrings": [ { "type": "HD Key Tree", @@ -780,8 +769,6 @@ } } }, - "unapprovedMsgs": {}, - "unapprovedMsgCount": 0, "unapprovedPersonalMsgs": {}, "unapprovedPersonalMsgCount": 0, "unapprovedDecryptMsgs": {}, @@ -1879,6 +1866,9 @@ } ], "addSnapAccountEnabled": false, + "watchEthereumAccountEnabled": false, + "bitcoinSupportEnabled": false, + "bitcoinTestnetSupportEnabled": false, "pendingApprovals": { "testApprovalId": { "id": "testApprovalId", @@ -1941,6 +1931,54 @@ } } } + }, + "subjects": { + "npm:@metamask/test-snap-bip44": { + "origin": "npm:@metamask/test-snap-bip44", + "permissions": { + "endowment:rpc": { + "caveats": [ + { + "type": "rpcOrigin", + "value": { + "allowedOrigins": ["npm:@metamask/json-rpc-example-snap"], + "dapps": true + } + } + ], + "date": 1718117256761, + "id": "MhjpHKQFfGpMzI6YzkPGU", + "invoker": "npm:@metamask/test-snap-bip44", + "parentCapability": "endowment:rpc" + }, + "snap_dialog": { + "caveats": null, + "date": 1718117256761, + "id": "sBxmdvnow7QiN9aS4uSdn", + "invoker": "npm:@metamask/test-snap-bip44", + "parentCapability": "snap_dialog" + }, + "snap_getBip44Entropy": { + "caveats": [ + { + "type": "permittedCoinTypes", + "value": [ + { + "coinType": 1 + }, + { + "coinType": 3 + } + ] + } + ], + "date": 1718117256762, + "id": "R9tggB2pDzyCcbt6dIebN", + "invoker": "npm:@metamask/test-snap-bip44", + "parentCapability": "snap_getBip44Entropy" + } + } + } } }, "ramps": { diff --git a/test/e2e/accounts/account-custom-name.spec.ts b/test/e2e/accounts/account-custom-name.spec.ts index cdab01cfe826..ca9e466e0b7f 100644 --- a/test/e2e/accounts/account-custom-name.spec.ts +++ b/test/e2e/accounts/account-custom-name.spec.ts @@ -45,7 +45,10 @@ describe('Account Custom Name Persistence', function (this: Suite) { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', anotherAccountLabel); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElementAndWaitToDisappear({ + text: 'Add account', + tag: 'button', + }); await locateAccountBalanceDOM(driver); // Verify initial custom account label after freshly added account was active diff --git a/test/e2e/accounts/common.ts b/test/e2e/accounts/common.ts index f4039d6b6533..d25983d0a816 100644 --- a/test/e2e/accounts/common.ts +++ b/test/e2e/accounts/common.ts @@ -1,4 +1,5 @@ import { privateToAddress } from 'ethereumjs-util'; +import messages from '../../../app/_locales/en/messages.json'; import FixtureBuilder from '../fixture-builder'; import { PRIVATE_KEY, @@ -10,7 +11,6 @@ import { validateContractDetails, multipleGanacheOptions, regularDelayMs, - openDapp, } from '../helpers'; import { Driver } from '../webdriver/driver'; import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; @@ -19,7 +19,6 @@ import { retry } from '../../../development/lib/retry'; /** * These are fixtures specific to Account Snap E2E tests: * -- connected to Test Dapp - * -- eth_sign enabled * -- two private keys with 25 ETH each * * @param title @@ -29,11 +28,6 @@ export const accountSnapFixtures = (title: string | undefined) => { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp(false) - .withPreferencesController({ - disabledRpcMethodPreferences: { - eth_sign: true, - }, - }) .build(), ganacheOptions: multipleGanacheOptions, title, @@ -85,6 +79,9 @@ export async function installSnapSimpleKeyring( tag: 'button', }); + // Wait until popup is closed before proceeding + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.SnapSimpleKeyringDapp); await driver.waitForSelector({ @@ -122,6 +119,12 @@ export async function importKeyAndSwitch(driver: Driver) { css: '[data-testid="confirmation-submit-button"]', text: 'Create', }); + // Click the add account button on the naming modal + await driver.clickElement({ + css: '[data-testid="submit-add-account-with-name"]', + text: 'Add account', + }); + // Click the ok button on the success modal await driver.clickElement({ css: '[data-testid="confirmation-submit-button"]', text: 'Ok', @@ -148,6 +151,12 @@ export async function makeNewAccountAndSwitch(driver: Driver) { css: '[data-testid="confirmation-submit-button"]', text: 'Create', }); + // Click the add account button on the naming modal + await driver.clickElement({ + css: '[data-testid="submit-add-account-with-name"]', + text: 'Add account', + }); + // Click the ok button on the success modal await driver.clickElement({ css: '[data-testid="confirmation-submit-button"]', text: 'Ok', @@ -174,7 +183,7 @@ async function switchToAccount2(driver: Driver) { await driver.clickElement({ tag: 'Button', - text: 'Snap Account 1', + text: 'SSK Account', }); await driver.assertElementNotPresent({ @@ -184,20 +193,8 @@ async function switchToAccount2(driver: Driver) { } export async function connectAccountToTestDapp(driver: Driver) { - try { - // Do an unusually fast switchToWindowWithTitle, just 1 second - await driver.switchToWindowWithTitle( - WINDOW_TITLES.TestDApp, - null, - 1000, - 1000, - ); - } catch { - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - await openDapp(driver); - } + await switchToOrOpenDapp(driver); + await driver.clickElement('#connectButton'); await driver.delay(regularDelayMs); @@ -317,7 +314,7 @@ export async function signData( await validateContractDetails(driver); } - await clickSignOnSignatureConfirmation({ driver, locatorID }); + await clickSignOnSignatureConfirmation({ driver }); if (isAsyncFlow) { await driver.delay(2000); @@ -372,3 +369,24 @@ export async function signData( }); } } + +export async function createBtcAccount(driver: Driver) { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ + text: messages.addNewBitcoinAccount.message, + tag: 'button', + }); + await driver.clickElementAndWaitToDisappear( + { + text: 'Add account', + tag: 'button', + }, + // Longer timeout than usual, this reduces the flakiness + // around Bitcoin account creation (mainly required for + // Firefox) + 5000, + ); +} diff --git a/test/e2e/accounts/create-snap-account.spec.ts b/test/e2e/accounts/create-snap-account.spec.ts index ab34ac0046c0..2a35b4b4c805 100644 --- a/test/e2e/accounts/create-snap-account.spec.ts +++ b/test/e2e/accounts/create-snap-account.spec.ts @@ -1,14 +1,48 @@ import { Suite } from 'mocha'; + import FixtureBuilder from '../fixture-builder'; -import { - WINDOW_TITLES, - defaultGanacheOptions, - switchToNotificationWindow, - unlockWallet, - withFixtures, -} from '../helpers'; +import { defaultGanacheOptions, WINDOW_TITLES, withFixtures } from '../helpers'; import { Driver } from '../webdriver/driver'; -import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../constants'; +import { installSnapSimpleKeyring } from './common'; + +/** + * Starts the flow to create a Snap account, including unlocking the wallet, + * connecting to the test Snaps page, installing the Snap, and initiating the + * create account process on the dapp. The function ends with switching to the + * first confirmation in the extension. + * + * @param driver - The WebDriver instance used to control the browser. + * @returns A promise that resolves when the setup steps are complete. + */ +async function startCreateSnapAccountFlow(driver: Driver): Promise { + await installSnapSimpleKeyring(driver, false); + + // move back to the Snap window to test the create account flow + await driver.waitAndSwitchToWindowWithTitle( + 2, + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + + // check the dapp connection status + await driver.waitForSelector({ + css: '#snapConnected', + text: 'Connected', + }); + + // create new account on dapp + await driver.clickElement({ + text: 'Create account', + tag: 'div', + }); + + await driver.clickElement({ + text: 'Create Account', + tag: 'button', + }); + + // Wait until dialog is opened before proceeding + await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); +} describe('Create Snap Account', function (this: Suite) { it('create Snap account popup contains correct Snap name and snapId', async function () { @@ -19,58 +53,8 @@ describe('Create Snap Account', function (this: Suite) { title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - - // navigate to test Snaps page and connect - await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); - await driver.clickElement('#connectButton'); - - // switch to metamask extension and click connect to start installing the snap - await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - // scroll to the bottom of the page - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); - - // click the install button to install the snap - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ - text: 'OK', - tag: 'button', - }); - - // move back to the Snap window to test the create account flow - await driver.switchToWindowWithTitle( - WINDOW_TITLES.SnapSimpleKeyringDapp, - ); - - // check the dapp connection status - await driver.waitForSelector({ - css: '#snapConnected', - text: 'Connected', - }); - - // create new account on dapp - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); - - await driver.clickElement({ - text: 'Create Account', - tag: 'button', - }); - - await switchToNotificationWindow(driver); + // start the create account flow and switch to dialog window + await startCreateSnapAccountFlow(driver); await driver.findElement({ css: '[data-testid="confirmation-submit-button"]', @@ -98,92 +82,183 @@ describe('Create Snap Account', function (this: Suite) { title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); + // start the create account flow and switch to dialog window + await startCreateSnapAccountFlow(driver); - // navigate to test Snaps page and connect - await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); - await driver.clickElement('#connectButton'); - - // switch to metamask extension and click connect to start installing the snap - await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); + // click the create button on the confirmation modal + await driver.clickElement('[data-testid="confirmation-submit-button"]'); - // scroll to the bottom of the page - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // click the add account button on the naming modal + await driver.clickElement( + '[data-testid="submit-add-account-with-name"]', + ); - // click the install button to install the snap - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', + // success screen should show account created with the snap suggested name + await driver.findElement({ + tag: 'h3', + text: 'Account created', }); - await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ - text: 'OK', - tag: 'button', + await driver.findElement({ + css: '.multichain-account-list-item__account-name__button', + text: 'SSK Account', }); - // move back to the Snap window to test the create account flow - await driver.switchToWindowWithTitle( + // click the okay button + await driver.clickElement('[data-testid="confirmation-submit-button"]'); + + // switch back to the test dapp/Snap window + await driver.waitAndSwitchToWindowWithTitle( + 2, WINDOW_TITLES.SnapSimpleKeyringDapp, ); - // check the dapp connection status - await driver.waitForSelector({ - css: '#snapConnected', - text: 'Connected', + // account should be created on the dapp + await driver.findElement({ + tag: 'p', + text: 'Successful request', }); - // create new account on dapp - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); + // switch to extension full screen view + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); - await driver.clickElement({ - text: 'Create Account', - tag: 'button', + // account should be created with the snap suggested name + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'SSK Account', }); + }, + ); + }); - await switchToNotificationWindow(driver); + it('creates multiple Snap accounts with increasing numeric suffixes', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await installSnapSimpleKeyring(driver, false); + + const expectedNames = ['SSK Account', 'SSK Account 2', 'SSK Account 3']; + + for (const [index, expectedName] of expectedNames.entries()) { + // move to the dapp window + await driver.waitAndSwitchToWindowWithTitle( + 2, + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + + // create new account on dapp + if (index === 0) { + // Only click the div for the first snap account creation + await driver.clickElement({ + text: 'Create account', + tag: 'div', + }); + } + await driver.clickElement({ + text: 'Create Account', + tag: 'button', + }); + + // wait until dialog is opened before proceeding + await driver.waitAndSwitchToWindowWithTitle(3, WINDOW_TITLES.Dialog); + + // click the create button on the confirmation modal + await driver.clickElement( + '[data-testid="confirmation-submit-button"]', + ); + + // click the add account button on the naming modal + await driver.clickElement( + '[data-testid="submit-add-account-with-name"]', + ); + + // click the okay button on the success screen + await driver.clickElement( + '[data-testid="confirmation-submit-button"]', + ); + + // switch to extension full screen view + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // verify the account is created with the expected name + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: expectedName, + }); + } + }, + ); + }); + it('create Snap account confirmation flow ends in approval success with custom name input', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // start the create account flow and switch to dialog window + await startCreateSnapAccountFlow(driver); + + // click the create button on the confirmation modal await driver.clickElement('[data-testid="confirmation-submit-button"]'); + // Add a custom name to the account + const newAccountLabel = 'Custom name'; + await driver.fill('[placeholder="SSK Account"]', newAccountLabel); + // click the add account button on the naming modal + await driver.clickElement( + '[data-testid="submit-add-account-with-name"]', + ); + + // success screen should show account created with the custom name await driver.findElement({ tag: 'h3', text: 'Account created', }); + await driver.findElement({ + css: '.multichain-account-list-item__account-name__button', + text: newAccountLabel, + }); // click the okay button await driver.clickElement('[data-testid="confirmation-submit-button"]'); // switch back to the test dapp/Snap window - await driver.switchToWindowWithTitle( + await driver.waitAndSwitchToWindowWithTitle( + 2, WINDOW_TITLES.SnapSimpleKeyringDapp, ); + // account should be created on the dapp await driver.findElement({ tag: 'p', text: 'Successful request', }); + // switch to extension full screen view await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); + // account should be created with the custom name await driver.findElement({ css: '[data-testid="account-menu-icon"]', - text: 'Snap Account 1', + text: newAccountLabel, }); }, ); }); - it('create Snap account confirmation cancelation results in error in Snap', async function () { + it('create Snap account confirmation cancellation results in error in Snap', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), @@ -191,71 +266,79 @@ describe('Create Snap Account', function (this: Suite) { title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - // navigate to test Snaps page and connect - await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); - await driver.clickElement('#connectButton'); - - // switch to metamask extension and click connect to start installing the snap - await switchToNotificationWindow(driver); - await driver.clickElement({ - text: 'Connect', - tag: 'button', - }); - - // scroll to the bottom of the page - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // start the create account flow and switch to dialog window + await startCreateSnapAccountFlow(driver); - // click the install button to install the snap - await driver.waitForSelector({ text: 'Confirm' }); - await driver.clickElement({ - text: 'Confirm', - tag: 'button', - }); - await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ - text: 'OK', - tag: 'button', - }); + // cancel account creation + await driver.clickElement('[data-testid="confirmation-cancel-button"]'); - // move back to the Snap window to test the create account flow - await driver.switchToWindowWithTitle( + // switch back to the test dapp/Snap window + await driver.waitAndSwitchToWindowWithTitle( + 2, WINDOW_TITLES.SnapSimpleKeyringDapp, ); - // check the dapp connection status - await driver.waitForSelector({ - css: '#snapConnected', - text: 'Connected', + // account should not be created in Snap + await driver.findElement({ + tag: 'p', + text: 'Error request', }); - // create new account on dapp - await driver.clickElement({ - text: 'Create account', - tag: 'div', - }); + // switch to extension full screen view + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); - await driver.clickElement({ - text: 'Create Account', - tag: 'button', + // account should not be created + await driver.assertElementNotPresent({ + css: '[data-testid="account-menu-icon"]', + text: 'SSK Account', }); + }, + ); + }); + + it('cancelling naming Snap account results in account not created', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // start the create account flow and switch to dialog window + await startCreateSnapAccountFlow(driver); - // switch to metamask extension - await switchToNotificationWindow(driver); + // confirm account creation + await driver.clickElement('[data-testid="confirmation-submit-button"]'); - // cancel account creation - await driver.clickElement('[data-testid="confirmation-cancel-button"]'); + // click the cancel button on the naming modal + await driver.clickElement( + '[data-testid="cancel-add-account-with-name"]', + ); // switch back to the test dapp/Snap window - await driver.switchToWindowWithTitle( + await driver.waitAndSwitchToWindowWithTitle( + 2, WINDOW_TITLES.SnapSimpleKeyringDapp, ); + // account should not be created in Snap await driver.findElement({ tag: 'p', text: 'Error request', }); + + // switch to extension full screen view + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + // account should not be created + await driver.assertElementNotPresent({ + css: '[data-testid="account-menu-icon"]', + text: 'SSK Account', + }); }, ); }); diff --git a/test/e2e/accounts/create-watch-account.spec.ts b/test/e2e/accounts/create-watch-account.spec.ts new file mode 100644 index 000000000000..75be806a221c --- /dev/null +++ b/test/e2e/accounts/create-watch-account.spec.ts @@ -0,0 +1,461 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import messages from '../../../app/_locales/en/messages.json'; +import FixtureBuilder from '../fixture-builder'; +import { defaultGanacheOptions, unlockWallet, withFixtures } from '../helpers'; +import { Driver } from '../webdriver/driver'; + +const ACCOUNT_1 = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; +const EOA_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; +const SHORTENED_EOA_ADDRESS = '0xd8dA6...96045'; +const DEFAULT_WATCHED_ACCOUNT_NAME = 'Watched Account 1'; + +/** + * Start the flow to create a watch account by clicking the account menu and selecting the option to add a watch account. + * + * @param driver - The WebDriver instance used to control the browser. + * @param unlockWalletFirst - Whether to unlock the wallet before starting the flow. + */ +async function startCreateWatchAccountFlow( + driver: Driver, + unlockWalletFirst: boolean = true, +): Promise { + if (unlockWalletFirst) { + await unlockWallet(driver); + } + + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-add-watch-only-account"]', + ); +} + +/** + * Watches an EOA address. + * + * @param driver - The WebDriver instance used to control the browser. + * @param unlockWalletFirst - Whether to unlock the wallet before watching the address. + * @param address - The EOA address to watch. + */ +async function watchEoaAddress( + driver: Driver, + unlockWalletFirst: boolean = true, + address: string = EOA_ADDRESS, +): Promise { + await startCreateWatchAccountFlow(driver, unlockWalletFirst); + await driver.fill( + '[placeholder="Enter a public address or ENS name"]', + address, + ); + await driver.clickElement({ text: 'Watch account', tag: 'button' }); + await driver.clickElement('[data-testid="submit-add-account-with-name"]'); +} + +/** + * Removes the selected account. + * + * @param driver - The WebDriver instance used to control the browser. + */ +async function removeSelectedAccount(driver: Driver): Promise { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', + ); + await driver.clickElement('[data-testid="account-list-menu-remove"]'); + await driver.clickElement({ text: 'Remove', tag: 'button' }); +} + +describe('Account-watcher snap', function (this: Suite) { + describe('Adding watched accounts', function () { + it('adds watch account with valid EOA address', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address + await watchEoaAddress(driver); + + // new account should be displayed in the account list + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: DEFAULT_WATCHED_ACCOUNT_NAME, + }); + await driver.findElement({ + css: '.mm-text--ellipsis', + text: SHORTENED_EOA_ADDRESS, + }); + }, + ); + }); + + it("disables 'Send' 'Swap' and 'Bridge' buttons for watch accounts", async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address + await watchEoaAddress(driver); + + // 'Send' button should be disabled + await driver.findElement( + '[data-testid="eth-overview-send"][disabled]', + ); + await driver.findElement( + '[data-testid="eth-overview-send"].icon-button--disabled', + ); + + // 'Swap' button should be disabled + await driver.findElement( + '[data-testid="token-overview-button-swap"][disabled]', + ); + await driver.findElement( + '[data-testid="token-overview-button-swap"].icon-button--disabled', + ); + + // 'Bridge' button should be disabled + await driver.findElement( + '[data-testid="eth-overview-bridge"][disabled]', + ); + await driver.findElement( + '[data-testid="eth-overview-bridge"].icon-button--disabled', + ); + + // check tooltips for disabled buttons + await driver.findElement( + '.icon-button--disabled [data-tooltipped][data-original-title="Not supported with this account."]', + ); + }, + ); + }); + }); + + describe('Invalid input handling', function () { + const invalidInputTests = [ + { + input: 'd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + message: 'Invalid input', + description: 'address missing 0x prefix', + }, + { + input: '0x123ABC', + message: 'Invalid address', + description: 'invalid address', + }, + { + input: 'invalid.eth', + message: 'Invalid ENS name', + description: 'invalid ENS name', + }, + { + input: ACCOUNT_1, + message: `Unknown snap error: Account address '${ACCOUNT_1}' already exists`, + description: 'existing address', + }, + ]; + + invalidInputTests.forEach(({ input, message, description }) => { + it(`handles invalid input: ${description}`, async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await startCreateWatchAccountFlow(driver); + + await driver.fill( + '[placeholder="Enter a public address or ENS name"]', + input, + ); + await driver.clickElement({ text: 'Watch account', tag: 'button' }); + + // error message should be displayed by the snap + await driver.findElement({ + css: '.snap-ui-renderer__text', + text: message, + }); + }, + ); + }); + }); + }); + + describe('Account management', function () { + it('does not allow user to import private key of watched address', async function () { + const PRIVATE_KEY_TWO = + '0xf444f52ea41e3a39586d7069cb8e8233e9f6b9dea9cbb700cce69ae860661cc8'; + const ACCOUNT_2 = '0x09781764c08de8ca82e156bbf156a3ca217c7950'; + + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address for ACCOUNT_2 + await watchEoaAddress(driver, true, ACCOUNT_2); + + // try to import private key of watched ACCOUNT_2 address + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ text: 'Import account', tag: 'button' }); + await driver.findClickableElement('#private-key-box'); + await driver.fill('#private-key-box', PRIVATE_KEY_TWO); + await driver.clickElement( + '[data-testid="import-account-confirm-button"]', + ); + + // error message should be displayed + await driver.findElement({ + css: '.mm-box--color-error-default', + text: 'KeyringController - The account you are trying to import is a duplicate', + }); + }, + ); + }); + + it("does not display 'Show private key' button for watch accounts", async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address + await watchEoaAddress(driver); + + // click to view account details + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-list-menu-details"]', + ); + // 'Show private key' button should not be displayed + await driver.assertElementNotPresent({ + css: 'button', + text: 'Show private key', + }); + }, + ); + }); + + it('removes a watched account', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address + await watchEoaAddress(driver); + + // remove the selected watched account + await removeSelectedAccount(driver); + + // account should be removed from the account list + await driver.assertElementNotPresent({ + css: '[data-testid="account-menu-icon"]', + text: DEFAULT_WATCHED_ACCOUNT_NAME, + }); + await driver.assertElementNotPresent({ + css: '.mm-text--ellipsis', + text: SHORTENED_EOA_ADDRESS, + }); + }, + ); + }); + + it('can remove and recreate a watched account', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + watchEthereumAccountEnabled: true, + }) + .withNetworkControllerOnMainnet() + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + // watch an EOA address + await watchEoaAddress(driver); + + // remove the selected watched account + await removeSelectedAccount(driver); + + // account should be removed from the account list + await driver.assertElementNotPresent({ + css: '[data-testid="account-menu-icon"]', + text: DEFAULT_WATCHED_ACCOUNT_NAME, + }); + await driver.assertElementNotPresent({ + css: '.mm-text--ellipsis', + text: SHORTENED_EOA_ADDRESS, + }); + + // watch the same EOA address again + await watchEoaAddress(driver, false); + + // same account should be displayed in the account list + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: DEFAULT_WATCHED_ACCOUNT_NAME, + }); + await driver.findElement({ + css: '.mm-text--ellipsis', + text: SHORTENED_EOA_ADDRESS, + }); + }, + ); + }); + }); + + describe('Experimental toggle', function () { + const navigateToExperimentalSettings = async (driver: Driver) => { + await driver.clickElement('[data-testid="account-options-menu-button"]'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ text: 'Experimental', tag: 'div' }); + await driver.waitForSelector({ + text: messages.watchEthereumAccountsToggle.message, + tag: 'span', + }); + }; + + const getToggleState = async (driver: Driver): Promise => { + const toggleInput = await driver.findElement( + '[data-testid="watch-account-toggle"]', + ); + return toggleInput.isSelected(); + }; + + const toggleWatchAccountOptionAndCloseSettings = async (driver: Driver) => { + await driver.clickElement('[data-testid="watch-account-toggle-div"]'); + await driver.clickElement('button[aria-label="Close"]'); + }; + + const verifyWatchAccountOptionAndCloseMenu = async ( + driver: Driver, + shouldBePresent: boolean, + ) => { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + if (shouldBePresent) { + await driver.waitForSelector({ + text: messages.addEthereumWatchOnlyAccount.message, + tag: 'button', + }); + } else { + await driver.assertElementNotPresent({ + text: messages.addEthereumWatchOnlyAccount.message, + tag: 'button', + }); + } + await driver.clickElement('button[aria-label="Close"]'); + }; + + it("will show the 'Watch an Ethereum account (Beta)' option when setting is enabled", async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await navigateToExperimentalSettings(driver); + + // verify toggle is off by default + assert.equal( + await getToggleState(driver), + false, + 'Toggle should be off by default', + ); + + // enable the toggle + await toggleWatchAccountOptionAndCloseSettings(driver); + + // verify the 'Watch and Ethereum account (Beta)' option is available + await verifyWatchAccountOptionAndCloseMenu(driver, true); + }, + ); + }); + + it('enables and then disables the toggle and the option to add a watch-only account behaves as expected', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await navigateToExperimentalSettings(driver); + + // enable the toggle + await toggleWatchAccountOptionAndCloseSettings(driver); + + // verify the 'Watch and Ethereum account (Beta)' option is available + await verifyWatchAccountOptionAndCloseMenu(driver, true); + + // navigate back to experimental settings + await navigateToExperimentalSettings(driver); + + // disable the toggle + await toggleWatchAccountOptionAndCloseSettings(driver); + + // verify the 'Watch and Ethereum account (Beta)' option is not available + await verifyWatchAccountOptionAndCloseMenu(driver, false); + }, + ); + }); + }); +}); diff --git a/test/e2e/accounts/forgot-password.spec.ts b/test/e2e/accounts/forgot-password.spec.ts new file mode 100644 index 000000000000..3bf347fa59cc --- /dev/null +++ b/test/e2e/accounts/forgot-password.spec.ts @@ -0,0 +1,65 @@ +import { Suite } from 'mocha'; +import { unlockWallet, withFixtures, TEST_SEED_PHRASE_TWO } from '../helpers'; +import FixtureBuilder from '../fixture-builder'; +import { Driver } from '../webdriver/driver'; + +const newPassword = 'this is the best password ever'; + +describe('Forgot password', function (this: Suite) { + it('resets password and then unlock wallet with new password', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + // Lock Wallet + await driver.waitForSelector( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ + text: 'Lock MetaMask', + tag: 'div', + }); + + // Go to reset password page + await driver.waitForSelector('.unlock-page__link'); + await driver.clickElement({ + text: 'Forgot password?', + tag: 'a', + }); + + // Reset password with a new password + await driver.pasteIntoField( + '[data-testid="import-srp__srp-word-0"]', + TEST_SEED_PHRASE_TWO, + ); + + await driver.fill('#password', newPassword); + await driver.fill('#confirm-password', newPassword); + await driver.press('#confirm-password', driver.Key.ENTER); + + // Lock wallet again + await driver.waitForSelector( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ + text: 'Lock MetaMask', + tag: 'div', + }); + + // log in with new password + await driver.fill('#password', newPassword); + await driver.press('#password', driver.Key.ENTER); + }, + ); + }); +}); diff --git a/test/e2e/accounts/smart-swap-disabled.spec.ts b/test/e2e/accounts/smart-swap-disabled.spec.ts index e12f7bcae7c2..4e6ce9b2c6ef 100644 --- a/test/e2e/accounts/smart-swap-disabled.spec.ts +++ b/test/e2e/accounts/smart-swap-disabled.spec.ts @@ -1,17 +1,17 @@ -import { title } from 'process'; import { Suite } from 'mocha'; -import { withFixtures } from '../helpers'; +import { withFixtures, defaultGanacheOptions } from '../helpers'; import { Driver } from '../webdriver/driver'; -import { - accountSnapFixtures, - installSnapSimpleKeyring, - makeNewAccountAndSwitch, -} from './common'; +import FixtureBuilder from '../fixture-builder'; +import { installSnapSimpleKeyring, makeNewAccountAndSwitch } from './common'; -describe('Smart Swaps', function (this: Suite) { - it('should be disabled for snap accounts', async function () { +describe('Snap Account - Smart Swaps', function (this: Suite) { + it('checks if smart swaps are disabled for snap accounts', async function () { await withFixtures( - accountSnapFixtures(title), + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, async ({ driver }: { driver: Driver }) => { await installSnapSimpleKeyring(driver, false); await makeNewAccountAndSwitch(driver); diff --git a/test/e2e/accounts/snap-account-contract-interaction.spec.ts b/test/e2e/accounts/snap-account-contract-interaction.spec.ts new file mode 100644 index 000000000000..4588d014802c --- /dev/null +++ b/test/e2e/accounts/snap-account-contract-interaction.spec.ts @@ -0,0 +1,87 @@ +import GanacheContractAddressRegistry from '../seeder/ganache-contract-address-registry'; +import { scrollAndConfirmAndAssertConfirm } from '../tests/confirmations/helpers'; +import { + createDepositTransaction, + TestSuiteArguments, +} from '../tests/confirmations/transactions/shared'; +import { + multipleGanacheOptionsForType2Transactions, + withFixtures, + openDapp, + WINDOW_TITLES, + locateAccountBalanceDOM, + clickNestedButton, + ACCOUNT_2, +} from '../helpers'; +import FixtureBuilder from '../fixture-builder'; +import { SMART_CONTRACTS } from '../seeder/smart-contracts'; +import { installSnapSimpleKeyring, importKeyAndSwitch } from './common'; + +describe('Snap Account Contract interaction', function () { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + + it('deposits to piggybank contract', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerSnapAccountConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: multipleGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ + driver, + contractRegistry, + ganacheServer, + }: TestSuiteArguments) => { + // Install Snap Simple Keyring and import key + await installSnapSimpleKeyring(driver, false); + await importKeyAndSwitch(driver); + + // Open DApp with contract + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + await openDapp(driver, contractAddress); + + // Create and confirm deposit transaction + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await createDepositTransaction(driver); + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + css: 'h2', + text: 'Transaction request', + }); + await scrollAndConfirmAndAssertConfirm(driver); + + // Confirm the transaction activity + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await clickNestedButton(driver, 'Activity'); + await driver.waitForSelector( + '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', + ); + await driver.waitForSelector({ + css: '[data-testid="transaction-list-item-primary-currency"]', + text: '-4 ETH', + }); + + // renders the correct ETH balance + await locateAccountBalanceDOM(driver, ganacheServer, ACCOUNT_2); + }, + ); + }); +}); diff --git a/test/e2e/accounts/snap-account-eth-swap.spec.ts b/test/e2e/accounts/snap-account-eth-swap.spec.ts new file mode 100644 index 000000000000..bb4c3c7a5770 --- /dev/null +++ b/test/e2e/accounts/snap-account-eth-swap.spec.ts @@ -0,0 +1,48 @@ +import { withFixtures, defaultGanacheOptions, WINDOW_TITLES } from '../helpers'; +import { Driver } from '../webdriver/driver'; +import FixtureBuilder from '../fixture-builder'; +import { + buildQuote, + reviewQuote, + waitForTransactionToComplete, + checkActivityTransaction, +} from '../tests/swaps/shared'; +import { installSnapSimpleKeyring } from './common'; + +const DAI = 'DAI'; +const TEST_ETH = 'TESTETH'; + +describe('Snap Account - Swap', function () { + it('swaps ETH for DAI using a snap account', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await installSnapSimpleKeyring(driver, false); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + await buildQuote(driver, { + amount: 0.001, + swapTo: DAI, + }); + await reviewQuote(driver, { + amount: 0.001, + swapFrom: TEST_ETH, + swapTo: DAI, + }); + await driver.clickElement({ text: 'Swap', tag: 'button' }); + await waitForTransactionToComplete(driver, { tokenName: 'DAI' }); + await checkActivityTransaction(driver, { + index: 0, + amount: '0.001', + swapFrom: TEST_ETH, + swapTo: DAI, + }); + }, + ); + }); +}); diff --git a/test/e2e/accounts/snap-account-settings.spec.ts b/test/e2e/accounts/snap-account-settings.spec.ts index 162fae62ec7e..5a27f10f4764 100644 --- a/test/e2e/accounts/snap-account-settings.spec.ts +++ b/test/e2e/accounts/snap-account-settings.spec.ts @@ -26,7 +26,12 @@ describe('Add snap account experimental settings', function (this: Suite) { text: 'Add account Snap', tag: 'button', }, - { findElementGuard: { text: 'Add a new account', tag: 'button' } }, // wait for the modal to appear + { + findElementGuard: { + text: 'Add a new Ethereum account', + tag: 'button', + }, + }, // wait for the modal to appear ); await driver.clickElement('.mm-box button[aria-label="Close"]'); diff --git a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts b/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts index d00540360317..24e996671da9 100644 --- a/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts +++ b/test/e2e/accounts/snap-account-signatures-and-disconnects.spec.ts @@ -1,6 +1,10 @@ import { Suite } from 'mocha'; import FixtureBuilder from '../fixture-builder'; -import { withFixtures, multipleGanacheOptions } from '../helpers'; +import { + withFixtures, + multipleGanacheOptions, + tempToggleSettingRedesignedConfirmations, +} from '../helpers'; import { Driver } from '../webdriver/driver'; import { installSnapSimpleKeyring, @@ -27,6 +31,8 @@ describe('Snap Account Signatures and Disconnects', function (this: Suite) { const newPublicKey = await makeNewAccountAndSwitch(driver); + await tempToggleSettingRedesignedConfirmations(driver); + // open the Test Dapp and connect Account 2 to it await connectAccountToTestDapp(driver); diff --git a/test/e2e/accounts/snap-account-signatures.spec.ts b/test/e2e/accounts/snap-account-signatures.spec.ts index 5c1a00a2c00c..536d8168b1a3 100644 --- a/test/e2e/accounts/snap-account-signatures.spec.ts +++ b/test/e2e/accounts/snap-account-signatures.spec.ts @@ -1,5 +1,8 @@ import { Suite } from 'mocha'; -import { openDapp, withFixtures } from '../helpers'; +import { + tempToggleSettingRedesignedConfirmations, + withFixtures, +} from '../helpers'; import { Driver } from '../webdriver/driver'; import { accountSnapFixtures, @@ -27,11 +30,10 @@ describe('Snap Account Signatures', function (this: Suite) { const newPublicKey = await makeNewAccountAndSwitch(driver); - await openDapp(driver); + await tempToggleSettingRedesignedConfirmations(driver); - // Run all 6 signature types + // Run all 5 signature types const locatorIDs = [ - '#ethSign', '#personalSign', '#signTypedData', '#signTypedDataV3', diff --git a/test/e2e/api-specs/ConfirmationRejectionRule.ts b/test/e2e/api-specs/ConfirmationRejectionRule.ts index 9a8348357a69..503d0358c63c 100644 --- a/test/e2e/api-specs/ConfirmationRejectionRule.ts +++ b/test/e2e/api-specs/ConfirmationRejectionRule.ts @@ -23,17 +23,12 @@ export class ConfirmationsRejectRule implements Rule { 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', @@ -112,22 +107,15 @@ export class ConfirmationsRejectRule implements Rule { 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 text = 'Cancel'; + + await this.driver.findClickableElements({ + text: 'Cancel', + tag: 'button', + }); + const screenshot = await this.driver.driver.takeScreenshot(); call.attachments = call.attachments || []; call.attachments.push({ diff --git a/test/e2e/api-specs/helpers.ts b/test/e2e/api-specs/helpers.ts index d4f4d3be22c5..51cdbbe47951 100644 --- a/test/e2e/api-specs/helpers.ts +++ b/test/e2e/api-specs/helpers.ts @@ -58,22 +58,18 @@ export const pollForResult = async ( `return window['${generatedKey}'];`, ); - while (result === null) { - // Continue polling if result is not set + if (result) { + // clear the result + await driver.executeScript(`delete window['${generatedKey}'];`); + } else { 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) { + if (result !== undefined && result !== null) { return result; } return pollForResult(driver, generatedKey); @@ -86,50 +82,47 @@ export const createDriverTransport = (driver: Driver) => { 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, - ); + addToQueue({ + name: 'transport', + resolve: () => { + // noop + }, + reject: () => { + // noop + }, + 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, + }, + }; + }); }, - }); - }; - return execute(); - }).then(async () => { - const response = await pollForResult(driver, generatedKey); - return response; + method, + params, + generatedKey, + ); + }, }); + return pollForResult(driver, generatedKey); }; }; diff --git a/test/e2e/background-socket/server-mocha-to-background.ts b/test/e2e/background-socket/server-mocha-to-background.ts new file mode 100644 index 000000000000..3b4cc1c88b2f --- /dev/null +++ b/test/e2e/background-socket/server-mocha-to-background.ts @@ -0,0 +1,130 @@ +import events from 'events'; +import { WebSocketServer } from 'ws'; +import { + MessageType, + ServerMochaEventEmitterType, + WindowProperties, +} from './types'; + +/** + * This singleton class runs on the Mocha/Selenium test. + * It's used to communicate from the Mocha/Selenium test to the Extension background script (service worker in MV3). + */ +class ServerMochaToBackground { + private server: WebSocketServer; + + private ws: WebSocket | null = null; + + private eventEmitter; + + constructor() { + this.server = new WebSocketServer({ port: 8111 }); + + console.debug('ServerMochaToBackground created'); + + this.server.on('connection', (ws: WebSocket) => { + // Check for existing connection and close it + if (this.ws) { + console.error( + 'ServerMochaToBackground got a second client connection, closing the first one', + ); + this.ws.close(); + } + + this.ws = ws; + + console.debug('ServerMochaToBackground got a client connection'); + + ws.onmessage = (ev: MessageEvent) => { + let message: MessageType; + + try { + message = JSON.parse(ev.data); + } catch (e) { + throw new Error( + `Error in JSON sent to ServerMochaToBackground: ${ + (e as Error).message + }`, + ); + } + + this.receivedMessage(message); + }; + + ws.onclose = () => { + this.ws = null; + console.debug('ServerMochaToBackground disconnected from client'); + }; + }); + + this.eventEmitter = new events.EventEmitter(); + } + + // This function is never explicitly called, but in the future it could be + stop() { + this.ws?.close(); + + this.server.close(); + + console.debug('ServerMochaToBackground stopped'); + } + + // Send a message to the Extension background script (service worker in MV3) + send(message: MessageType) { + if (!this.ws) { + throw new Error('No client connected to ServerMochaToBackground'); + } + + this.ws.send(JSON.stringify(message)); + } + + // Handle messages received from the Extension background script (service worker in MV3) + private receivedMessage(message: MessageType) { + if (message.command === 'openTabs' && message.tabs) { + this.eventEmitter.emit('openTabs', message.tabs); + } else if (message.command === 'notFound') { + throw new Error( + `No window found by background script with ${message.property}: ${message.value}`, + ); + } + } + + // This is not used in the current code, but could be used in the future + queryTabs(tabTitle: string) { + this.send({ command: 'queryTabs', title: tabTitle }); + } + + // Sends the message to the Extension, and waits for a response + async waitUntilWindowWithProperty(property: WindowProperties, value: string) { + this.send({ command: 'waitUntilWindowWithProperty', property, value }); + + const tabs = await this.waitForResponse(); + // console.debug('ServerMochaToBackground got the response', tabs); + + // The return value here is less useful than we had hoped, because the tabs + // are not in the same order as driver.getAllWindowHandles() + return tabs; + } + + // This is a way to wait for an event async, without timeouts or polling + async waitForResponse() { + return new Promise((resolve) => { + this.eventEmitter.once('openTabs', resolve); + }); + } +} + +// Singleton setup below +let _serverMochaToBackground: ServerMochaToBackground; + +export function getServerMochaToBackground() { + if (!_serverMochaToBackground) { + startServerMochaToBackground(); + } + + return _serverMochaToBackground; +} + +function startServerMochaToBackground() { + _serverMochaToBackground = new ServerMochaToBackground(); +} diff --git a/test/e2e/background-socket/socket-background-to-mocha.ts b/test/e2e/background-socket/socket-background-to-mocha.ts new file mode 100644 index 000000000000..b08650ee4008 --- /dev/null +++ b/test/e2e/background-socket/socket-background-to-mocha.ts @@ -0,0 +1,153 @@ +import log from 'loglevel'; +import { isManifestV3 } from '../../../shared/modules/mv3.utils'; +import { MessageType, WindowProperties } from './types'; + +/** + * This singleton class runs on the Extension background script (service worker in MV3). + * It's used to communicate from the Extension background script to the Mocha/Selenium test. + * The main advantage is that it can call chrome.tabs.query(). + * We had hoped it would be able to call chrome.tabs.highlight(), but Selenium doesn't see the tab change. + */ +class SocketBackgroundToMocha { + private client: WebSocket; + + constructor() { + this.client = new WebSocket('ws://localhost:8111'); + + this.client.onopen = () => + log.debug('SocketBackgroundToMocha WebSocket connection opened'); + + this.client.onmessage = (ev: MessageEvent) => { + let message: MessageType; + + try { + message = JSON.parse(ev.data); + } catch (e) { + throw new Error( + `Error in JSON sent to SocketBackgroundToMocha: ${ + (e as Error).message + }`, + ); + } + + this.receivedMessage(message); + }; + + this.client.onclose = () => + log.debug('SocketBackgroundToMocha WebSocket connection closed'); + + this.client.onerror = (error) => + log.error('SocketBackgroundToMocha WebSocket error:', error); + } + + /** + * Waits until a window with the given property is open. + * delayStep = 200ms, timeout = 10s + * + * You can think of this kind of like a template function: + * If `property` is `title`, then this becomes `waitUntilWindowWithTitle` + * If `property` is `url`, then this becomes `waitUntilWindowWithUrl` + * Remember that `a[property]` becomes `a.title` or `a.url` + * + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to wait for + * @returns The handle of the window tab with the given property value + */ + async waitUntilWindowWithProperty(property: WindowProperties, value: string) { + let tabs: chrome.tabs.Tab[] = []; + const delayStep = 200; + const timeout = 10000; + + for ( + let timeElapsed = 0; + timeElapsed <= timeout; + timeElapsed += delayStep + ) { + tabs = await this.queryTabs({}); + + const index = tabs.findIndex((a) => a[property] === value); + + if (index !== -1) { + this.send({ command: 'openTabs', tabs: this.cleanTabs(tabs) }); + return; + } + + // wait for delayStep milliseconds + await new Promise((resolve) => setTimeout(resolve, delayStep)); + } + + // The window was not found at the end of the timeout + this.send({ + command: 'notFound', + property, + value, + tabs: this.cleanTabs(tabs), + }); + } + + // This function exists to support both MV2 and MV3 + private async queryTabs(queryInfo: object): Promise { + if (isManifestV3) { + // With MV3, chrome.tabs.query has an await form + return await chrome.tabs.query(queryInfo); + } + + // With MV2, we have to wrap chrome.tabs.query in a Promise + return new Promise((resolve) => { + chrome.tabs.query(queryInfo, (tabs: chrome.tabs.Tab[]) => { + resolve(tabs); + }); + }); + } + + // Clean up the tab data before sending them to the client + private cleanTabs(tabs: chrome.tabs.Tab[]): chrome.tabs.Tab[] { + return tabs.map((tab) => { + // This field can be very long, and is not needed + if (tab.favIconUrl && tab.favIconUrl.length > 40) { + tab.favIconUrl = undefined; + } + + return tab; + }); + } + + // Send a message to the Mocha/Selenium test + send(message: MessageType) { + this.client.send(JSON.stringify(message)); + } + + // Handle messages received from the Mocha/Selenium test + private async receivedMessage(message: MessageType) { + log.debug('SocketBackgroundToMocha received message:', message); + + if (message.command === 'queryTabs') { + const tabs = await this.queryTabs({ title: message.title }); + log.debug('SocketBackgroundToMocha sending tabs:', tabs); + this.send({ command: 'openTabs', tabs: this.cleanTabs(tabs) }); + } else if ( + message.command === 'waitUntilWindowWithProperty' && + message.property && + message.value + ) { + this.waitUntilWindowWithProperty(message.property, message.value); + } + } +} + +// Singleton setup below +let _socketBackgroundToMocha: SocketBackgroundToMocha; + +export function getSocketBackgroundToMocha() { + if (!_socketBackgroundToMocha) { + startSocketBackgroundToMocha(); + } + + return _socketBackgroundToMocha; +} + +function startSocketBackgroundToMocha() { + if (process.env.IN_TEST) { + _socketBackgroundToMocha = new SocketBackgroundToMocha(); + } +} diff --git a/test/e2e/background-socket/types.ts b/test/e2e/background-socket/types.ts new file mode 100644 index 000000000000..00a734374c90 --- /dev/null +++ b/test/e2e/background-socket/types.ts @@ -0,0 +1,24 @@ +export type MessageType = { + command: + | 'openTabs' + | 'notFound' + | 'queryTabs' + | 'waitUntilWindowWithProperty'; + tabs?: chrome.tabs.Tab[]; + title?: string; + property?: WindowProperties; + value?: string; +}; + +export type Handle = { + id: string; + title: string; + url: string; +}; + +export type WindowProperties = 'title' | 'url'; + +export type ServerMochaEventEmitterType = { + openTabs: [openTabs: chrome.tabs.Tab[]]; + notFound: [openTabs: chrome.tabs.Tab[]]; +}; diff --git a/test/e2e/background-socket/window-handles.ts b/test/e2e/background-socket/window-handles.ts new file mode 100644 index 000000000000..8e86a48a68cb --- /dev/null +++ b/test/e2e/background-socket/window-handles.ts @@ -0,0 +1,231 @@ +import log from 'loglevel'; +import { ThenableWebDriver } from 'selenium-webdriver'; +import { getServerMochaToBackground } from './server-mocha-to-background'; +import { Handle, WindowProperties } from './types'; + +/** + * Keeps a list of window handles and their properties (title, url). + * Takes control of switchToWindowWithTitle and switchToWindowWithUrl away from driver.js + */ +export class WindowHandles { + driver: ThenableWebDriver; + + rawHandles: string[] = []; + + annotatedHandles: Handle[] = []; + + constructor(driver: ThenableWebDriver) { + this.driver = driver; + } + + // Gets all window handles and annotates them with title and url + async getAllWindowHandles() { + this.rawHandles = await this.driver.getAllWindowHandles(); + + this.updateAnnotatedHandles(); + + log.debug('rawHandles', this.rawHandles); + log.debug('annotatedHandles', this.annotatedHandles); + + return this.rawHandles; + } + + // Remove outdated annotatedHandles and add new annotatedHandles + updateAnnotatedHandles() { + // Remove any annotatedHandles that are no longer present + for (let i = 0; i < this.annotatedHandles.length; i++) { + const handleId = this.annotatedHandles[i].id; + if (this.rawHandles.indexOf(handleId) === -1) { + log.debug('Removing handle:', this.annotatedHandles[i]); + this.annotatedHandles.splice(i, 1); + i -= 1; + } + } + + // Add any new rawHandles to the annotatedHandles + for (const handleId of this.rawHandles) { + if (!this.annotatedHandles.find((a) => a.id === handleId)) { + this.annotatedHandles.push({ id: handleId, title: '', url: '' }); + } + } + } + + /** + * Get the given property (title or url) of the current window. + * + * @param property - 'title' or 'url' + * @param optionalCurrentHandle - If we already know the current handle, we can pass it in here + * @returns Depending on `property`, returns the title or url of the current window, or null if no return value is requested + */ + async getCurrentWindowProperties( + property?: WindowProperties, + optionalCurrentHandle?: string, + ) { + const currentHandle = + optionalCurrentHandle || (await this.driver.getWindowHandle()); + + let currentTitle, currentUrl; + + // Wait 25 x 200ms = 5 seconds for the title and url to be set + for (let i = 0; i < 25 && !currentTitle && !currentUrl; i++) { + currentTitle = await this.driver.getTitle(); + currentUrl = await this.driver.getCurrentUrl(); + + if (!currentTitle || !currentUrl) { + await this.driver.sleep(200); + } + } + + if (!currentTitle || !currentUrl) { + log.debug( + 'Cannot get properties of current window', + currentHandle, + currentTitle, + currentUrl, + ); + return null; + } + + let annotatedHandle = this.annotatedHandles.find( + (a) => a.id === currentHandle, + ); + + if (annotatedHandle) { + // Handle already there, update it + annotatedHandle.title = currentTitle; + annotatedHandle.url = currentUrl; + } else { + // Handle not there, add it + annotatedHandle = { + id: currentHandle, + title: currentTitle, + url: currentUrl, + }; + this.annotatedHandles.push(annotatedHandle); + } + + if (property) { + // Return the current title or url + return annotatedHandle[property]; + } + + // Didn't ask for any return value + return null; + } + + // Count the number of handles with an unknown title + countUntitledHandles() { + return this.annotatedHandles.filter((a) => !a.title).length; + } + + // Count the number of handles with an unknown template property + countHandlesWithoutProperty(property: WindowProperties) { + return this.annotatedHandles.filter((a) => !a[property]).length; + } + + /** + * Switches the context of the browser session to the window/tab with the given property. + * + * You can think of this kind of like a template function: + * If `property` is `title`, then this becomes `switchToWindowWithTitle` + * If `property` is `url`, then this becomes `switchToWindowWithUrl` + * Remember that `a[property]` becomes `a.title` or `a.url` + * + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to switch to + * @returns The handle of the window tab with the given property value + */ + async switchToWindowWithProperty(property: WindowProperties, value: string) { + // Ask the extension to wait until the window with the given property is open + // (just ignore the return value here, because the tabs are not in the same order as driver.getAllWindowHandles()) + await getServerMochaToBackground().waitUntilWindowWithProperty( + property, + value, + ); + + await this.getAllWindowHandles(); + + // See if we already know the handle by annotation + const handle = this.annotatedHandles.find((a) => a[property] === value); + let handleId = handle?.id; + + // If there's exactly one un-annotated handle, we should try it + if (!handleId) { + if (this.countHandlesWithoutProperty(property) === 1) { + handleId = this.annotatedHandles.find((a) => !a[property])?.id; + } + } + + // Do the actual switching for the one we found + if (handleId) { + const matchesProperty = await this.switchToHandleAndCheckForProperty( + handleId, + property, + value, + ); + + if (matchesProperty) { + return handleId; + } + } + + // We have not found it in the annotatedHandles, or the one we found was wrong, so we have to cycle through all + // handles and bring them into focus to get their titles/urls. There is no need for repeats or delays, because + // ServerMochaToBackground has already waited for the tab to be found + for (handleId of this.rawHandles) { + const matchesProperty = await this.switchToHandleAndCheckForProperty( + handleId, + property, + value, + ); + + if (matchesProperty) { + return handleId; + } + } + + // If we still haven't found it, throw an error + throw new Error(`No window with ${property}: ${value}`); + } + + /** + * Switches the window and makes sure it matches the expected title or url. + * + * @param handleId - The handle we want to switch to + * @param property - 'title' or 'url' + * @param value - The value we're searching for and want to switch to + * @returns Whether the window we switched to has the expected property value + */ + async switchToHandleAndCheckForProperty( + handleId: string, + property: WindowProperties, + value: string, + ): Promise { + await this.driver.switchTo().window(handleId); + const handleProperty = await this.getCurrentWindowProperties( + property, + handleId, + ); + + return handleProperty === value; + } + + /** + * If we already know this window, switch to it + * Otherwise, return null + * This is used in helpers.switchToOrOpenDapp() and when there's an alert open + * + * @param title - The title of the window we want to switch to + */ + async switchToWindowIfKnown(title: string) { + const handle = this.annotatedHandles.find((a) => a.title === title); + const handleId = handle?.id; + + if (handleId) { + await this.driver.switchTo().window(handleId); + return handleId; + } + + return null; + } +} diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index cb68df89e9c0..738d766f8555 100755 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -20,7 +20,10 @@ const ALL_PAGES = Object.values(PAGES); async function measurePage(pageName) { let metrics; await withFixtures( - { fixtures: new FixtureBuilder().build() }, + { + fixtures: new FixtureBuilder().build(), + disableServerMochaToBackground: true, + }, async ({ driver }) => { await driver.delay(tinyDelayMs); await unlockWallet(driver, { diff --git a/test/e2e/changedFilesUtil.js b/test/e2e/changedFilesUtil.js new file mode 100644 index 000000000000..19615317e121 --- /dev/null +++ b/test/e2e/changedFilesUtil.js @@ -0,0 +1,44 @@ +const fs = require('fs').promises; +const path = require('path'); + +const BASE_PATH = path.resolve(__dirname, '..', '..'); +const CHANGED_FILES_PATH = path.join( + BASE_PATH, + 'changed-files', + 'changed-files.txt', +); + +/** + * Reads the list of changed files from the git diff file. + * + * @returns {Promise} An array of changed file paths. + */ +async function readChangedFiles() { + try { + const data = await fs.readFile(CHANGED_FILES_PATH, 'utf8'); + const changedFiles = data.split('\n'); + return changedFiles; + } catch (error) { + console.error('Error reading from file:', error); + return []; + } +} + +/** + * Filters the list of changed files to include only E2E test files within the 'test/e2e/' directory. + * + * @returns {Promise} An array of filtered E2E test file paths. + */ +async function filterE2eChangedFiles() { + const changedFiles = await readChangedFiles(); + const e2eChangedFiles = changedFiles + .filter( + (file) => + file.startsWith('test/e2e/') && + (file.endsWith('.spec.js') || file.endsWith('.spec.ts')), + ) + .map((file) => `${BASE_PATH}/${file}`); + return e2eChangedFiles; +} + +module.exports = { filterE2eChangedFiles, readChangedFiles }; diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index ad1689e570d6..4dee1053aaee 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -20,7 +20,7 @@ export const BUNDLER_URL = 'http://localhost:3000/rpc'; /* URL of the 4337 account snap site. */ export const ERC_4337_ACCOUNT_SNAP_URL = - 'https://metamask.github.io/snap-account-abstraction-keyring/0.2.2/'; + 'https://metamask.github.io/snap-account-abstraction-keyring/0.4.1/'; /* Salt used to generate the 4337 account. */ export const ERC_4337_ACCOUNT_SALT = '0x1'; @@ -31,7 +31,13 @@ export const SIMPLE_ACCOUNT_FACTORY = /* URL of the Snap Simple Keyring site. */ export const TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL = - 'https://metamask.github.io/snap-simple-keyring/1.1.1/'; + 'https://metamask.github.io/snap-simple-keyring/1.1.2/'; /* Address of the VerifyingPaymaster smart contract deployed to Ganache. */ export const VERIFYING_PAYMASTER = '0xbdbDEc38ed168331b1F7004cc9e5392A2272C1D7'; + +/* Default ganache ETH balance in decimal when first login */ +export const DEFAULT_GANACHE_ETH_BALANCE_DEC = '25'; + +/* Default BTC address created using test SRP */ +export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 1ffe75134970..27db906cd754 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -1,4 +1,4 @@ -const { NetworkStatus } = require('@metamask/network-controller'); +const { mockNetworkState } = require('../stub/networks'); const { CHAIN_IDS } = require('../../shared/constants/network'); const { FirstTimeFlowType } = require('../../shared/constants/onboarding'); @@ -16,10 +16,10 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { UserStorageController: { isProfileSyncingEnabled: true, }, - MetamaskNotificationsController: { + NotificationServicesController: { subscriptionAccountsSeen: [], isFeatureAnnouncementsEnabled: false, - isMetamaskNotificationsEnabled: false, + isNotificationServicesEnabled: false, isMetamaskNotificationsFeatureSeen: false, metamaskNotificationsList: [], metamaskNotificationsReadList: [], @@ -41,7 +41,6 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -117,6 +116,15 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { }, snapsInstallPrivacyWarningShown: true, }, + BridgeController: { + bridgeState: { + bridgeFeatureFlags: { + extensionSupport: false, + srcNetworkAllowlist: ['0x1', '0xa', '0xe708'], + destNetworkAllowlist: ['0x1', '0xa', '0xe708'], + }, + }, + }, CurrencyController: { currentCurrency: 'usd', currencyRates: { @@ -145,32 +153,15 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { traits: {}, }, NetworkController: { - selectedNetworkClientId: 'networkConfigurationId', - networksMetadata: { - networkConfigurationId: { - EIPS: {}, - status: NetworkStatus.Available, - }, - }, - providerConfig: { + ...mockNetworkState({ + id: 'networkConfigurationId', chainId: inputChainId, nickname: 'Localhost 8545', - rpcPrefs: {}, rpcUrl: 'http://localhost:8545', ticker: 'ETH', - type: 'rpc', - id: 'networkConfigurationId', - }, - networkConfigurations: { - networkConfigurationId: { - chainId: inputChainId, - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - networkConfigurationId: 'networkConfigurationId', - }, - }, + blockExplorerUrl: undefined, + }), + providerConfig: { id: 'networkConfigurationId' }, }, OnboardingController: { completedOnboarding: true, @@ -208,7 +199,8 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { smartTransactionsOptInStatus: false, useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, - showTokenAutodetectModal: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, }, selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1', theme: 'light', diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 3560ad7f3ff7..ddc819b2bcf3 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -4,11 +4,11 @@ const { } = require('@metamask/snaps-utils'); const { merge } = require('lodash'); const { toHex } = require('@metamask/controller-utils'); -const { NetworkStatus } = require('@metamask/network-controller'); +const { mockNetworkState } = require('../stub/networks'); -const { CHAIN_IDS, NETWORK_TYPES } = require('../../shared/constants/network'); +const { CHAIN_IDS } = require('../../shared/constants/network'); const { SMART_CONTRACTS } = require('./seeder/smart-contracts'); -const { DAPP_URL, DAPP_ONE_URL } = require('./helpers'); +const { DAPP_URL, DAPP_ONE_URL, ACCOUNT_1 } = require('./helpers'); const { DEFAULT_FIXTURE_ACCOUNT, ERC_4337_ACCOUNT } = require('./constants'); const { defaultFixture, @@ -40,32 +40,15 @@ function onboardingFixture() { }, }, NetworkController: { - selectedNetworkClientId: 'networkConfigurationId', - networksMetadata: { - networkConfigurationId: { - EIPS: {}, - status: NetworkStatus.Available, - }, - }, - providerConfig: { - ticker: 'ETH', - type: 'rpc', - rpcUrl: 'http://localhost:8545', + ...mockNetworkState({ + id: 'networkConfigurationId', chainId: CHAIN_IDS.LOCALHOST, nickname: 'Localhost 8545', - id: 'networkConfigurationId', - }, - networkConfigurations: { - networkConfigurationId: { - chainId: CHAIN_IDS.LOCALHOST, - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - networkConfigurationId: 'networkConfigurationId', - type: 'rpc', - }, - }, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: undefined, + }), + providerConfig: { id: 'networkConfigurationId' }, }, PreferencesController: { advancedGasFee: null, @@ -87,7 +70,8 @@ function onboardingFixture() { smartTransactionsOptInStatus: false, useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, - showTokenAutodetectModal: false, + isRedesignedConfirmationsDeveloperEnabled: false, + showConfirmationAdvancedDetails: false, }, useExternalServices: true, theme: 'light', @@ -223,59 +207,48 @@ class FixtureBuilder { withNetworkController(data) { merge(this.fixture.data.NetworkController, data); + this.fixture.data.NetworkController.providerConfig = { + id: this.fixture.data.NetworkController.selectedNetworkClientId, + }; return this; } withNetworkControllerOnMainnet() { - merge(this.fixture.data.NetworkController, { - providerConfig: { - chainId: CHAIN_IDS.MAINNET, - nickname: '', - rpcUrl: '', - type: NETWORK_TYPES.MAINNET, - }, - }); - return this; + return this.withNetworkController({ selectedNetworkClientId: 'mainnet' }); } withNetworkControllerDoubleGanache() { - return this.withNetworkController({ - networkConfigurations: { - networkConfigurationId: { - chainId: CHAIN_IDS.LOCALHOST, - nickname: 'Localhost 8545', - rpcPrefs: {}, - rpcUrl: 'http://localhost:8545', - ticker: 'ETH', - networkConfigurationId: 'networkConfigurationId', - id: 'networkConfigurationId', - }, - '76e9cd59-d8e2-47e7-b369-9c205ccb602c': { - id: '76e9cd59-d8e2-47e7-b369-9c205ccb602c', - rpcUrl: 'http://localhost:8546', - chainId: '0x53a', - ticker: 'ETH', - nickname: 'Localhost 8546', - rpcPrefs: {}, - }, + const ganacheNetworks = mockNetworkState( + { + chainId: CHAIN_IDS.LOCALHOST, + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', }, - }); + { + id: '76e9cd59-d8e2-47e7-b369-9c205ccb602c', + rpcUrl: 'http://localhost:8546', + chainId: '0x53a', + ticker: 'ETH', + nickname: 'Localhost 8546', + }, + ); + delete ganacheNetworks.selectedNetworkClientId; + return this.withNetworkController(ganacheNetworks); } withNetworkControllerTripleGanache() { this.withNetworkControllerDoubleGanache(); - merge(this.fixture.data.NetworkController, { - networkConfigurations: { - '243ad4c2-10a6-4621-9536-e3a67f4dd4c9': { - id: '243ad4c2-10a6-4621-9536-e3a67f4dd4c9', - rpcUrl: 'http://localhost:7777', - chainId: '0x3e8', - ticker: 'ETH', - nickname: 'Localhost 7777', - rpcPrefs: {}, - }, - }, - }); + merge( + this.fixture.data.NetworkController, + mockNetworkState({ + rpcUrl: 'http://localhost:7777', + chainId: '0x3e8', + ticker: 'ETH', + nickname: 'Localhost 7777', + blockExplorerUrl: undefined, + }), + ); return this; } @@ -365,6 +338,19 @@ class FixtureBuilder { return this; } + withBridgeControllerDefaultState() { + this.fixture.data.BridgeController = { + bridgeState: { + bridgeFeatureFlags: { + destNetworkAllowlist: [], + extensionSupport: false, + srcNetworkAllowlist: [], + }, + }, + }; + return this; + } + withPermissionControllerConnectedToTestDapp(restrictReturnedAccounts = true) { return this.withPermissionController({ subjects: { @@ -393,6 +379,32 @@ class FixtureBuilder { }); } + withPermissionControllerSnapAccountConnectedToTestDapp( + restrictReturnedAccounts = true, + ) { + return this.withPermissionController({ + subjects: { + [DAPP_URL]: { + origin: DAPP_URL, + permissions: { + eth_accounts: { + id: 'ZaqPEWxyhNCJYACFw93jE', + parentCapability: 'eth_accounts', + invoker: DAPP_URL, + caveats: restrictReturnedAccounts && [ + { + type: 'restrictReturnedAccounts', + value: ['0x09781764c08de8ca82e156bbf156a3ca217c7950'], + }, + ], + date: 1664388714636, + }, + }, + }, + }, + }); + } + withPermissionControllerConnectedToTwoTestDapps( restrictReturnedAccounts = true, ) { @@ -539,6 +551,11 @@ class FixtureBuilder { }); } + withPreferencesControllerAndFeatureFlag(flags) { + merge(this.fixture.data.PreferencesController, flags); + return this; + } + withAccountsController(data) { merge(this.fixture.data.AccountsController, data); return this; @@ -555,7 +572,6 @@ class FixtureBuilder { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -576,7 +592,6 @@ class FixtureBuilder { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -596,7 +611,6 @@ class FixtureBuilder { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -625,7 +639,6 @@ class FixtureBuilder { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -646,7 +659,6 @@ class FixtureBuilder { options: {}, methods: [ 'personal_sign', - 'eth_sign', 'eth_signTransaction', 'eth_signTypedData_v1', 'eth_signTypedData_v3', @@ -736,7 +748,7 @@ class FixtureBuilder { return this; } - withTokensControllerERC20() { + withTokensControllerERC20({ chainId = 1337 } = {}) { merge(this.fixture.data.TokensController, { tokens: [ { @@ -752,7 +764,7 @@ class FixtureBuilder { ignoredTokens: [], detectedTokens: [], allTokens: { - [toHex(1337)]: { + [toHex(chainId)]: { '0x5cfe73b6021e818b776b421b1c4db2474086a7e1': [ { address: `__FIXTURE_SUBSTITUTION__CONTRACT${SMART_CONTRACTS.HST}`, @@ -1227,6 +1239,142 @@ class FixtureBuilder { }); } + withTransactionControllerOPLayer2Transaction() { + const FROM_ADDRESS = ACCOUNT_1; + const TRANSACTION_ID = 'f0fc75d0-181d-11ef-9546-8b2366f13afd'; + const TRANSACTION_TYPE = 'contractInteraction'; + const TEST_NETWORK_CLIENT_ID = 'networkConfigurationId'; + + return this.withTransactionController({ + transactions: { + [TRANSACTION_ID]: { + actionId: 3577139671, + chainId: '0xa', + dappSuggestedGasFees: { gas: '0x31f10' }, + defaultGasEstimates: { + estimateType: 'dappSuggested', + gas: '0x31f10', + maxFeePerGas: '0x3b014b3', + maxPriorityFeePerGas: '0x3b014b3', + }, + gasFeeEstimates: { gasPrice: '0x3b202d0', type: 'eth_gasPrice' }, + gasFeeEstimatesLoaded: true, + history: [ + { + actionId: 3577139671, + chainId: '0xa', + dappSuggestedGasFees: { gas: '0x31f10' }, + defaultGasEstimates: { + estimateType: 'dappSuggested', + gas: '0x31f10', + maxFeePerGas: '0x3b014b3', + maxPriorityFeePerGas: '0x3b014b3', + }, + id: TRANSACTION_ID, + layer1GasFee: '0x175283ae57', + networkClientId: TEST_NETWORK_CLIENT_ID, + origin: 'https://metamask.github.io', + securityAlertResponse: { + reason: 'loading', + result_type: 'validation_in_progress', + securityAlertId: '30626504-0069-4278-9e2e-3d7fba2e6aef', + }, + sendFlowHistory: [], + status: 'unapproved', + time: 1716370234797, + txParams: { + data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + from: FROM_ADDRESS, + gas: '0x31f10', + maxFeePerGas: '0x3b014b3', + maxPriorityFeePerGas: '0x3b014b3', + value: '0x0', + }, + type: TRANSACTION_TYPE, + userEditedGasLimit: false, + userFeeLevel: 'dappSuggested', + verifiedOnBlockchain: false, + }, + [ + { + note: 'TransactionController#updateSimulationData - Update simulation data', + op: 'add', + path: '/simulationData', + timestamp: 1716370235743, + value: { + error: { code: 'disabled', message: 'Simulation disabled' }, + tokenBalanceChanges: [], + }, + }, + ], + [ + { + note: 'TransactionController:updatesecurityAlertResponse - securityAlertResponse updated', + op: 'replace', + path: '/securityAlertResponse/result_type', + timestamp: 1716370236091, + value: 'Benign', + }, + { + op: 'replace', + path: '/securityAlertResponse/reason', + value: '', + }, + { + op: 'add', + path: '/securityAlertResponse/description', + value: '', + }, + { op: 'add', path: '/securityAlertResponse/features', value: [] }, + { + op: 'add', + path: '/securityAlertResponse/block', + value: 120385722, + }, + { + op: 'add', + path: '/gasFeeEstimates', + value: { gasPrice: '0x3b014b3', type: 'eth_gasPrice' }, + }, + { op: 'add', path: '/gasFeeEstimatesLoaded', value: true }, + ], + ], + id: TRANSACTION_ID, + layer1GasFee: '0x19fdabf615', + networkClientId: TEST_NETWORK_CLIENT_ID, + origin: 'https://metamask.github.io', + securityAlertResponse: { + block: 120385722, + description: '', + features: [], + reason: '', + result_type: 'Benign', + securityAlertId: '30626504-0069-4278-9e2e-3d7fba2e6aef', + }, + sendFlowHistory: [], + simulationData: { + error: { code: 'disabled', message: 'Simulation disabled' }, + tokenBalanceChanges: [], + }, + status: 'unapproved', + time: 1716370234797, + txParams: { + data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + from: FROM_ADDRESS, + gas: '0x31f10', + maxFeePerGas: '0x3b014b3', + maxPriorityFeePerGas: '0x3b014b3', + value: '0x0', + }, + type: TRANSACTION_TYPE, + userEditedGasLimit: false, + userFeeLevel: 'dappSuggested', + verifiedOnBlockchain: false, + }, + }, + }); + } + withTransactionControllerApprovedTransaction() { return this.withTransactionController({ transactions: { diff --git a/test/e2e/flask/btc/btc-account-overview.spec.ts b/test/e2e/flask/btc/btc-account-overview.spec.ts new file mode 100644 index 000000000000..bce4c45e9c04 --- /dev/null +++ b/test/e2e/flask/btc/btc-account-overview.spec.ts @@ -0,0 +1,47 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { withBtcAccountSnap } from './common-btc'; + +describe('BTC Account - Overview', function (this: Suite) { + it('has portfolio button enabled for BTC accounts', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + await driver.waitForSelector({ + text: 'Send', + tag: 'button', + css: '[disabled]', + }); + + await driver.waitForSelector({ + text: 'Swap', + tag: 'button', + css: '[disabled]', + }); + + await driver.waitForSelector({ + text: 'Bridge', + tag: 'button', + css: '[disabled]', + }); + + const buySellButton = await driver.waitForSelector( + '[data-testid="coin-overview-buy"]', + ); + // Ramps now support buyable chains dynamically (https://github.com/MetaMask/metamask-extension/pull/24041), for now it's + // disabled for Bitcoin + assert.equal(await buySellButton.isEnabled(), false); + + const portfolioButton = await driver.waitForSelector( + '[data-testid="coin-overview-receive"]', + ); + assert.equal(await portfolioButton.isEnabled(), true); + }, + ); + }); +}); diff --git a/test/e2e/flask/btc/btc-dapp-connection.spec.ts b/test/e2e/flask/btc/btc-dapp-connection.spec.ts new file mode 100644 index 000000000000..2eb33a4b7d74 --- /dev/null +++ b/test/e2e/flask/btc/btc-dapp-connection.spec.ts @@ -0,0 +1,21 @@ +import { Suite } from 'mocha'; +import { openDapp, WINDOW_TITLES } from '../../helpers'; +import { withBtcAccountSnap } from './common-btc'; + +describe('BTC Account - Dapp Connection', function (this: Suite) { + it('cannot connect to dapps', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await openDapp(driver); + await driver.clickElement('#connectButton'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.assertElementNotPresent( + '[data-testid="choose-account-list-1"]', + ); + }, + ); + }); +}); diff --git a/test/e2e/flask/btc/btc-experimental-settings.spec.ts b/test/e2e/flask/btc/btc-experimental-settings.spec.ts new file mode 100644 index 000000000000..45bc7d0d1701 --- /dev/null +++ b/test/e2e/flask/btc/btc-experimental-settings.spec.ts @@ -0,0 +1,48 @@ +import { Suite } from 'mocha'; + +import messages from '../../../../app/_locales/en/messages.json'; +import FixtureBuilder from '../../fixture-builder'; +import { + defaultGanacheOptions, + unlockWallet, + withFixtures, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; + +describe('BTC Experimental Settings', function (this: Suite) { + it('will show `Add a new Bitcoin account (Beta)` option when setting is enabled', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ text: 'Experimental', tag: 'div' }); + + await driver.waitForSelector({ + text: messages.bitcoinSupportToggleTitle.message, + tag: 'span', + }); + + await driver.clickElement('[data-testid="bitcoin-support-toggle-div"]'); + + await driver.clickElement('button[aria-label="Close"]'); + + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.waitForSelector({ + text: messages.addNewBitcoinAccount.message, + tag: 'button', + }); + }, + ); + }); +}); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts new file mode 100644 index 000000000000..0bcf1663b501 --- /dev/null +++ b/test/e2e/flask/btc/common-btc.ts @@ -0,0 +1,59 @@ +import { Mockttp } from 'mockttp'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, unlockWallet } from '../../helpers'; +import { DEFAULT_BTC_ACCOUNT } from '../../constants'; +import { Driver } from '../../webdriver/driver'; +import { createBtcAccount } from '../../accounts/common'; + +const GENERATE_MOCK_BTC_BALANCE_CALL = ( + address: string = DEFAULT_BTC_ACCOUNT, +): { data: { [address: string]: number } } => { + return { + data: { + [address]: 9999, + }, + }; +}; + +export async function mockBtcBalanceQuote( + mockServer: Mockttp, + address: string = DEFAULT_BTC_ACCOUNT, +) { + return [ + await mockServer + .forGet(/https:\/\/api\.blockchair\.com\/bitcoin\/addresses\/balances/u) + .withQuery({ + addresses: address, + }) + .thenCallback(() => ({ + statusCode: 200, + json: GENERATE_MOCK_BTC_BALANCE_CALL(address), + })), + ]; +} + +export async function withBtcAccountSnap( + { + title, + bitcoinSupportEnabled, + }: { title?: string; bitcoinSupportEnabled?: boolean }, + test: (driver: Driver) => Promise, +) { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withPreferencesControllerAndFeatureFlag({ + bitcoinSupportEnabled: bitcoinSupportEnabled ?? true, + }) + .build(), + title, + dapp: true, + testSpecificMock: mockBtcBalanceQuote, + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await createBtcAccount(driver); + await test(driver); + }, + ); +} diff --git a/test/e2e/flask/btc/create-btc-account.spec.ts b/test/e2e/flask/btc/create-btc-account.spec.ts new file mode 100644 index 000000000000..a6031a956a37 --- /dev/null +++ b/test/e2e/flask/btc/create-btc-account.spec.ts @@ -0,0 +1,179 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import messages from '../../../../app/_locales/en/messages.json'; + +import { + WALLET_PASSWORD, + completeSRPRevealQuiz, + getSelectedAccountAddress, + openSRPRevealQuiz, + removeSelectedAccount, + tapAndHoldToRevealSRP, +} from '../../helpers'; +import { createBtcAccount } from '../../accounts/common'; +import { withBtcAccountSnap } from './common-btc'; + +describe('Create BTC Account', function (this: Suite) { + it('create BTC account from the menu', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + }, + ); + }); + + it('cannot create multiple BTC accounts', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.delay(500); + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + + const createButton = await driver.findElement({ + text: messages.addNewBitcoinAccount.message, + tag: 'button', + }); + assert.equal(await createButton.isEnabled(), false); + + // modal will still be here + await driver.clickElement('.mm-box button[aria-label="Close"]'); + + // check the number of accounts. it should only be 2. + await driver.clickElement('[data-testid="account-menu-icon"]'); + const menuItems = await driver.findElements( + '.multichain-account-list-item', + ); + assert.equal(menuItems.length, 2); + }, + ); + }); + + it('can cancel the removal of BTC account', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', + ); + await driver.clickElement('[data-testid="account-list-menu-remove"]'); + await driver.clickElement({ text: 'Nevermind', tag: 'button' }); + + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + // check the number of accounts. it should only be 2. + await driver.clickElement('[data-testid="account-menu-icon"]'); + const menuItems = await driver.findElements( + '.multichain-account-list-item', + ); + assert.equal(menuItems.length, 2); + }, + ); + }); + + it('can recreate BTC account after deleting it', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + const accountAddress = await getSelectedAccountAddress(driver); + await removeSelectedAccount(driver); + + // Recreate account + await createBtcAccount(driver); + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + const recreatedAccountAddress = await getSelectedAccountAddress(driver); + assert(accountAddress === recreatedAccountAddress); + }, + ); + }); + + it('can recreate BTC account after restoring wallet with SRP', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver) => { + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + const accountAddress = await getSelectedAccountAddress(driver); + + await openSRPRevealQuiz(driver); + await completeSRPRevealQuiz(driver); + await driver.fill('[data-testid="input-password"]', WALLET_PASSWORD); + await driver.press('[data-testid="input-password"]', driver.Key.ENTER); + await tapAndHoldToRevealSRP(driver); + const seedPhrase = await ( + await driver.findElement('[data-testid="srp_text"]') + ).getText(); + + // Reset wallet + await driver.clickElement( + '[data-testid="account-options-menu-button"]', + ); + const lockButton = await driver.findClickableElement( + '[data-testid="global-menu-lock"]', + ); + assert.equal(await lockButton.getText(), 'Lock MetaMask'); + await lockButton.click(); + + await driver.clickElement({ + text: 'Forgot password?', + tag: 'a', + }); + + await driver.pasteIntoField( + '[data-testid="import-srp__srp-word-0"]', + seedPhrase, + ); + + await driver.fill( + '[data-testid="create-vault-password"]', + WALLET_PASSWORD, + ); + await driver.fill( + '[data-testid="create-vault-confirm-password"]', + WALLET_PASSWORD, + ); + + await driver.clickElement({ + text: 'Restore', + tag: 'button', + }); + + await createBtcAccount(driver); + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Bitcoin Account', + }); + + const recreatedAccountAddress = await getSelectedAccountAddress(driver); + assert(accountAddress === recreatedAccountAddress); + }, + ); + }); +}); diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts index d84779ba79b6..be7141444c97 100644 --- a/test/e2e/flask/user-operations.spec.ts +++ b/test/e2e/flask/user-operations.spec.ts @@ -22,6 +22,8 @@ import { import { buildQuote, reviewQuote } from '../tests/swaps/shared'; import { Driver } from '../webdriver/driver'; import { Bundler } from '../bundler'; +import { SWAP_TEST_ETH_USDC_TRADES_MOCK } from '../../data/mock-data'; +import { Mockttp } from '../mock-e2e'; enum TransactionDetailRowIndex { Nonce = 0, @@ -31,17 +33,19 @@ enum TransactionDetailRowIndex { async function installExampleSnap(driver: Driver) { await driver.openNewPage(ERC_4337_ACCOUNT_SNAP_URL); await driver.clickElement('#connectButton'); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', }); await driver.findElement({ text: 'Add to MetaMask', tag: 'h3' }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + await driver.clickElementSafe('[data-testid="snap-install-scroll"]', 200); + await driver.waitForSelector({ text: 'Confirm' }); await driver.clickElement({ text: 'Confirm', tag: 'button', }); + await driver.waitForSelector({ text: 'OK' }); await driver.clickElement({ text: 'OK', tag: 'button', @@ -60,6 +64,7 @@ async function createSnapAccount( await driver.clickElement({ text: 'Create Account', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.clickElement({ text: 'Ok', tag: 'button' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.ERC4337Snap); } @@ -71,24 +76,38 @@ async function setSnapConfig( entrypoint, simpleAccountFactory, paymaster, + paymasterSK, }: { bundlerUrl: string; entrypoint: string; simpleAccountFactory: string; paymaster?: string; + paymasterSK?: string; }, ) { - const data = JSON.stringify({ - bundlerUrl, - entryPoint: entrypoint, + await driver.switchToWindowWithTitle('Account Abstraction Snap'); + await driver.clickElement('[data-testid="chain-select"]'); + await driver.clickElement('[data-testid="chain-id-1337"]'); + await driver.fill('[data-testid="bundlerUrl"]', bundlerUrl); + await driver.fill('[data-testid="entryPoint"]', entrypoint); + await driver.fill( + '[data-testid="simpleAccountFactory"]', simpleAccountFactory, - customVerifyingPaymasterAddress: paymaster, - }); + ); + if (paymaster) { + await driver.fill( + '[data-testid="customVerifyingPaymasterAddress"]', + paymaster, + ); + } + if (paymasterSK) { + await driver.fill( + '[data-testid="customVerifyingPaymasterSK"]', + paymasterSK, + ); + } - await driver.switchToWindowWithTitle('Account Abstraction Snap'); - await driver.clickElement({ text: 'Set Chain Config' }); - await driver.fill('#set-chain-config-chain-config-object', data); - await driver.clickElement({ text: 'Set Chain Configs', tag: 'button' }); + await driver.clickElement({ text: 'Set Chain Config', tag: 'button' }); } async function createSwap(driver: Driver) { @@ -102,6 +121,7 @@ async function createSwap(driver: Driver) { swapFrom: 'TESTETH', swapTo: 'USDC', }); + await driver.clickElement({ text: 'Swap', tag: 'button' }); await driver.clickElement({ text: 'Close', tag: 'button' }); } @@ -167,6 +187,17 @@ async function expectTransactionDetailsMatchReceipt( ); } +async function mockSwapsTransactionQuote(mockServer: Mockttp) { + return [ + await mockServer + .forGet('https://swap.api.cx.metamask.io/networks/1/trades') + .thenCallback(() => ({ + statusCode: 200, + json: SWAP_TEST_ETH_USDC_TRADES_MOCK, + })), + ]; +} + async function withAccountSnap( { title, paymaster }: { title?: string; paymaster?: string }, test: (driver: Driver, bundlerServer: Bundler) => Promise, @@ -183,6 +214,7 @@ async function withAccountSnap( ganacheOptions: { hardfork: 'london', }, + testSpecificMock: mockSwapsTransactionQuote, }, async ({ driver, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index cab7c2fb1157..afef379fb361 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -16,7 +16,13 @@ const { PAGES } = require('./webdriver/driver'); const GanacheSeeder = require('./seeder/ganache-seeder'); const { Bundler } = require('./bundler'); const { SMART_CONTRACTS } = require('./seeder/smart-contracts'); -const { ERC_4337_ACCOUNT } = require('./constants'); +const { + ERC_4337_ACCOUNT, + DEFAULT_GANACHE_ETH_BALANCE_DEC, +} = require('./constants'); +const { + getServerMochaToBackground, +} = require('./background-socket/server-mocha-to-background'); const tinyDelayMs = 200; const regularDelayMs = tinyDelayMs * 2; @@ -33,6 +39,22 @@ const convertToHexValue = (val) => `0x${new BigNumber(val, 10).toString(16)}`; const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18); +/** + * @typedef {object} Fixtures + * @property {import('./webdriver/driver').Driver} driver - The driver number. + * @property {GanacheContractAddressRegistry | undefined} contractRegistry - The contract registry. + * @property {Ganache | undefined} ganacheServer - The Ganache server. + * @property {Ganache | undefined} secondaryGanacheServer - The secondary Ganache server. + * @property {mockttp.MockedEndpoint[]} mockedEndpoint - The mocked endpoint. + * @property {Bundler} bundlerServer - The bundler server. + * @property {mockttp.Mockttp} mockServer - The mock server. + */ + +/** + * + * @param {object} options + * @param {(fixtures: Fixtures) => Promise} testSuite + */ async function withFixtures(options, testSuite) { const { dapp, @@ -45,6 +67,7 @@ async function withFixtures(options, testSuite) { ignoredConsoleErrors = [], dappPath = undefined, disableGanache, + disableServerMochaToBackground = false, dappPaths, testSpecificMock = function () { // do nothing. @@ -67,6 +90,10 @@ async function withFixtures(options, testSuite) { const dappServer = []; const phishingPageServer = new PhishingWarningPageServer(); + if (!disableServerMochaToBackground) { + getServerMochaToBackground(); + } + let webDriver; let driver; let failed = false; @@ -168,9 +195,9 @@ async function withFixtures(options, testSuite) { if (typeof originalProperty === 'function') { return (...args) => { console.log( - `[driver] Called '${prop}' with arguments ${JSON.stringify( + `${new Date().toISOString()} [driver] Called '${prop}' with arguments ${JSON.stringify( args, - ).slice(0, 200)}`, // limit the length of the log entry to 200 characters + ).slice(0, 224)}`, // limit the length of the log entry to 224 characters ); return originalProperty.bind(target)(...args); }; @@ -613,6 +640,12 @@ const testSRPDropdownIterations = async (options, driver, iterations) => { const openSRPRevealQuiz = async (driver) => { // navigate settings to reveal SRP await driver.clickElement('[data-testid="account-options-menu-button"]'); + + // fix race condition with mmi build + if (process.env.MMI) { + await driver.waitForSelector('[data-testid="global-menu-mmi-portfolio"]'); + } + await driver.clickElement({ text: 'Settings', tag: 'div' }); await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); await driver.clickElement('[data-testid="reveal-seed-words"]'); @@ -695,19 +728,19 @@ const switchToOrOpenDapp = async ( contract = null, dappURL = DAPP_URL, ) => { - try { - // Do an unusually fast switchToWindowWithTitle, just 1 second - await driver.switchToWindowWithTitle( - WINDOW_TITLES.TestDApp, - null, - 1000, - 1000, - ); - } catch { + const handle = await driver.windowHandles.switchToWindowIfKnown( + WINDOW_TITLES.TestDApp, + ); + + if (!handle) { await openDapp(driver, contract, dappURL); } }; +/** + * + * @param {import('./webdriver/driver').Driver} driver + */ const connectToDapp = async (driver) => { await openDapp(driver); // Connect to dapp @@ -716,12 +749,12 @@ const connectToDapp = async (driver) => { tag: 'button', }); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Next', tag: 'button', }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Confirm', tag: 'button', }); @@ -732,31 +765,48 @@ const PRIVATE_KEY = '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC'; const PRIVATE_KEY_TWO = - '0xa444f52ea41e3a39586d7069cb8e8233e9f6b9dea9cbb700cce69ae860661cc8'; + '0xf444f52ea41e3a39586d7069cb8e8233e9f6b9dea9cbb700cce69ae860661cc8'; const ACCOUNT_1 = '0x5cfe73b6021e818b776b421b1c4db2474086a7e1'; const ACCOUNT_2 = '0x09781764c08de8ca82e156bbf156a3ca217c7950'; const defaultGanacheOptions = { - accounts: [{ secretKey: PRIVATE_KEY, balance: convertETHToHexGwei(25) }], + accounts: [ + { + secretKey: PRIVATE_KEY, + balance: convertETHToHexGwei(DEFAULT_GANACHE_ETH_BALANCE_DEC), + }, + ], +}; + +const defaultGanacheOptionsForType2Transactions = { + ...defaultGanacheOptions, + // EVM version that supports type 2 transactions (EIP1559) + hardfork: 'london', }; const multipleGanacheOptions = { accounts: [ { secretKey: PRIVATE_KEY, - balance: convertETHToHexGwei(25), + balance: convertETHToHexGwei(DEFAULT_GANACHE_ETH_BALANCE_DEC), }, { secretKey: PRIVATE_KEY_TWO, - balance: convertETHToHexGwei(25), + balance: convertETHToHexGwei(DEFAULT_GANACHE_ETH_BALANCE_DEC), }, ], }; +const multipleGanacheOptionsForType2Transactions = { + ...multipleGanacheOptions, + // EVM version that supports type 2 transactions (EIP1559) + hardfork: 'london', +}; + const generateGanacheOptions = ({ secretKey = PRIVATE_KEY, - balance = convertETHToHexGwei(25), + balance = convertETHToHexGwei(DEFAULT_GANACHE_ETH_BALANCE_DEC), ...otherProps }) => { const accounts = [ @@ -783,7 +833,6 @@ const editGasFeeForm = async (driver, gasLimit, gasPrice) => { }; const openActionMenuAndStartSendFlow = async (driver) => { - await driver.delay(500); await driver.clickElement('[data-testid="eth-overview-send"]'); }; @@ -807,22 +856,10 @@ const sendScreenToConfirmScreen = async ( await driver.fill('.unit-input__input', quantity); // check if element exists and click it - await driver - .findElement({ - text: 'I understand', - tag: 'button', - }) - .then( - (_found) => { - driver.clickElement({ - text: 'I understand', - tag: 'button', - }); - }, - (error) => { - console.error('Element not found.', error); - }, - ); + await driver.clickElementSafe({ + text: 'I understand', + tag: 'button', + }); await driver.clickElement({ text: 'Continue', tag: 'button' }); }; @@ -876,11 +913,23 @@ const TEST_SEED_PHRASE = const TEST_SEED_PHRASE_TWO = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'; -// Usually happens when onboarded to make sure the state is retrieved from metamaskState properly, or after txn is made -const locateAccountBalanceDOM = async (driver, ganacheServer) => { +/** + * Checks the balance for a specific address. If no address is provided, it defaults to the first address. + * This function is typically used during onboarding to ensure the state is retrieved correctly from metamaskState, + * or after a transaction is made. + * + * @param {WebDriver} driver - The WebDriver instance. + * @param {Ganache} [ganacheServer] - The Ganache server instance (optional). + * @param {string} [address] - The address to check the balance for (optional). + */ +const locateAccountBalanceDOM = async ( + driver, + ganacheServer, + address = null, +) => { const balanceSelector = '[data-testid="eth-overview__primary-currency"]'; if (ganacheServer) { - const balance = await ganacheServer.getBalance(); + const balance = await ganacheServer.getBalance(address); await driver.waitForSelector({ css: balanceSelector, text: `${balance} ETH`, @@ -956,12 +1005,10 @@ function genRandInitBal(minETHBal = 10, maxETHBal = 100, decimalPlaces = 4) { * * @param {object} options - Options for the function. * @param {WebDriver} options.driver - The WebDriver instance controlling the browser. - * @param {string} [options.locatorID] - ID of the signature element (if any). * @param {boolean} [options.snapSigInsights] - Whether to wait for the insights snap to be ready before clicking the sign button. */ async function clickSignOnSignatureConfirmation({ driver, - locatorID = null, snapSigInsights = false, }) { if (snapSigInsights) { @@ -971,15 +1018,6 @@ async function clickSignOnSignatureConfirmation({ } await driver.clickElement({ text: 'Sign', tag: 'button' }); - - // #ethSign has a second Sign confirmation button that says "Your funds may be at risk" - if (locatorID === '#ethSign') { - await driver.clickElement({ - text: 'Sign', - tag: 'button', - css: '[data-testid="signature-warning-sign-button"]', - }); - } } /** @@ -990,36 +1028,24 @@ async function clickSignOnSignatureConfirmation({ * @param {WebDriver} driver */ async function validateContractDetails(driver) { - const verifyContractDetailsButton = await driver.findElement( - '.signature-request-content__verify-contract-details', - ); + const verifyDetailsBtnSelector = + '.signature-request-content__verify-contract-details'; - verifyContractDetailsButton.click(); + await driver.clickElement(verifyDetailsBtnSelector); await driver.clickElement({ text: 'Got it', tag: 'button' }); - // Approve signing typed data - try { - await driver.clickElement( - '[data-testid="signature-request-scroll-button"]', - ); - } catch (error) { - // Ignore error if scroll button is not present - } - await driver.delay(regularDelayMs); + await driver.clickElementSafe( + '[data-testid="signature-request-scroll-button"]', + ); } /** - * This method assumes the extension is open, the dapp is open and waits for a - * third window handle to open (the notification window). Once it does it - * switches to the new window. - * + * @deprecated since the background socket was added, and special handling is no longer necessary + * Just call `await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog)` instead. * @param {WebDriver} driver - * @param numHandles */ -async function switchToNotificationWindow(driver, numHandles = 3) { - const windowHandles = await driver.waitUntilXWindowHandles(numHandles); - - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog, windowHandles); +async function switchToNotificationWindow(driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); } /** @@ -1028,9 +1054,9 @@ async function switchToNotificationWindow(driver, numHandles = 3) { * for each mock in the array. * * @param {WebDriver} driver - * @param {import('mockttp').Mockttp} mockedEndpoints + * @param {import('mockttp').MockedEndpoint[]} mockedEndpoints * @param {boolean} hasRequest - * @returns {import('mockttp/dist/pluggable-admin').MockttpClientResponse[]} + * @returns {Promise} */ async function getEventPayloads(driver, mockedEndpoints, hasRequest = true) { await driver.wait( @@ -1126,6 +1152,80 @@ async function initBundler(bundlerServer, ganacheServer, usePaymaster) { } } +async function removeSelectedAccount(driver) { + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '.multichain-account-list-item--selected [data-testid="account-list-item-menu-button"]', + ); + await driver.clickElement('[data-testid="account-list-menu-remove"]'); + await driver.clickElement({ text: 'Remove', tag: 'button' }); +} + +async function getSelectedAccountAddress(driver) { + await driver.clickElement('[data-testid="account-options-menu-button"]'); + await driver.clickElement('[data-testid="account-list-menu-details"]'); + const accountAddress = await ( + await driver.findElement('[data-testid="address-copy-button-text"]') + ).getText(); + await driver.clickElement('.mm-box button[aria-label="Close"]'); + + return accountAddress; +} + +/** + * Rather than using the FixtureBuilder#withPreferencesController to set the setting + * we need to manually set the setting because the migration #122 overrides this. + * We should be able to remove this when we delete the redesignedConfirmationsEnabled setting. + * + * @param driver + */ +async function tempToggleSettingRedesignedConfirmations(driver) { + // Ensure we are on the extension window + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + // Open settings menu button + const accountOptionsMenuSelector = + '[data-testid="account-options-menu-button"]'; + await driver.waitForSelector(accountOptionsMenuSelector); + await driver.clickElement(accountOptionsMenuSelector); + + // fix race condition with mmi build + if (process.env.MMI) { + await driver.waitForSelector('[data-testid="global-menu-mmi-portfolio"]'); + } + + // Click settings from dropdown menu + await driver.clickElement('[data-testid="global-menu-settings"]'); + + // Click Experimental tab + const experimentalTabRawLocator = { + text: 'Experimental', + tag: 'div', + }; + await driver.clickElement(experimentalTabRawLocator); + + // Click redesignedConfirmationsEnabled toggle + await driver.clickElement( + '[data-testid="toggle-redesigned-confirmations-container"]', + ); +} + +/** + * Opens the account options menu safely, handling potential race conditions + * with the MMI build. + * + * @param {WebDriver} driver - The WebDriver instance used to interact with the browser. + * @returns {Promise} A promise that resolves when the menu is opened and any necessary waits are complete. + */ +async function openMenuSafe(driver) { + await driver.clickElement('[data-testid="account-options-menu-button"]'); + + // fix race condition with mmi build + if (process.env.MMI) { + await driver.waitForSelector('[data-testid="global-menu-mmi-portfolio"]'); + } +} + module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, @@ -1164,6 +1264,8 @@ module.exports = { connectToDapp, multipleGanacheOptions, defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + multipleGanacheOptionsForType2Transactions, sendTransaction, sendScreenToConfirmScreen, findAnotherAccountFromAccountList, @@ -1192,4 +1294,8 @@ module.exports = { getCleanAppState, editGasFeeForm, clickNestedButton, + removeSelectedAccount, + getSelectedAccountAddress, + tempToggleSettingRedesignedConfirmations, + openMenuSafe, }; diff --git a/test/e2e/json-rpc/switchEthereumChain.spec.js b/test/e2e/json-rpc/switchEthereumChain.spec.js index ef195f4b9a2b..75715b6ff00b 100644 --- a/test/e2e/json-rpc/switchEthereumChain.spec.js +++ b/test/e2e/json-rpc/switchEthereumChain.spec.js @@ -9,6 +9,7 @@ const { switchToNotificationWindow, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); +const { isManifestV3 } = require('../../../shared/modules/mv3.utils'); describe('Switch Ethereum Chain for two dapps', function () { it('switches the chainId of two dapps when switchEthereumChain of one dapp is confirmed', async function () { @@ -50,7 +51,9 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.clickElement(experimentalTabRawLocator); // Toggle off request queue setting (on by default now) - await driver.clickElement('.request-queue-toggle'); + await driver.clickElement( + '[data-testid="experimental-setting-toggle-request-queue"]', + ); // open two dapps const dappOne = await openDapp(driver, undefined, DAPP_URL); @@ -99,7 +102,7 @@ describe('Switch Ethereum Chain for two dapps', function () { ); }); - it('should queue switchEthereumChain request from second dapp after send tx request', async function () { + it('queues switchEthereumChain request from second dapp after send tx request', async function () { await withFixtures( { dapp: true, @@ -137,7 +140,9 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.clickElement(experimentalTabRawLocator); // Toggle off request queue setting (on by default now) - await driver.clickElement('.request-queue-toggle'); + await driver.clickElement( + '[data-testid="experimental-setting-toggle-request-queue"]', + ); // open two dapps const dappOne = await openDapp(driver, undefined, DAPP_URL); @@ -189,7 +194,7 @@ describe('Switch Ethereum Chain for two dapps', function () { ); }); - it('should queue send tx after switchEthereum request with a warning, confirming removes pending tx', async function () { + it('queues send tx after switchEthereum request with a warning, confirming removes pending tx', async function () { await withFixtures( { dapp: true, @@ -227,7 +232,9 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.clickElement(experimentalTabRawLocator); // Toggle off request queue setting (on by default now) - await driver.clickElement('.request-queue-toggle'); + await driver.clickElement( + '[data-testid="experimental-setting-toggle-request-queue"]', + ); // open two dapps const dappOne = await openDapp(driver, undefined, DAPP_URL); @@ -275,18 +282,14 @@ describe('Switch Ethereum Chain for two dapps', function () { // if this is an MV3 build(3 or 4 total) await driver.wait(async () => { const windowHandles = await driver.getAllWindowHandles(); - const numberOfWindowHandlesToExpect = - process.env.ENABLE_MV3 === 'true' || - process.env.ENABLE_MV3 === undefined - ? 4 - : 3; + const numberOfWindowHandlesToExpect = isManifestV3 ? 4 : 3; return windowHandles.length === numberOfWindowHandlesToExpect; }); }, ); }); - it('should queue send tx after switchEthereum request with a warning, if switchEthereum request is cancelled should show pending tx', async function () { + it('queues send tx after switchEthereum request with a warning, if switchEthereum request is cancelled should show pending tx', async function () { await withFixtures( { dapp: true, @@ -324,7 +327,9 @@ describe('Switch Ethereum Chain for two dapps', function () { await driver.clickElement(experimentalTabRawLocator); // Toggle off request queue setting (on by default now) - await driver.clickElement('.request-queue-toggle'); + await driver.clickElement( + '[data-testid="experimental-setting-toggle-request-queue"]', + ); // open two dapps const dappOne = await openDapp(driver, undefined, DAPP_URL); diff --git a/test/e2e/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png b/test/e2e/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png deleted file mode 100644 index 2e39ce7a48db..000000000000 Binary files a/test/e2e/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png and /dev/null differ diff --git a/test/e2e/mock-e2e.js b/test/e2e/mock-e2e.js index f3279377114d..c71cc00c7a32 100644 --- a/test/e2e/mock-e2e.js +++ b/test/e2e/mock-e2e.js @@ -1,10 +1,17 @@ const fs = require('fs'); +const { + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, +} = require('../../shared/constants/bridge'); const { GAS_API_BASE_URL, SWAPS_API_V2_BASE_URL, TOKEN_API_BASE_URL, } = require('../../shared/constants/swaps'); +const { + DEFAULT_FEATURE_FLAGS_RESPONSE: BRIDGE_DEFAULT_FEATURE_FLAGS_RESPONSE, +} = require('./tests/bridge/constants'); const CDN_CONFIG_PATH = 'test/e2e/mock-cdn/cdn-config.txt'; const CDN_STALE_DIFF_PATH = 'test/e2e/mock-cdn/cdn-stale-diff.txt'; @@ -70,11 +77,11 @@ const browserAPIRequestDomains = * Setup E2E network mocks. * * @param {Mockttp} server - The mock server used for network mocks. - * @param {(server: Mockttp) => MockedEndpoint} testSpecificMock - A function for setting up test-specific network mocks + * @param {(server: Mockttp) => Promise} testSpecificMock - A function for setting up test-specific network mocks * @param {object} options - Network mock options. * @param {string} options.chainId - The chain ID used by the default configured network. * @param {string} options.ethConversionInUsd - The USD conversion rate for ETH. - * @returns {SetupMockReturn} + * @returns {Promise} */ async function setupMocking( server, @@ -293,6 +300,19 @@ async function setupMocking( }; }); + [ + `${BRIDGE_DEV_API_BASE_URL}/getAllFeatureFlags`, + `${BRIDGE_PROD_API_BASE_URL}/getAllFeatureFlags`, + ].forEach( + async (url) => + await server.forGet(url).thenCallback(() => { + return { + statusCode: 200, + json: BRIDGE_DEFAULT_FEATURE_FLAGS_RESPONSE, + }; + }), + ); + await server .forGet(`https://token.api.cx.metamask.io/tokens/${chainId}`) .thenCallback(() => { @@ -619,6 +639,12 @@ async function setupMocking( // Notification APIs mockNotificationServices(server); + await server.forGet(/^https:\/\/sourcify.dev\/(.*)/u).thenCallback(() => { + return { + statusCode: 404, + }; + }); + /** * Returns an array of alphanumerically sorted hostnames that were requested * during the current test suite. @@ -629,6 +655,15 @@ async function setupMocking( return [...privacyReport].sort(); } + /** + * Excludes hosts from the privacyReport if they are refered to by the MetaMask Portfolio + * in a different tab. This is because the Portfolio is a separate application + * + * @param request + */ + const portfolioRequestsMatcher = (request) => + request.headers.referer === 'https://portfolio.metamask.io/'; + /** * Listen for requests and add the hostname to the privacy report if it did * not previously exist. This is used to track which hosts are requested @@ -638,7 +673,10 @@ async function setupMocking( * operation. See the browserAPIRequestDomains regex above. */ server.on('request-initiated', (request) => { - if (request.headers.host.match(browserAPIRequestDomains) === null) { + if ( + request.headers.host.match(browserAPIRequestDomains) === null && + !portfolioRequestsMatcher(request) + ) { privacyReport.add(request.headers.host); } }); diff --git a/test/e2e/mv3-perf-stats/init-load-stats.js b/test/e2e/mv3-perf-stats/init-load-stats.js index f9975557d3af..40584343d990 100755 --- a/test/e2e/mv3-perf-stats/init-load-stats.js +++ b/test/e2e/mv3-perf-stats/init-load-stats.js @@ -22,7 +22,10 @@ async function profilePageLoad() { const parsedLogs = {}; try { await withFixtures( - { fixtures: new FixtureBuilder().build() }, + { + fixtures: new FixtureBuilder().build(), + disableServerMochaToBackground: true, + }, async ({ driver }) => { await driver.delay(tinyDelayMs); await driver.navigate(); diff --git a/test/e2e/page-objects/flows/login.flow.ts b/test/e2e/page-objects/flows/login.flow.ts new file mode 100644 index 000000000000..039de3e12d98 --- /dev/null +++ b/test/e2e/page-objects/flows/login.flow.ts @@ -0,0 +1,30 @@ +import LoginPage from '../pages/login-page'; +import HomePage from '../pages/homepage'; +import { Driver } from '../../webdriver/driver'; +import { DEFAULT_GANACHE_ETH_BALANCE_DEC } from '../../constants'; +import { WALLET_PASSWORD } from '../../helpers'; + +/** + * This method unlocks the wallet and verifies that the user lands on the homepage with the expected balance. It is designed to be the initial step in setting up a test environment. + * + * @param driver - The webdriver instance. + * @param password - The password used to unlock the wallet. Defaults to WALLET_PASSWORD. + * @param expectedBalance - The expected balance to be displayed on the homepage after successful login. Defaults to DEFAULT_GANACHE_ETH_BALANCE_DEC, reflecting common usage in test setups. + */ +export const loginWithBalanceValidation = async ( + driver: Driver, + password: string = WALLET_PASSWORD, + expectedBalance: string = DEFAULT_GANACHE_ETH_BALANCE_DEC, +) => { + console.log('Navigate to unlock page and try to login with pasword'); + await driver.navigate(); + const loginPage = new LoginPage(driver); + await loginPage.check_pageIsLoaded(); + await loginPage.fillPassword(password); + await loginPage.clickUnlockButton(); + + // user should land on homepage after successfully logging in with password + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(expectedBalance); +}; diff --git a/test/e2e/page-objects/flows/send-transaction.flow.ts b/test/e2e/page-objects/flows/send-transaction.flow.ts new file mode 100644 index 000000000000..8291dc96a4e6 --- /dev/null +++ b/test/e2e/page-objects/flows/send-transaction.flow.ts @@ -0,0 +1,43 @@ +import HomePage from '../pages/homepage'; +import ConfirmTxPage from '../pages/send/confirm-tx-page'; +import SendTokenPage from '../pages/send/send-token-page'; +import { Driver } from '../../webdriver/driver'; + +/** + * This function initiates the steps required to send a transaction from the homepage to final confirmation. + * + * @param driver - The webdriver instance. + * @param recipientAddress - The recipient address. + * @param amount - The amount of the asset to be sent in the transaction. + * @param gasfee - The expected transaction gas fee. + * @param totalfee - The expected total transaction fee. + */ +export const sendTransaction = async ( + driver: Driver, + recipientAddress: string, + amount: string, + gasfee: string, + totalfee: string, +): Promise => { + console.log( + `Start flow to send amount ${amount} to recipient ${recipientAddress} on home screen`, + ); + // click send button on homepage to start flow + const homePage = new HomePage(driver); + await homePage.startSendFlow(); + + // user should land on send token screen to fill recipient and amount + const sendToPage = new SendTokenPage(driver); + await sendToPage.check_pageIsLoaded(); + await sendToPage.fillRecipient(recipientAddress); + await sendToPage.fillAmount(amount); + await sendToPage.goToNextScreen(); + + // confirm transaction when user lands on confirm transaction screen + const confirmTxPage = new ConfirmTxPage(driver); + await confirmTxPage.check_pageIsLoaded(gasfee, totalfee); + await confirmTxPage.confirmTx(); + + // user should land on homepage after transaction is confirmed + await homePage.check_pageIsLoaded(); +}; diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts new file mode 100644 index 000000000000..add85e88b7b7 --- /dev/null +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -0,0 +1,18 @@ +import { Driver } from '../../webdriver/driver'; + +class HeaderNavbar { + private driver: Driver; + + private accountMenuButton: string; + + constructor(driver: Driver) { + this.driver = driver; + this.accountMenuButton = '[data-testid="account-menu-icon"]'; + } + + async openAccountMenu(): Promise { + await this.driver.clickElement(this.accountMenuButton); + } +} + +export default HeaderNavbar; diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts new file mode 100644 index 000000000000..5e596021a449 --- /dev/null +++ b/test/e2e/page-objects/pages/homepage.ts @@ -0,0 +1,165 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../webdriver/driver'; +import { DEFAULT_GANACHE_ETH_BALANCE_DEC } from '../../constants'; +import HeaderNavbar from './header-navbar'; + +class HomePage { + private driver: Driver; + + private sendButton: string; + + private activityTab: string; + + private tokensTab: string; + + private balance: string; + + private completedTransactions: string; + + private confirmedTransactions: object; + + private transactionAmountsInActivity: string; + + public headerNavbar: HeaderNavbar; + + constructor(driver: Driver) { + this.driver = driver; + this.headerNavbar = new HeaderNavbar(driver); + this.sendButton = '[data-testid="eth-overview-send"]'; + this.activityTab = '[data-testid="account-overview__activity-tab"]'; + this.tokensTab = '[data-testid="account-overview__asset-tab"]'; + this.confirmedTransactions = { + text: 'Confirmed', + css: '.transaction-status-label--confirmed', + }; + this.balance = '[data-testid="eth-overview__primary-currency"]'; + this.completedTransactions = '[data-testid="activity-list-item"]'; + this.transactionAmountsInActivity = + '[data-testid="transaction-list-item-primary-currency"]'; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.sendButton, + this.activityTab, + this.tokensTab, + ]); + } catch (e) { + console.log('Timeout while waiting for home page to be loaded', e); + throw e; + } + console.log('Home page is loaded'); + } + + async check_expectedBalanceIsDisplayed( + expectedBalance: string = DEFAULT_GANACHE_ETH_BALANCE_DEC, + ): Promise { + try { + await this.driver.waitForSelector({ + css: this.balance, + text: `${expectedBalance} ETH`, + }); + } catch (e) { + const balance = await this.driver.waitForSelector(this.balance); + const currentBalance = parseFloat(await balance.getText()); + const errorMessage = `Expected balance ${expectedBalance} ETH, got balance ${currentBalance} ETH`; + console.log(errorMessage, e); + throw e; + } + console.log( + `Expected balance ${expectedBalance} ETH is displayed on homepage`, + ); + } + + async startSendFlow(): Promise { + await this.driver.clickElement(this.sendButton); + } + + async goToActivityList(): Promise { + console.log(`Open activity tab on homepage`); + await this.driver.clickElement(this.activityTab); + } + + /** + * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. + * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. + * + * @param expectedNumber - The number of confirmed transactions expected to be displayed in activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of confirmed transactions is displayed within the timeout period. + */ + async check_confirmedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} confirmed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const confirmedTxs = await this.driver.findElements( + this.confirmedTransactions, + ); + return confirmedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} confirmed transactions found in activity list on homepage`, + ); + } + + /** + * This function checks the specified number of completed transactions are displayed in the activity list on the homepage. + * It waits up to 10 seconds for the expected number of completed transactions to be visible. + * + * @param expectedNumber - The number of completed transactions expected to be displayed in the activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of completed transactions is displayed within the timeout period. + */ + async check_completedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} completed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const completedTxs = await this.driver.findElements( + this.completedTransactions, + ); + return completedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} completed transactions found in activity list on homepage`, + ); + } + + /** + * This function checks if a specified transaction amount at the specified index matches the expected one. + * + * @param expectedAmount - The expected transaction amount to be displayed. Defaults to '-1 ETH'. + * @param expectedNumber - The 1-based index of the transaction in the activity list whose amount is to be checked. + * Defaults to 1, indicating the first transaction in the list. + * @returns A promise that resolves if the transaction amount at the specified index matches the expected amount. + * The promise is rejected if the amounts do not match or if an error occurs during the process. + * @example + * // To check if the third transaction in the activity list displays an amount of '2 ETH' + * await check_txAmountInActivity('2 ETH', 3); + */ + async check_txAmountInActivity( + expectedAmount: string = '-1 ETH', + expectedNumber: number = 1, + ): Promise { + const transactionAmounts = await this.driver.findElements( + this.transactionAmountsInActivity, + ); + const transactionAmountsText = await transactionAmounts[ + expectedNumber - 1 + ].getText(); + assert.equal( + transactionAmountsText, + expectedAmount, + `${transactionAmountsText} is displayed as transaction amount instead of ${expectedAmount} for transaction ${expectedNumber}`, + ); + console.log( + `Amount for transaction ${expectedNumber} is displayed as ${expectedAmount}`, + ); + } +} + +export default HomePage; diff --git a/test/e2e/page-objects/pages/login-page.ts b/test/e2e/page-objects/pages/login-page.ts new file mode 100644 index 000000000000..b96600675040 --- /dev/null +++ b/test/e2e/page-objects/pages/login-page.ts @@ -0,0 +1,45 @@ +import { Driver } from '../../webdriver/driver'; + +class LoginPage { + private driver: Driver; + + private passwordInput: string; + + private unlockButton: string; + + private welcomeBackMessage: object; + + constructor(driver: Driver) { + this.driver = driver; + this.passwordInput = '[data-testid="unlock-password"]'; + this.unlockButton = '[data-testid="unlock-submit"]'; + this.welcomeBackMessage = { + css: '[data-testid="unlock-page-title"]', + text: 'Welcome back!', + }; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.welcomeBackMessage, + this.passwordInput, + this.unlockButton, + ]); + } catch (e) { + console.log('Timeout while waiting for login page to be loaded', e); + throw e; + } + console.log('Login page is loaded'); + } + + async fillPassword(password: string): Promise { + await this.driver.fill(this.passwordInput, password); + } + + async clickUnlockButton(): Promise { + await this.driver.clickElement(this.unlockButton); + } +} + +export default LoginPage; diff --git a/test/e2e/page-objects/pages/send/confirm-tx-page.ts b/test/e2e/page-objects/pages/send/confirm-tx-page.ts new file mode 100644 index 000000000000..0b51f9c6c4aa --- /dev/null +++ b/test/e2e/page-objects/pages/send/confirm-tx-page.ts @@ -0,0 +1,58 @@ +import { Driver } from '../../../webdriver/driver'; + +class ConfirmTxPage { + private driver: Driver; + + private confirmButton: string; + + private totalFee: string; + + private transactionFee: string; + + constructor(driver: Driver) { + this.driver = driver; + this.confirmButton = '[data-testid="page-container-footer-next"]'; + this.transactionFee = '[data-testid="confirm-gas-display"]'; + this.totalFee = '[data-testid="confirm-page-total-amount"]'; + } + + /** + * Verifies that the confirm transaction page is fully loaded by checking for the presence of confirm button and the expected gas values. + * + * @param expectedGasFee - The expected gas fee value to be displayed on the page. + * @param expectedTotalFee - The expected total fee value to be displayed on the page. + * @returns A promise that resolves when all specified elements are verified to be present and contain the expected values, indicating the page has fully loaded. + */ + async check_pageIsLoaded( + expectedGasFee: string, + expectedTotalFee: string, + ): Promise { + try { + await Promise.all([ + this.driver.waitForSelector(this.confirmButton), + this.driver.waitForSelector({ + css: this.transactionFee, + text: `${expectedGasFee} ETH`, + }), + this.driver.waitForSelector({ + css: this.totalFee, + text: `${expectedTotalFee} ETH`, + }), + ]); + } catch (e) { + console.log( + `Timeout while waiting for confirm transaction screen to be loaded, expected gas fee is: ${expectedGasFee} and expected total fee is: ${expectedTotalFee}`, + e, + ); + throw e; + } + console.log('Confirm transaction page is loaded with expected gas value'); + } + + async confirmTx(): Promise { + console.log('Click confirm button to confirm transaction'); + await this.driver.clickElement(this.confirmButton); + } +} + +export default ConfirmTxPage; diff --git a/test/e2e/page-objects/pages/send/send-token-page.ts b/test/e2e/page-objects/pages/send/send-token-page.ts new file mode 100644 index 000000000000..b5b703814a5f --- /dev/null +++ b/test/e2e/page-objects/pages/send/send-token-page.ts @@ -0,0 +1,131 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../../webdriver/driver'; + +class SendTokenPage { + private driver: Driver; + + private inputRecipient: string; + + private inputAmount: string; + + private scanButton: string; + + private continueButton: object; + + private ensResolvedName: string; + + private ensAddressAsRecipient: string; + + private ensResolvedAddress: string; + + constructor(driver: Driver) { + this.driver = driver; + this.inputAmount = '[data-testid="currency-input"]'; + this.inputRecipient = '[data-testid="ens-input"]'; + this.scanButton = '[data-testid="ens-qr-scan-button"]'; + this.ensResolvedName = + '[data-testid="multichain-send-page__recipient__item__title"]'; + this.ensResolvedAddress = + '[data-testid="multichain-send-page__recipient__item__subtitle"]'; + this.ensAddressAsRecipient = '[data-testid="ens-input-selected"]'; + this.continueButton = { + text: 'Continue', + tag: 'button', + }; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.scanButton, + this.inputRecipient, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for send token screen to be loaded', + e, + ); + throw e; + } + console.log('Send token screen is loaded'); + } + + async fillRecipient(recipientAddress: string): Promise { + console.log( + `Fill recipient input with ${recipientAddress} on send token screen`, + ); + await this.driver.pasteIntoField(this.inputRecipient, recipientAddress); + } + + async fillAmount(amount: string): Promise { + console.log(`Fill amount input with ${amount} on send token screen`); + const inputAmount = await this.driver.waitForSelector(this.inputAmount); + await this.driver.pasteIntoField(this.inputAmount, amount); + // The return value is not ts-compatible, requiring a temporary any cast to access the element's value. This will be corrected with the driver function's ts migration. + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const inputValue = await (inputAmount as any).getProperty('value'); + assert.equal( + inputValue, + amount, + `Error when filling amount field on send token screen: the value entered is ${inputValue} instead of expected ${amount}.`, + ); + } + + async goToNextScreen(): Promise { + await this.driver.clickElement(this.continueButton); + } + + /** + * Verifies that an ENS domain correctly resolves to the specified Ethereum address on the send token screen. + * + * @param ensDomain - The ENS domain name expected to resolve (e.g., "test.eth"). + * @param address - The Ethereum address to which the ENS domain is expected to resolve. + * @returns A promise that resolves if the ENS domain successfully resolves to the specified address on send token screen. + */ + async check_ensAddressResolution( + ensDomain: string, + address: string, + ): Promise { + console.log( + `Check ENS domain resolution: '${ensDomain}' should resolve to address '${address}' on the send token screen.`, + ); + // check if ens domain is resolved as expected address + await this.driver.waitForSelector({ + text: ensDomain, + css: this.ensResolvedName, + }); + await this.driver.waitForSelector({ + text: address, + css: this.ensResolvedAddress, + }); + } + + /** + * Verifies that an address resolved via ENS can be selected as the recipient on the send token screen. + * + * @param ensDomain - The ENS domain name expected to resolve to the given address. + * @param address - The Ethereum address to which the ENS domain is expected to resolve. + * @returns A promise that resolves if the ENS domain can be successfully used as a recipient address on the send token screen. + */ + async check_ensAddressAsRecipient( + ensDomain: string, + address: string, + ): Promise { + // click to select the resolved adress + await this.driver.clickElement({ + text: ensDomain, + css: this.ensResolvedName, + }); + // user should be able to send token to the resolved address + await this.driver.waitForSelector({ + css: this.ensAddressAsRecipient, + text: ensDomain + address, + }); + console.log( + `ENS domain '${ensDomain}' resolved to address '${address}' and can be used as recipient on send token screen.`, + ); + } +} + +export default SendTokenPage; diff --git a/test/e2e/playwright/global/specs/protect-intrinsics.spec.ts b/test/e2e/playwright/global/specs/protect-intrinsics.spec.ts new file mode 100644 index 000000000000..7f67cd587f2b --- /dev/null +++ b/test/e2e/playwright/global/specs/protect-intrinsics.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; +// @ts-expect-error lint fails otherwise +import 'ses'; +import '../../../../../app/scripts/lockdown-run'; +import '../../../../../app/scripts/lockdown-more'; +import { + getGlobalProperties, + testIntrinsic, +} from '../../../../helpers/protect-intrinsics-helpers'; + +test.describe('non-modifiable intrinsics', () => { + getGlobalProperties().forEach((propertyName) => { + test(`intrinsic globalThis["${propertyName}"]`, () => { + expect(() => testIntrinsic(propertyName)).not.toThrow(); + }); + }); +}); diff --git a/test/e2e/mmi/.env.example b/test/e2e/playwright/mmi/.env.example similarity index 100% rename from test/e2e/mmi/.env.example rename to test/e2e/playwright/mmi/.env.example diff --git a/test/e2e/mmi/Dockerfile b/test/e2e/playwright/mmi/Dockerfile similarity index 86% rename from test/e2e/mmi/Dockerfile rename to test/e2e/playwright/mmi/Dockerfile index 27bcf32d61fd..06c9fe52e8d5 100644 --- a/test/e2e/mmi/Dockerfile +++ b/test/e2e/playwright/mmi/Dockerfile @@ -5,7 +5,7 @@ WORKDIR '/usr/src/app' # Copy test files COPY playwright.config.ts . COPY env.ts test/helpers/env.ts -COPY . ./test/e2e/mmi/ +COPY . ./test/e2e/playwright/mmi/ # Copy extension for test COPY ./dist/chrome ./dist/chrome @@ -13,4 +13,4 @@ COPY ./dist/chrome ./dist/chrome RUN yarn add dotenv@^16.0.1 @playwright/test@^1.39.0 axios@^1.1.3 ENV HEADLESS=false -ENTRYPOINT ["npx", "xvfb-run", "playwright", "test", "--workers=1", "--project=mmi.visual"] \ No newline at end of file +ENTRYPOINT ["npx", "xvfb-run", "playwright", "test", "--workers=1", "--project=mmi.visual"] diff --git a/test/e2e/mmi/README.md b/test/e2e/playwright/mmi/README.md similarity index 98% rename from test/e2e/mmi/README.md rename to test/e2e/playwright/mmi/README.md index 7c4b37ff6ec0..951550731b9e 100644 --- a/test/e2e/mmi/README.md +++ b/test/e2e/playwright/mmi/README.md @@ -73,13 +73,14 @@ For more information on visual tests, refer to the [Playwright documentation](ht You need to have docker installed. You won't nned to run any command to build images. packege.json scripts will do the magic. -Running visual tests requires the generation of a Docker image. This ensures that the screenshots are consistent with the operating system used in the pipeline. All required to build this image is described in file `test/e2e/mmi/Dockerfile`. The actual process to build, call the tests, and tear down files is defined in `test/e2e/mmi/scripts/run-visual-test.sh`. +Running visual tests requires the generation of a Docker image. This ensures that the screenshots are consistent with the operating system used in the pipeline. All required to build this image is described in file `test/e2e/mmi/Dockerfile`. The actual process to build, call the tests, and tear down files is defined in `test/e2e/playwright/mmi/scripts/run-visual-test.sh`. **Only docker generated screenshots will be commited. Those generated out of docker will be git-ignored.** ## Reports Test reports are generated in the public folder. To obtain comprehensive, readable reports with direct access to `traces.zip`, run the following script: + ``` yarn test:e2e:pw:report ``` diff --git a/test/e2e/mmi/custodian-hooks/ICustodianTestClient.ts b/test/e2e/playwright/mmi/custodian-hooks/ICustodianTestClient.ts similarity index 100% rename from test/e2e/mmi/custodian-hooks/ICustodianTestClient.ts rename to test/e2e/playwright/mmi/custodian-hooks/ICustodianTestClient.ts diff --git a/test/e2e/mmi/custodian-hooks/hooks.ts b/test/e2e/playwright/mmi/custodian-hooks/hooks.ts similarity index 100% rename from test/e2e/mmi/custodian-hooks/hooks.ts rename to test/e2e/playwright/mmi/custodian-hooks/hooks.ts diff --git a/test/e2e/mmi/env.ts b/test/e2e/playwright/mmi/env.ts similarity index 100% rename from test/e2e/mmi/env.ts rename to test/e2e/playwright/mmi/env.ts diff --git a/test/e2e/mmi/helpers/custodian-helper.ts b/test/e2e/playwright/mmi/helpers/custodian-helper.ts similarity index 100% rename from test/e2e/mmi/helpers/custodian-helper.ts rename to test/e2e/playwright/mmi/helpers/custodian-helper.ts diff --git a/test/e2e/mmi/helpers/dapps-helpers.ts b/test/e2e/playwright/mmi/helpers/dapps-helpers.ts similarity index 100% rename from test/e2e/mmi/helpers/dapps-helpers.ts rename to test/e2e/playwright/mmi/helpers/dapps-helpers.ts diff --git a/test/e2e/mmi/helpers/extension-loader.ts b/test/e2e/playwright/mmi/helpers/extension-loader.ts similarity index 81% rename from test/e2e/mmi/helpers/extension-loader.ts rename to test/e2e/playwright/mmi/helpers/extension-loader.ts index 31834d7fce50..c7506fdc36e6 100644 --- a/test/e2e/mmi/helpers/extension-loader.ts +++ b/test/e2e/playwright/mmi/helpers/extension-loader.ts @@ -1,9 +1,9 @@ import path from 'path'; import { test as base, chromium } from '@playwright/test'; -import { isHeadless } from '../../../helpers/env'; +import { isHeadless } from '../../../../helpers/env'; -const extensionPath = path.join(__dirname, '../../../../dist/chrome'); +const extensionPath = path.join(__dirname, '../../../../../dist/chrome'); export const test = base.extend({ // eslint-disable-next-line no-empty-pattern diff --git a/test/e2e/mmi/helpers/oauth-api-helper.ts b/test/e2e/playwright/mmi/helpers/oauth-api-helper.ts similarity index 100% rename from test/e2e/mmi/helpers/oauth-api-helper.ts rename to test/e2e/playwright/mmi/helpers/oauth-api-helper.ts diff --git a/test/e2e/mmi/helpers/saturn-api-helper.ts b/test/e2e/playwright/mmi/helpers/saturn-api-helper.ts similarity index 100% rename from test/e2e/mmi/helpers/saturn-api-helper.ts rename to test/e2e/playwright/mmi/helpers/saturn-api-helper.ts diff --git a/test/e2e/mmi/helpers/utils.ts b/test/e2e/playwright/mmi/helpers/utils.ts similarity index 100% rename from test/e2e/mmi/helpers/utils.ts rename to test/e2e/playwright/mmi/helpers/utils.ts diff --git a/test/e2e/mmi/pageObjects/mmi-accountMenu-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-accountMenu-page.ts similarity index 90% rename from test/e2e/mmi/pageObjects/mmi-accountMenu-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-accountMenu-page.ts index 7680769bbe6c..7aa1431019d0 100644 --- a/test/e2e/mmi/pageObjects/mmi-accountMenu-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-accountMenu-page.ts @@ -47,6 +47,10 @@ export class MMIAccountMenuPage { .filter({ hasText: 'Select an account' }); } + delay(time: number) { + return new Promise((resolve) => setTimeout(resolve, time)); + } + async connectCustodian(name: string, visual?: boolean, qrCode?: boolean) { await this.page .getByRole('button', { name: /Add account or hardware wallet/iu }) @@ -56,9 +60,10 @@ export class MMIAccountMenuPage { if (visual) { // wait until all custodian icons are loaded await this.page.waitForLoadState(); - await test.expect - .soft(this.page) - .toHaveScreenshot('custodian_list.png', { fullPage: true }); + await test.expect.soft(this.page).toHaveScreenshot('custodian_list.png', { + fullPage: true, + maxDiffPixelRatio: 0.06, + }); } const custodian = await getCustodianInfoByName(name); @@ -72,13 +77,24 @@ export class MMIAccountMenuPage { .click(); if (qrCode) { + await this.delay(3000); + const spanElement = await this.page.$('span.hidden'); if (spanElement) { + await this.delay(3000); + + const startTime = Date.now(); + const timeout = 10000; + let data = await spanElement.getAttribute('data-value'); while (!data) { - await new Promise((resolve) => setTimeout(resolve, 1000)); + if (Date.now() - startTime > timeout) { + break; + } + + await this.delay(3000); data = await spanElement.getAttribute('data-value'); } @@ -119,8 +135,11 @@ export class MMIAccountMenuPage { } async selectCustodyAccount(account: string) { - await this.accountsMenu(); - await this.dialog.getByText(`${account}`).click(); + if (account) { + await this.accountsMenu(); + + await this.dialog.getByText(`${account}`).click(); + } } async accountMenuScreenshot(screenshotName: string) { @@ -144,12 +163,6 @@ export class MMIAccountMenuPage { .getByRole('button', { name: `${accountToRemoveName} Options` }) .click(); await this.page.getByText('Remove custodian token').click(); - // Scrollbar issues with different environments - // const dialog = this.page - // .getByRole('dialog') - // .filter({ hasText: 'Remove custodian token' }); - - // await test.expect.soft(dialog).toHaveScreenshot(); await this.page.getByRole('button', { name: /close/iu }).first().click(); } diff --git a/test/e2e/mmi/pageObjects/mmi-auth0-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-auth0-page.ts similarity index 94% rename from test/e2e/mmi/pageObjects/mmi-auth0-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-auth0-page.ts index 761fb8684de7..c8c304412936 100644 --- a/test/e2e/mmi/pageObjects/mmi-auth0-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-auth0-page.ts @@ -45,6 +45,7 @@ export class Auth0Page { .locator('#password') .fill(process.env.MMI_E2E_E2E_AUTH0_PASSWORD as string); await this.page.getByRole('button', { name: /continue/iu }).click(); + await this.page.getByRole('button', { name: /E2E Organization/iu }).click(); await expect(this.page).toHaveURL(portfolio); } } diff --git a/test/e2e/mmi/pageObjects/mmi-dummyApp-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts similarity index 94% rename from test/e2e/mmi/pageObjects/mmi-dummyApp-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts index 3aff49658f53..a9a175e82971 100644 --- a/test/e2e/mmi/pageObjects/mmi-dummyApp-page.ts +++ b/test/e2e/playwright/mmi/pageObjects/mmi-dummyApp-page.ts @@ -60,14 +60,17 @@ export class DummyAppPage { if (isSign) { await popup.click('button:has-text("Confirm")'); } else { - // Confirm await popup.getByTestId('page-container-footer-next').click(); - // Approve + + if (buttonId === 'approveTokens') { + await popup.getByTestId('page-container-footer-next').click(); + } + await popup .getByTestId('custody-confirm-link__btn') .click({ timeout: 10000 }); - // } } + await popup.close(); } diff --git a/test/e2e/mmi/pageObjects/mmi-extension-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-extension-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-extension-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-extension-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-main-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-main-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-main-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-main-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-mainMenu-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-mainMenu-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-mainMenu-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-mainMenu-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-network-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-network-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-network-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-network-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-saturn-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-saturn-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-saturn-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-saturn-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-saturn-ui-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-saturn-ui-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-saturn-ui-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-saturn-ui-page.ts diff --git a/test/e2e/mmi/pageObjects/mmi-signup-page.ts b/test/e2e/playwright/mmi/pageObjects/mmi-signup-page.ts similarity index 100% rename from test/e2e/mmi/pageObjects/mmi-signup-page.ts rename to test/e2e/playwright/mmi/pageObjects/mmi-signup-page.ts diff --git a/test/e2e/mmi/resources/circleci-artifact-screnshot.png b/test/e2e/playwright/mmi/resources/circleci-artifact-screnshot.png similarity index 100% rename from test/e2e/mmi/resources/circleci-artifact-screnshot.png rename to test/e2e/playwright/mmi/resources/circleci-artifact-screnshot.png diff --git a/test/e2e/mmi/resources/trace.png b/test/e2e/playwright/mmi/resources/trace.png similarity index 100% rename from test/e2e/mmi/resources/trace.png rename to test/e2e/playwright/mmi/resources/trace.png diff --git a/test/e2e/mmi/scripts/run-visual-test.sh b/test/e2e/playwright/mmi/scripts/run-visual-test.sh similarity index 74% rename from test/e2e/mmi/scripts/run-visual-test.sh rename to test/e2e/playwright/mmi/scripts/run-visual-test.sh index d3249d7bc792..a5621b7fd9af 100755 --- a/test/e2e/mmi/scripts/run-visual-test.sh +++ b/test/e2e/playwright/mmi/scripts/run-visual-test.sh @@ -6,18 +6,18 @@ set -e # Define variables for better readability IMAGE_NAME=e2e/mmi-dashboard:latest CONTAINER_VOLUME_1=$(pwd)/public/playwright/playwright-reports:/usr/src/app/public/playwright/playwright-reports -CONTAINER_VOLUME_2=$(pwd)/test/e2e/mmi/specs:/usr/src/app/test/e2e/mmi/specs +CONTAINER_VOLUME_2=$(pwd)/test/e2e/playwright/mmi/specs:/usr/src/app/test/e2e/playwright/mmi/specs # copy mmi build to the docker context -mkdir -p test/e2e/mmi/dist -cp -r dist/chrome test/e2e/mmi/dist/chrome +mkdir -p test/e2e/playwright/mmi/dist +cp -r dist/chrome test/e2e/playwright/mmi/dist/chrome # copy playwright config to the docker context -cp playwright.config.ts test/helpers/env.ts test/e2e/mmi/ +cp playwright.config.ts test/helpers/env.ts test/e2e/playwright/mmi/ # Build the Docker image echo "Building the Docker image..." -docker build -t $IMAGE_NAME test/e2e/mmi/ +docker build -t $IMAGE_NAME test/e2e/playwright/mmi/ # Check the script parameter UPDATE_SNAPSHOTS="" @@ -39,8 +39,8 @@ docker image rm $IMAGE_NAME # Remove files copied the building the image echo "Removing playwright.config.ts..." -rm test/e2e/mmi/playwright.config.ts +rm test/e2e/playwright/mmi/playwright.config.ts echo "Removing mmi dist/chrome from test dir..." -rm -rf test/e2e/mmi/dist +rm -rf test/e2e/playwright/mmi/dist echo "Script completed successfully." diff --git a/test/e2e/mmi/specs/dapp.signature.spec.ts b/test/e2e/playwright/mmi/specs/dapp.signature.spec.ts similarity index 100% rename from test/e2e/mmi/specs/dapp.signature.spec.ts rename to test/e2e/playwright/mmi/specs/dapp.signature.spec.ts diff --git a/test/e2e/mmi/specs/dapp.spec.ts b/test/e2e/playwright/mmi/specs/dapp.spec.ts similarity index 100% rename from test/e2e/mmi/specs/dapp.spec.ts rename to test/e2e/playwright/mmi/specs/dapp.spec.ts diff --git a/test/e2e/mmi/specs/extension.visual.spec.ts b/test/e2e/playwright/mmi/specs/extension.visual.spec.ts similarity index 97% rename from test/e2e/mmi/specs/extension.visual.spec.ts rename to test/e2e/playwright/mmi/specs/extension.visual.spec.ts index 240f3dce75b2..c9f6717b26ec 100644 --- a/test/e2e/mmi/specs/extension.visual.spec.ts +++ b/test/e2e/playwright/mmi/specs/extension.visual.spec.ts @@ -10,7 +10,8 @@ import { MMIAccountMenuPage } from '../pageObjects/mmi-accountMenu-page'; import { SEPOLIA_DISPLAY_NAME } from '../helpers/utils'; test.describe('MMI extension', () => { - test('Interactive token replacement', async ({ page, context }) => { + // @TODO come back later, it passes locally, fails in CI + test.skip('Interactive token replacement', async ({ page, context }) => { test.slow(); // Getting extension id of MMI const extensions = new ChromeExtensionPage(await context.newPage()); @@ -104,6 +105,8 @@ test.describe('MMI extension', () => { 'Custody Account R', 'Custody Account S', 'Custody Account T', + 'TR', + 'TR2', ]; // Getting extension id of MMI diff --git a/test/e2e/playwright/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png new file mode 100644 index 000000000000..c1c7aafffa6e Binary files /dev/null and b/test/e2e/playwright/mmi/specs/extension.visual.spec.ts-snapshots/popop-token-remove-approve-mmi-visual-linux.png differ diff --git a/test/e2e/mmi/specs/extension.visual.spec.ts-snapshots/token-replacement-notification-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/extension.visual.spec.ts-snapshots/token-replacement-notification-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/extension.visual.spec.ts-snapshots/token-replacement-notification-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/extension.visual.spec.ts-snapshots/token-replacement-notification-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/navigation.spec.ts b/test/e2e/playwright/mmi/specs/navigation.spec.ts similarity index 84% rename from test/e2e/mmi/specs/navigation.spec.ts rename to test/e2e/playwright/mmi/specs/navigation.spec.ts index 30620d483445..7aca00aba1c4 100644 --- a/test/e2e/mmi/specs/navigation.spec.ts +++ b/test/e2e/playwright/mmi/specs/navigation.spec.ts @@ -7,11 +7,10 @@ import { getPageAndCloseRepeated, } from '../helpers/utils'; import { MMIMainMenuPage } from '../pageObjects/mmi-mainMenu-page'; -import { Auth0Page } from '../pageObjects/mmi-auth0-page'; import { MMIMainPage } from '../pageObjects/mmi-main-page'; -const portfolio = `${process.env.MMI_E2E_MMI_DASHBOARD_URL}/portfolio`; -const stake = `${process.env.MMI_E2E_MMI_DASHBOARD_URL}/stake`; +const portfolio = `${process.env.MMI_E2E_MMI_DASHBOARD_URL}`; +const stake = `${process.env.MMI_E2E_MMI_DASHBOARD_URL}`; const support = 'https://mmi-support.metamask.io/hc/en-us'; const supportContactUs = 'https://mmi-support.metamask.io/hc/en-us/requests/new'; @@ -22,8 +21,7 @@ const termsOfUse = 'https://consensys.io/terms-of-use'; const learnMoreArticles = 'https://support.metamask.io/'; test.describe('MMI Navigation', () => { - test('MMI full navigation links', async ({ context }) => { - test.slow(); + test('MMI full navigation links', async ({ page, context }) => { // Getting extension id of MMI const extensions = new ChromeExtensionPage(await context.newPage()); @@ -41,20 +39,23 @@ test.describe('MMI Navigation', () => { await signUp.authentication(); await signUp.info(); - // This is removed to improve test performance - // Signin auth0 - const auth0 = new Auth0Page(await context.newPage()); - await auth0.signIn(); - await auth0.page.close(); + // Setup testnetwork in settings + const mainMenuPage = new MMIMainMenuPage(page, extensionId as string); + await mainMenuPage.goto(); + await mainMenuPage.fillPassword(); + await mainMenuPage.finishOnboarding(); + await mainMenuPage.selectMenuOption('settings'); + await mainMenuPage.selectSettings('Advance'); + await mainMenuPage.switchTestNetwork(); + // await mainMenuPage.showIncomingTransactionsOff() + await mainMenuPage.closeSettings(); - // Close pages not used to remove data from logs + // // Close pages not used to remove data from logs await closePages(context, ['metamask-institutional.io']); const mainPage = new MMIMainPage( await getPageAndCloseRepeated(context, 'home.html'), ); - await mainPage.finishOnboarding(); - // Check main page links await checkLinkURL( context, @@ -90,10 +91,6 @@ test.describe('MMI Navigation', () => { ); // Check main menu links - const mainMenuPage = new MMIMainMenuPage( - mainPage.page, - extensionId as string, - ); await mainMenuPage.openMenu(); await checkLinkURL( context, diff --git a/test/e2e/mmi/specs/qrCode.spec.ts b/test/e2e/playwright/mmi/specs/qrCode.spec.ts similarity index 96% rename from test/e2e/mmi/specs/qrCode.spec.ts rename to test/e2e/playwright/mmi/specs/qrCode.spec.ts index 23ad4c88e1b4..72a5ff7ea62b 100644 --- a/test/e2e/mmi/specs/qrCode.spec.ts +++ b/test/e2e/playwright/mmi/specs/qrCode.spec.ts @@ -10,7 +10,7 @@ import { SEPOLIA_DISPLAY_NAME } from '../helpers/utils'; test.describe('QR Code Connection Request', () => { // @TODO Follow up task to understand why this test fails more times than it passes - test.skip('run the extension and add custodian accounts using the QR Code feature', async ({ + test('run the extension and add custodian accounts using the QR Code feature', async ({ page, context, }) => { diff --git a/test/e2e/mmi/specs/transactions.spec.ts b/test/e2e/playwright/mmi/specs/transactions.spec.ts similarity index 100% rename from test/e2e/mmi/specs/transactions.spec.ts rename to test/e2e/playwright/mmi/specs/transactions.spec.ts diff --git a/test/e2e/mmi/specs/visual.spec.ts b/test/e2e/playwright/mmi/specs/visual.spec.ts similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts rename to test/e2e/playwright/mmi/specs/visual.spec.ts diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/MMI-visual-Full-visual-e2e-1-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/MMI-visual-Full-visual-e2e-1-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/MMI-visual-Full-visual-e2e-1-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/MMI-visual-Full-visual-e2e-1-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/connect-custodian-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/connect-custodian-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/connect-custodian-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/connect-custodian-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-connection-info-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-list-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-list-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-list-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-list-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-remove-token-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-remove-token-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/custodian-remove-token-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custodian-remove-token-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/custody-accounts-selection-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custody-accounts-selection-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/custody-accounts-selection-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/custody-accounts-selection-mmi-visual-linux.png diff --git a/test/e2e/mmi/specs/visual.spec.ts-snapshots/mainWindow-custodian-selected-mmi-visual-linux.png b/test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/mainWindow-custodian-selected-mmi-visual-linux.png similarity index 100% rename from test/e2e/mmi/specs/visual.spec.ts-snapshots/mainWindow-custodian-selected-mmi-visual-linux.png rename to test/e2e/playwright/mmi/specs/visual.spec.ts-snapshots/mainWindow-custodian-selected-mmi-visual-linux.png diff --git a/test/e2e/playwright/shared/pageObjects/activity-list-page.ts b/test/e2e/playwright/shared/pageObjects/activity-list-page.ts new file mode 100644 index 000000000000..3e55772865b5 --- /dev/null +++ b/test/e2e/playwright/shared/pageObjects/activity-list-page.ts @@ -0,0 +1,26 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export class ActivityListPage { + private page: Page; + + readonly activityItem: Locator; + + readonly status: Locator; + + constructor(page: Page) { + this.page = page; + this.activityItem = this.page + .getByTestId('activity-list-item-action') + .first(); + + this.status = this.page.locator('.transaction-status-label').first(); + } + + async checkActivityIsConfirmed(options: { activity: string }) { + const itemText = await this.activityItem.innerText(); + await expect(itemText).toEqual(options.activity); + + const itemStatus = await this.status.innerText(); + await expect(itemStatus).toEqual('Confirmed'); + } +} diff --git a/test/e2e/playwright/shared/pageObjects/extension-page.ts b/test/e2e/playwright/shared/pageObjects/extension-page.ts new file mode 100644 index 000000000000..df4363000b2a --- /dev/null +++ b/test/e2e/playwright/shared/pageObjects/extension-page.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { chromium } from '@playwright/test'; + +const extensionPath = path.join(__dirname, '../../../../../dist/chrome'); + +export class ChromeExtensionPage { + async initExtension() { + const launchOptions = { + headless: false, + args: [`--disable-extensions-except=${extensionPath}`], + }; + if (process.env.HEADLESS === 'true') { + launchOptions.args.push('--headless=new'); + } + const context = await chromium.launchPersistentContext('', launchOptions); + await context.newPage(); + await context.waitForEvent('page'); + const pages = context.pages(); + const page = pages[pages.length - 1]; // return last tab + await page.waitForSelector('text=/I agree to MetaMask/'); + return page; + } +} diff --git a/test/e2e/playwright/shared/pageObjects/network-controller-page.ts b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts new file mode 100644 index 000000000000..aed0e7377b16 --- /dev/null +++ b/test/e2e/playwright/shared/pageObjects/network-controller-page.ts @@ -0,0 +1,84 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class NetworkController { + readonly page: Page; + + readonly networkDisplay: Locator; + + readonly addNetworkButton: Locator; + + readonly addNetworkManuallyButton: Locator; + + readonly approveBtn: Locator; + + readonly saveBtn: Locator; + + readonly switchToNetworkBtn: Locator; + + readonly gotItBtn: Locator; + + readonly networkSearch: Locator; + + readonly networkName: Locator; + + readonly networkRpc: Locator; + + readonly networkChainId: Locator; + + readonly networkTicker: Locator; + + constructor(page: Page) { + this.page = page; + this.networkDisplay = this.page.getByTestId('network-display'); + this.addNetworkButton = this.page.getByText('Add network'); + this.addNetworkManuallyButton = this.page.getByTestId( + 'add-network-manually', + ); + this.saveBtn = this.page.getByRole('button', { name: 'Save' }); + this.approveBtn = this.page.getByTestId('confirmation-submit-button'); + this.switchToNetworkBtn = this.page.locator('button', { + hasText: 'Switch to', + }); + this.gotItBtn = this.page.getByRole('button', { name: 'Got it' }); + this.networkSearch = this.page.locator('input[type="search"]'); + this.networkName = this.page.getByTestId('network-form-network-name'); + this.networkRpc = this.page.getByTestId('network-form-rpc-url'); + this.networkChainId = this.page.getByTestId('network-form-chain-id'); + this.networkTicker = this.page.getByTestId('network-form-ticker-input'); + } + + async addCustomNetwork(options: { + name: string; + url: string; + chainID: string; + symbol: string; + }) { + await this.networkDisplay.click(); + await this.addNetworkButton.click(); + await this.addNetworkManuallyButton.click(); + + await this.networkName.waitFor(); + await this.networkName.fill(options.name); + await this.networkRpc.fill(options.url); + await this.networkChainId.fill(options.chainID); + await this.networkTicker.fill(options.symbol); + await this.saveBtn.click(); + await this.switchToNetworkBtn.click(); + } + + async addPopularNetwork(options: { networkName: string }) { + await this.networkDisplay.click(); + await this.addNetworkButton.click(); + const addBtn = this.page.getByTestId(`add-network-${options.networkName}`); + await addBtn.click(); + await this.approveBtn.click(); + await this.switchToNetworkBtn.click(); + await this.gotItBtn.click(); + } + + async selectNetwork(options: { networkName: string }) { + await this.networkDisplay.click(); + await this.networkSearch.fill(options.networkName); + await this.page.getByText(options.networkName).click(); + } +} diff --git a/test/e2e/playwright/shared/pageObjects/signup-page.ts b/test/e2e/playwright/shared/pageObjects/signup-page.ts new file mode 100644 index 000000000000..e4165fc1ab89 --- /dev/null +++ b/test/e2e/playwright/shared/pageObjects/signup-page.ts @@ -0,0 +1,84 @@ +import { type Locator, type Page } from '@playwright/test'; + +const SEED_PHRASE = + 'ancient cloth onion decline gloom air scare addict action exhaust neck auto'; +const ACCOUNT_PASSWORD = '123123123'; + +export class SignUpPage { + readonly page: Page; + + readonly getStartedBtn: Locator; + + readonly importWalletBtn: Locator; + + readonly confirmSecretBtn: Locator; + + readonly agreeBtn: Locator; + + readonly noThanksBtn: Locator; + + readonly passwordTxt: Locator; + + readonly passwordConfirmTxt: Locator; + + readonly agreeCheck: Locator; + + readonly agreeTandCCheck: Locator; + + readonly agreePasswordTermsCheck: Locator; + + readonly importBtn: Locator; + + readonly doneBtn: Locator; + + readonly gotItBtn: Locator; + + readonly nextBtn: Locator; + + readonly enableButton: Locator; + + constructor(page: Page) { + this.page = page; + this.getStartedBtn = page.locator('button:has-text("Get started")'); + this.importWalletBtn = page.locator( + 'button:has-text("Import an existing wallet")', + ); + this.confirmSecretBtn = page.locator( + 'button:has-text("Confirm Secret Recovery Phrase")', + ); + this.agreeBtn = page.locator('button:has-text("I agree")'); + this.noThanksBtn = page.locator('button:has-text("No thanks")'); + this.passwordTxt = page.getByTestId('create-password-new'); + this.passwordConfirmTxt = page.getByTestId('create-password-confirm'); + this.agreeCheck = page.getByTestId('create-new-vault__terms-checkbox'); + this.agreeTandCCheck = page.getByTestId('onboarding-terms-checkbox'); + this.agreePasswordTermsCheck = page.getByTestId('create-password-terms'); + this.importBtn = page.getByTestId('create-password-import'); + this.doneBtn = page.getByTestId('pin-extension-done'); + this.gotItBtn = page.getByTestId('onboarding-complete-done'); + this.nextBtn = page.getByTestId('pin-extension-next'); + this.agreeBtn = page.locator('button:has-text("I agree")'); + this.enableButton = page.locator('button:has-text("Enable")'); + } + + async importWallet() { + await this.agreeTandCCheck.click(); + await this.importWalletBtn.click(); + await this.agreeBtn.click(); + + const seeds = SEED_PHRASE?.trim().split(/\s+/u); + for (const [index, element] of (seeds as string[]).entries()) { + await this.page + .locator(`data-testid=import-srp__srp-word-${index}`) + .fill(element); + } + await this.confirmSecretBtn.click(); + await this.passwordTxt.fill(ACCOUNT_PASSWORD as string); + await this.passwordConfirmTxt.fill(ACCOUNT_PASSWORD as string); + await this.agreePasswordTermsCheck.click(); + await this.importBtn.click(); + await this.gotItBtn.click(); + await this.nextBtn.click(); + await this.doneBtn.click(); + } +} diff --git a/test/e2e/playwright/shared/pageObjects/wallet-page.ts b/test/e2e/playwright/shared/pageObjects/wallet-page.ts new file mode 100644 index 000000000000..47a9b667c96c --- /dev/null +++ b/test/e2e/playwright/shared/pageObjects/wallet-page.ts @@ -0,0 +1,45 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class WalletPage { + private page: Page; + + readonly importTokensButton: Locator; + + readonly importButton: Locator; + + readonly swapButton: Locator; + + readonly activityListTab: Locator; + + readonly tokenTab: Locator; + + constructor(page: Page) { + this.page = page; + this.swapButton = this.page.getByTestId('token-overview-button-swap'); + this.importTokensButton = this.page.getByText('Import tokens').first(); + this.importButton = this.page.getByText('Import ('); + this.tokenTab = this.page.getByTestId('account-overview__asset-tab'); + this.activityListTab = this.page.getByTestId( + 'account-overview__activity-tab', + ); + } + + async importTokens() { + await this.page.waitForSelector('text=/new token/'); + await this.importTokensButton.click(); + await this.importButton.waitFor({ state: 'visible' }); + await this.importButton.click(); + } + + async selectTokenWallet() { + await this.tokenTab.click(); + } + + async selectSwapAction() { + await this.swapButton.click(); + } + + async selectActivityList() { + await this.activityListTab.click(); + } +} diff --git a/test/e2e/playwright/swap/README.md b/test/e2e/playwright/swap/README.md new file mode 100644 index 000000000000..185a5f957be7 --- /dev/null +++ b/test/e2e/playwright/swap/README.md @@ -0,0 +1,77 @@ +# Playwright Swap E2E Testing + +This directory contains a set of specific Swap end-to-end tests created using [Playwright](https://playwright.dev/). + +## Setup + +1. Installing Playwright + +If you run the tests for first time, you may need to install the browser dependency (Playwright will inform you in case you need): + +``` +yarn playwright install chromium +``` + +2. Prepare the Build: + +Use the following command to install all dependencies: + +``` +yarn +``` + +3. Build: + +Use the following command to generate the Extension build: + +``` +yarn dist +``` + +## Running the Tests + +From the root of the project, you can use the following scripts to run the tests: + +``` +yarn test:e2e:swap +``` + +### Debug test + +If you're interested in [debugging tests](https://playwright.dev/docs/debug), we suggest installing the Visual Studio plugin. This will allow you to run each test individually, providing a more streamlined debugging process. + +If you don't want to run the tests in headless mode please specify `headless: false` in the following section of the config file `playwright.config.ts` in the root folder: + +``` + { + name: 'swap', + testMatch: '/playwright/swap/specs/*swap.spec.ts', + use: { + ...devices['Desktop Chrome'], + headless: true, + }, + }, +``` + +In the same `playwright.config.ts` file you can also specify `fullyParallel: false` if you don't want run the tests in parallel: + +## Reports + +Test reports are generated in the public folder. To obtain comprehensive, readable reports with direct access to `traces.zip`, run the following script: + +``` +yarn test:e2e:pw:report +``` + +Note that the attachment on every test, `trace` provide you with a wealth of useful information for debugging. + +![Playwright trace detail](resources/trace.png) + +### CircleCI logs + +When tests finish on the pipeline, you can find the same logs that you use locally. The logs are generated split by thread and can be found within the Artifact tab on the Job run page on CircleCI. You will see a link `public/playwright/playwright-reports/html/index.html` (it is the first artifact of each thread). Click there and you will be redirected to the Playwright report. +![CircleCI Job Actifact detail](resources/circleci-artifact-screnshot.png) + +## Contact Swap team + +If you encounter any problems while working on these e2e tests, you can write into the Consensys Slack channel `#metaswap-core`. diff --git a/test/e2e/playwright/swap/pageObjects/swap-page.ts b/test/e2e/playwright/swap/pageObjects/swap-page.ts new file mode 100644 index 000000000000..094d92b0d116 --- /dev/null +++ b/test/e2e/playwright/swap/pageObjects/swap-page.ts @@ -0,0 +1,128 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class SwapPage { + private page: Page; + + readonly toggleSmartSwap: Locator; + + readonly updateSettingsButton: Locator; + + readonly swapFromDropDown: Locator; + + readonly swapToDropDown: Locator; + + readonly tokenSearch: Locator; + + readonly tokenList: Locator; + + readonly tokenQty: Locator; + + readonly fetchQuoteButton: Locator; + + readonly swapTokenButton: Locator; + + readonly backButton: Locator; + + readonly switchTokensButton: Locator; + + readonly closeButton: Locator; + + constructor(page: Page) { + this.page = page; + this.toggleSmartSwap = this.page.locator('text="On"'); + this.updateSettingsButton = this.page.getByTestId( + 'update-transaction-settings-button', + ); + this.swapFromDropDown = this.page.getByTestId( + 'prepare-swap-page-swap-from', + ); + this.swapToDropDown = this.page.getByTestId('prepare-swap-page-swap-to'); + this.switchTokensButton = this.page.getByTestId( + 'prepare-swap-page-switch-tokens', + ); + this.tokenSearch = this.page.locator( + '[id="list-with-search__text-search"]', + ); + this.tokenList = this.page.getByTestId( + 'searchable-item-list-primary-label', + ); + this.tokenQty = this.page.getByTestId( + 'prepare-swap-page-from-token-amount', + ); + this.fetchQuoteButton = this.page.getByText('Fetch quote'); + this.swapTokenButton = this.page.locator('button', { hasText: 'Swap' }); + this.closeButton = this.page.getByText('Close'); + this.backButton = this.page.locator('[title="Cancel"]'); + } + + async fetchQuote(options: { from?: string; to: string; qty: string }) { + // Enter Swap Quantity + await this.tokenQty.fill(options.qty); + + // Enter source token + if (options.from) { + this.swapFromDropDown.click(); + await this.tokenSearch.fill(options.from); + await this.selectTokenFromList(options.from); + } + + // Enter destionation token + await this.swapToDropDown.click(); + await this.tokenSearch.fill(options.to); + await this.selectTokenFromList(options.to); + } + + async swap() { + await this.waitForCountDown(); + + // Clear Swap Anyway button if present + const swapAnywayButton = await this.page.$('text=/Swap anyway/'); + if (swapAnywayButton) { + await swapAnywayButton.click(); + } + await this.swapTokenButton.click(); + } + + async switchTokens() { + // Wait for swap button to appear + await this.swapTokenButton.waitFor(); + await this.switchTokensButton.click(); + await this.waitForCountDown(); + } + + async gotBack() { + await this.backButton.click(); + } + + async waitForCountDown(second: number = 23) { + await this.page.waitForSelector(`text=/New quotes in 0:${second}/`); + } + + async waitForTransactionToComplete() { + await this.page.waitForSelector('text=/Transaction complete/'); + await this.closeButton.click(); // Close button + } + + async waitForInsufficentBalance() { + await this.page.waitForSelector('text="Insufficient balance"'); + await this.waitForCountDown(); + } + + async selectTokenFromList(symbol: string) { + let searchItem; + do { + searchItem = await this.tokenList.first().textContent(); + } while (searchItem !== symbol); + + await this.tokenList.first().click(); + } + + async waitForSearchListToPopulate(symbol: string): Promise { + let searchItem; + do { + searchItem = await this.tokenList.first().textContent(); + } while (searchItem !== symbol); + + return await this.tokenList.first().click(); + } +} diff --git a/test/e2e/playwright/swap/specs/swap.spec.ts b/test/e2e/playwright/swap/specs/swap.spec.ts new file mode 100644 index 000000000000..a795f67b2753 --- /dev/null +++ b/test/e2e/playwright/swap/specs/swap.spec.ts @@ -0,0 +1,111 @@ +import { test } from '@playwright/test'; + +import { ChromeExtensionPage } from '../../shared/pageObjects/extension-page'; +import { SignUpPage } from '../../shared/pageObjects/signup-page'; +import { NetworkController } from '../../shared/pageObjects/network-controller-page'; +import { SwapPage } from '../pageObjects/swap-page'; +import { WalletPage } from '../../shared/pageObjects/wallet-page'; +import { ActivityListPage } from '../../shared/pageObjects/activity-list-page'; + +let swapPage: SwapPage; +let networkController: NetworkController; +let walletPage: WalletPage; +let activityListPage: ActivityListPage; + +const Tenderly = { + Mainnet: { + name: 'Tenderly', + url: 'https://rpc.tenderly.co/fork/cdbcd795-097d-4624-aa16-680374d89a43', + chainID: '1', + symbol: 'ETH', + }, +}; + +test.beforeEach( + 'Initialize extension, import wallet and add custom networks', + async () => { + const extension = new ChromeExtensionPage(); + const page = await extension.initExtension(); + + const signUp = new SignUpPage(page); + await signUp.importWallet(); + + networkController = new NetworkController(page); + swapPage = new SwapPage(page); + activityListPage = new ActivityListPage(page); + + await networkController.addCustomNetwork(Tenderly.Mainnet); + walletPage = new WalletPage(page); + await page.waitForTimeout(2000); + }, +); + +test('Swap ETH to DAI - Switch to Arbitrum and fetch quote - Switch ETH - WETH', async () => { + await walletPage.importTokens(); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ from: 'ETH', to: 'DAI', qty: '.001' }); + await swapPage.swap(); + await swapPage.waitForTransactionToComplete(); + await walletPage.selectActivityList(); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap ETH to DAI', + }); + + await networkController.addPopularNetwork({ networkName: 'Arbitrum One' }); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ to: 'MATIC', qty: '.001' }); + await swapPage.waitForInsufficentBalance(); + await swapPage.gotBack(); + + await networkController.selectNetwork({ networkName: 'Tenderly' }); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap ETH to DAI', + }); + await walletPage.selectTokenWallet(); + await walletPage.importTokens(); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ from: 'ETH', to: 'WETH', qty: '.001' }); + await swapPage.swap(); + await swapPage.waitForTransactionToComplete(); + await walletPage.selectActivityList(); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap ETH to WETH', + }); +}); + +test('Swap WETH to ETH - Switch to Avalanche and fetch quote - Switch DAI - USDC', async () => { + await walletPage.importTokens(); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ from: 'ETH', to: 'WETH', qty: '.001' }); + await swapPage.swap(); + await swapPage.waitForTransactionToComplete(); + await walletPage.selectActivityList(); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap ETH to WETH', + }); + + await networkController.addPopularNetwork({ + networkName: 'Avalanche Network C-Chain', + }); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ to: 'USDC', qty: '.001' }); + await swapPage.waitForInsufficentBalance(); + + await swapPage.gotBack(); + + await networkController.selectNetwork({ networkName: 'Tenderly' }); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap ETH to WETH', + }); + await walletPage.selectTokenWallet(); + await walletPage.importTokens(); + await walletPage.selectSwapAction(); + await swapPage.fetchQuote({ from: 'DAI', to: 'USDC', qty: '.5' }); + await swapPage.switchTokens(); + await swapPage.swap(); + await swapPage.waitForTransactionToComplete(); + await walletPage.selectActivityList(); + await activityListPage.checkActivityIsConfirmed({ + activity: 'Swap USDC to DAI', + }); +}); diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 380efd5c5205..0f400e8a34e7 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -37,8 +37,7 @@ "showFiatInTestnets": false, "showTestNetworks": false, "smartTransactionsOptInStatus": false, - "useNativeCurrencyAsPrimaryCurrency": true, - "showTokenAutodetectModal": "boolean" + "useNativeCurrencyAsPrimaryCurrency": true }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js index 0ff043261a7b..6150f5c71aa9 100644 --- a/test/e2e/run-all.js +++ b/test/e2e/run-all.js @@ -6,9 +6,10 @@ const { hideBin } = require('yargs/helpers'); const { runInShell } = require('../../development/lib/run-command'); const { exitWithError } = require('../../development/lib/exit-with-error'); const { loadBuildTypesConfig } = require('../../development/lib/build-type'); +const { filterE2eChangedFiles } = require('./changedFilesUtil'); // These tests should only be run on Flask for now. -const FLASK_ONLY_TESTS = ['test-snap-namelookup.spec.js']; +const FLASK_ONLY_TESTS = []; const getTestPathsForTestDir = async (testDir) => { const testFilenames = await fs.promises.readdir(testDir, { @@ -30,9 +31,47 @@ const getTestPathsForTestDir = async (testDir) => { return testPaths; }; +// Quality Gate Retries +const RETRIES_FOR_NEW_OR_CHANGED_TESTS = 5; + +/** + * Runs the quality gate logic to filter and append changed or new tests if present. + * + * @param {string} fullTestList - List of test paths to be considered. + * @param {string[]} changedOrNewTests - List of changed or new test paths. + * @returns {string} The updated full test list. + */ +async function applyQualityGate(fullTestList, changedOrNewTests) { + let qualityGatedList = fullTestList; + + if (changedOrNewTests.length > 0) { + // Filter to include only the paths present in fullTestList + const filteredTests = changedOrNewTests.filter((test) => + fullTestList.includes(test), + ); + + // If there are any filtered tests, append them to fullTestList + if (filteredTests.length > 0) { + const filteredTestsString = filteredTests.join('\n'); + for (let i = 0; i < RETRIES_FOR_NEW_OR_CHANGED_TESTS; i++) { + qualityGatedList += `\n${filteredTestsString}`; + } + } + } + + return qualityGatedList; +} + // For running E2Es in parallel in CI -function runningOnCircleCI(testPaths) { - const fullTestList = testPaths.join('\n'); +async function runningOnCircleCI(testPaths) { + const changedOrNewTests = await filterE2eChangedFiles(); + console.log('Changed or new test list:', changedOrNewTests); + + const fullTestList = await applyQualityGate( + testPaths.join('\n'), + changedOrNewTests, + ); + console.log('Full test list:', fullTestList); fs.writeFileSync('test/test-results/fullTestList.txt', fullTestList); @@ -46,7 +85,7 @@ function runningOnCircleCI(testPaths) { // Report if no tests found, exit gracefully if (result.indexOf('There were no tests found') !== -1) { console.log(`run-all.js info: Skipping this node because "${result}"`); - return []; + return { fullTestList: [] }; } // If there's no text file, it means this node has no tests, so exit gracefully @@ -54,13 +93,15 @@ function runningOnCircleCI(testPaths) { console.log( 'run-all.js info: Skipping this node because there is no myTestList.txt', ); - return []; + return { fullTestList: [] }; } // take the space-delimited result and split into an array - return fs + const myTestList = fs .readFileSync('test/test-results/myTestList.txt', { encoding: 'utf8' }) .split(' '); + + return { fullTestList: myTestList, changedOrNewTests }; } async function main() { @@ -204,8 +245,10 @@ async function main() { await fs.promises.mkdir('test/test-results/e2e', { recursive: true }); let myTestList; + let changedOrNewTests; if (process.env.CIRCLECI) { - myTestList = runningOnCircleCI(testPaths); + ({ fullTestList: myTestList, changedOrNewTests = [] } = + await runningOnCircleCI(testPaths)); } else { myTestList = testPaths; } @@ -217,7 +260,12 @@ async function main() { if (testPath !== '') { testPath = testPath.replace('\n', ''); // sometimes there's a newline at the end of the testPath console.log(`\nExecuting testPath: ${testPath}\n`); - await runInShell('node', [...args, testPath]); + + const isTestChangedOrNew = changedOrNewTests?.includes(testPath); + const qualityGateArg = isTestChangedOrNew + ? ['--stop-after-one-failure'] + : []; + await runInShell('node', [...args, ...qualityGateArg, testPath]); } } } diff --git a/test/e2e/run-e2e-test.js b/test/e2e/run-e2e-test.js index a4c0496dbda6..8a86a72a07dc 100644 --- a/test/e2e/run-e2e-test.js +++ b/test/e2e/run-e2e-test.js @@ -35,7 +35,7 @@ async function main() { 'Set how many times the test should be retried upon failure.', type: 'number', }) - .option('retry-until-failure', { + .option('stop-after-one-failure', { default: false, description: 'Retries until the test fails', type: 'boolean', @@ -73,7 +73,7 @@ async function main() { mmi, e2eTestPath, retries, - retryUntilFailure, + stopAfterOneFailure, leaveRunning, updateSnapshot, updatePrivacySnapshot, @@ -136,12 +136,13 @@ async function main() { // Tests that contains `@no-mmi` will be grep (-g) and inverted (-i) // meaning that all tests with @no-mmi in the title will be ignored extraArgs.push('-g', '@no-mmi', '-i'); + process.env.MMI = 'true'; } const dir = 'test/test-results/e2e'; fs.mkdir(dir, { recursive: true }); - await retry({ retries, retryUntilFailure }, async () => { + await retry({ retries, stopAfterOneFailure }, async () => { await runInShell('yarn', [ 'mocha', `--config=${configFile}`, diff --git a/test/e2e/seeder/ganache.ts b/test/e2e/seeder/ganache.ts index e7eab2461a86..13f5b9d588f3 100644 --- a/test/e2e/seeder/ganache.ts +++ b/test/e2e/seeder/ganache.ts @@ -34,18 +34,24 @@ export class Ganache { }); } - async getBalance(): Promise { - const accounts = await this.getAccounts(); + async getBalance(address = null): Promise { const provider = await this.getProvider(); - if (!accounts?.[0] || !provider) { + if (!provider) { + console.log('No provider found'); + return 0; + } + + const accountToUse = address || (await this.getAccounts())?.[0]; + + if (!accountToUse) { console.log('No accounts found'); return 0; } const balanceHex = await provider.request({ method: 'eth_getBalance', - params: [accounts[0], 'latest'], + params: [accountToUse, 'latest'], }); const balanceInt = parseInt(balanceHex, 16) / 10 ** 18; diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index 98bbdfea099a..a7a69e13a13c 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -1,3 +1,3 @@ module.exports = { - TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.11.0/', + TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/snaps/test-snaps/2.13.0/', }; diff --git a/test/e2e/snaps/test-snap-cronjob.spec.js b/test/e2e/snaps/test-snap-cronjob.spec.js index feb3f539e96e..b19de4246ac3 100644 --- a/test/e2e/snaps/test-snap-cronjob.spec.js +++ b/test/e2e/snaps/test-snap-cronjob.spec.js @@ -68,7 +68,7 @@ describe('Test Snap Cronjob', function () { // look for the dialog popup to verify cronjob fired await driver.waitForSelector({ - css: '.snap-delineator__content', + css: '.snap-ui-renderer__content', text: 'This dialog was triggered by a cronjob', }); diff --git a/test/e2e/snaps/test-snap-dialog.spec.js b/test/e2e/snaps/test-snap-dialog.spec.js index 5f2700e5fd40..3b82781321da 100644 --- a/test/e2e/snaps/test-snap-dialog.spec.js +++ b/test/e2e/snaps/test-snap-dialog.spec.js @@ -2,14 +2,13 @@ const { defaultGanacheOptions, withFixtures, unlockWallet, - switchToNotificationWindow, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); describe('Test Snap Dialog', function () { - it('test all three snap_dialog types', async function () { + it('test all four snap_dialog types', async function () { await withFixtures( { fixtures: new FixtureBuilder().build(), @@ -34,7 +33,7 @@ describe('Test Snap Dialog', function () { await driver.clickElement('#connectdialogs'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -49,7 +48,7 @@ describe('Test Snap Dialog', function () { await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -63,11 +62,12 @@ describe('Test Snap Dialog', function () { text: 'Reconnect to Dialogs Snap', }); + // test 1 - alert dialog // click on alert dialog await driver.clickElement('#sendAlertButton'); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // check dialog contents const result = await driver.findElement('.snap-ui-renderer__panel'); @@ -78,7 +78,7 @@ describe('Test Snap Dialog', function () { }); // click ok button - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -92,14 +92,15 @@ describe('Test Snap Dialog', function () { text: 'null', }); + // test 2 - confirmation dialog // click conf button await driver.clickElement('#sendConfirmationButton'); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click reject - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Reject', tag: 'button', }); @@ -117,10 +118,10 @@ describe('Test Snap Dialog', function () { await driver.clickElement('#sendConfirmationButton'); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click accept - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Approve', tag: 'button', }); @@ -134,14 +135,15 @@ describe('Test Snap Dialog', function () { text: 'true', }); + // test 3 - prompt dialog // click prompt button await driver.clickElement('#sendPromptButton'); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click cancel button - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Cancel', tag: 'button', }); @@ -159,13 +161,13 @@ describe('Test Snap Dialog', function () { await driver.clickElement('#sendPromptButton'); // switch to dialog popup - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // fill '2323' in form field await driver.pasteIntoField('.mm-input', '2323'); // click submit button - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'Submit', tag: 'button', }); @@ -178,6 +180,52 @@ describe('Test Snap Dialog', function () { css: '#dialogResult', text: '"2323"', }); + + // test 4 - custom dialog + // click custom button + await driver.clickElement('#sendCustomButton'); + + // switch to dialog popup + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // click cancel button + await driver.clickElementAndWaitForWindowToClose({ + text: 'Cancel', + tag: 'span', + }); + + // switch back to test snaps tab + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // check result is equal to 'null' + await driver.waitForSelector({ + css: '#dialogResult', + text: 'null', + }); + + // click prompt button + await driver.clickElement('#sendCustomButton'); + + // switch to dialog popup + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // fill '2323' in form field + await driver.pasteIntoField('#custom-input', '2323'); + + // click confirm button + await driver.clickElementAndWaitForWindowToClose({ + text: 'Confirm', + tag: 'span', + }); + + // switch back to test snaps tab + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // check result is equal to '2323' + await driver.waitForSelector({ + css: '#dialogResult', + text: '"2323"', + }); }, ); }); diff --git a/test/e2e/snaps/test-snap-get-locale.spec.js b/test/e2e/snaps/test-snap-get-locale.spec.js index 3c14dd462ee4..9ccd4f3c141f 100644 --- a/test/e2e/snaps/test-snap-get-locale.spec.js +++ b/test/e2e/snaps/test-snap-get-locale.spec.js @@ -88,9 +88,6 @@ describe('Test Snap Get Locale', function () { ); // click on the global action menu - await driver.waitForSelector( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement( '[data-testid="account-options-menu-button"]', ); @@ -115,10 +112,12 @@ describe('Test Snap Get Locale', function () { // try to select dansk from the list await driver.clickElement({ text: 'Dansk', tag: 'option' }); + // there are 2 re-renders which cause flakiness (issue #25651) + // the delay can be removed once the issue is fixed in the app level + await driver.delay(1000); + await driver.assertElementNotPresent('.loading-overlay'); + // click on the global action menu - await driver.waitForSelector( - '[data-testid="account-options-menu-button"]', - ); await driver.clickElement( '[data-testid="account-options-menu-button"]', ); diff --git a/test/e2e/snaps/test-snap-homepage.spec.js b/test/e2e/snaps/test-snap-homepage.spec.js index d02dd8852900..8f1e851d0a0d 100644 --- a/test/e2e/snaps/test-snap-homepage.spec.js +++ b/test/e2e/snaps/test-snap-homepage.spec.js @@ -88,10 +88,6 @@ describe('Test Snap Homepage', function () { }); // check that the home page appears and contains the right info - await driver.waitForSelector({ - text: 'Content from Home Page Example Snap', - tag: 'p', - }); await driver.waitForSelector({ text: 'Welcome to my Snap home page!', tag: 'p', diff --git a/test/e2e/snaps/test-snap-interactive-ui.spec.js b/test/e2e/snaps/test-snap-interactive-ui.spec.js index ec4d47fee817..edbea8567872 100644 --- a/test/e2e/snaps/test-snap-interactive-ui.spec.js +++ b/test/e2e/snaps/test-snap-interactive-ui.spec.js @@ -35,7 +35,11 @@ describe('Test Snap Interactive UI', function () { tag: 'button', }); - await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + // We need a bigger timeout as the Connect action takes some time + await driver.clickElementSafe( + '[data-testid="snap-install-scroll"]', + 3000, + ); await driver.waitForSelector({ text: 'Confirm' }); diff --git a/test/e2e/snaps/test-snap-jsx.spec.js b/test/e2e/snaps/test-snap-jsx.spec.js new file mode 100644 index 000000000000..b5f651125232 --- /dev/null +++ b/test/e2e/snaps/test-snap-jsx.spec.js @@ -0,0 +1,96 @@ +const { + defaultGanacheOptions, + withFixtures, + unlockWallet, + WINDOW_TITLES, +} = require('../helpers'); +const FixtureBuilder = require('../fixture-builder'); +const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); + +describe('Test Snap JSX', function () { + it('can use JSX for snap dialog', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + + // navigate to test snaps page and connect to wasm snap + await driver.driver.get(TEST_SNAPS_WEBSITE_URL); + + // wait for page to load + await driver.waitForSelector({ + text: 'Installed Snaps', + tag: 'h2', + }); + + // find and scroll to the jsx test and connect + const snapButton = await driver.findElement('#connectjsx'); + await driver.scrollToElement(snapButton); + await driver.delay(1000); + await driver.waitForSelector('#connectjsx'); + await driver.clickElement('#connectjsx'); + + // switch to dialog window and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.waitForSelector({ + text: 'Connect', + tag: 'button', + }); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'Confirm' }); + + await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'OK' }); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'OK', + tag: 'button', + }); + + // click send inputs on test snap page + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // wait for npm installation success + await driver.waitForSelector({ + css: '#connectjsx', + text: 'Reconnect to JSX Snap', + }); + + // click on show jsx dialog + await driver.clickElement('#displayJsx'); + + // switch to dialog window + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // check for count zero + await driver.waitForSelector({ + text: '0', + tag: 'p', + }); + + // click increment twice + await driver.clickElement('#increment'); + + // wait for count to be 1 + await driver.waitForSelector({ + text: '1', + tag: 'p', + }); + }, + ); + }); +}); diff --git a/test/e2e/snaps/test-snap-management.spec.js b/test/e2e/snaps/test-snap-management.spec.js index e2bef1c2663f..7e62311af6bc 100644 --- a/test/e2e/snaps/test-snap-management.spec.js +++ b/test/e2e/snaps/test-snap-management.spec.js @@ -116,6 +116,12 @@ describe('Test Snap Management', function () { await driver.switchToWindowWithTitle( WINDOW_TITLES.ExtensionInFullScreenView, ); + + // click the back arrow to return to the main extension page + await driver.waitForSelector('[aria-label="Back"]'); + await driver.clickElement('[aria-label="Back"]'); + + // click account options menu button await driver.waitForSelector( '[data-testid="account-options-menu-button"]', ); @@ -126,11 +132,22 @@ describe('Test Snap Management', function () { css: '[data-testid="global-menu-notification-count"]', text: '1', }); + // this click will close the menu await driver.clickElement( '[data-testid="account-options-menu-button"]', ); + // go into the notifications snap page + await driver.waitForSelector({ + text: 'Notifications Example Snap', + tag: 'p', + }); + await driver.clickElement({ + text: 'Notifications Example Snap', + tag: 'p', + }); + // try to remove snap await driver.clickElement({ text: 'Remove Notifications Example Snap', diff --git a/test/e2e/snaps/test-snap-metrics.spec.js b/test/e2e/snaps/test-snap-metrics.spec.js index 8c5978f28609..6856aab2fd08 100644 --- a/test/e2e/snaps/test-snap-metrics.spec.js +++ b/test/e2e/snaps/test-snap-metrics.spec.js @@ -1,7 +1,6 @@ const { strict: assert } = require('assert'); const { withFixtures, - switchToNotificationWindow, unlockWallet, getEventPayloads, WINDOW_TITLES, @@ -199,16 +198,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -292,16 +283,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -378,16 +361,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -455,16 +430,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectnotifications'); - // switch to metamask extension and click connect - const windowHandles = await driver.waitUntilXWindowHandles( - 3, - 1000, - 10000, - ); - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -485,8 +452,9 @@ describe('Test Snap Metrics', function () { }); // switch to the original MM tab - const extensionPage = windowHandles[0]; - await driver.switchToWindow(extensionPage); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // click on the global action menu await driver.waitForSelector( @@ -580,8 +548,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -589,6 +557,12 @@ describe('Test Snap Metrics', function () { await driver.waitForSelector({ text: 'Confirm' }); + // Wait for the permissions content to be rendered + await driver.waitForSelector({ + text: 'Bitcoin Legacy', + tag: 'span', + }); + await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); await driver.clickElement({ @@ -614,12 +588,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -633,8 +602,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdateNew'); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Confirm' }); @@ -653,7 +622,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // look for the correct version text await driver.waitForSelector({ @@ -705,6 +674,9 @@ describe('Test Snap Metrics', function () { .build(), title: this.test.fullTitle(), testSpecificMock: mockSegment, + ignoredConsoleErrors: [ + 'MetaMask - RPC Error: User rejected the request.', + ], }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); @@ -724,8 +696,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -733,6 +705,12 @@ describe('Test Snap Metrics', function () { await driver.waitForSelector({ text: 'Confirm' }); + // Wait for the permissions content to be rendered + await driver.waitForSelector({ + text: 'Bitcoin Legacy', + tag: 'span', + }); + await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); await driver.clickElement({ @@ -758,12 +736,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -777,8 +750,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdateNew'); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Confirm' }); @@ -789,7 +762,10 @@ describe('Test Snap Metrics', function () { tag: 'button', }); - await driver.switchToWindow(windowHandles[0]); + // It is necessary to use switchToWindowIfKnown because there is an alert open. + // Trying to use switchToWindowWithTitle (the new Socket version) or even + // closeAlertPopup will cause an error. + await driver.switchToWindowIfKnown(WINDOW_TITLES.TestSnaps); // check that snap updated event metrics have been sent const events = await getEventPayloads(driver, mockedEndpoints); @@ -834,7 +810,9 @@ describe('Test Snap Metrics', function () { .build(), title: this.test.fullTitle(), testSpecificMock: mockSegment, - ignoredConsoleErrors: ['Object'], + ignoredConsoleErrors: [ + 'MetaMask - RPC Error: Failed to fetch snap "npm:@metamask/bip32-example-snap": Failed to fetch tarball for package "@metamask/bip32-example-snap"..', + ], }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); @@ -854,8 +832,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.clickElement('#connectUpdate'); - // switch to metamask extension and click connect - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -863,6 +841,12 @@ describe('Test Snap Metrics', function () { await driver.waitForSelector({ text: 'Confirm' }); + // Wait for the permissions content to be rendered + await driver.waitForSelector({ + text: 'Bitcoin Legacy', + tag: 'span', + }); + await driver.clickElementSafe('[data-testid="snap-install-scroll"]'); await driver.clickElement({ @@ -888,12 +872,7 @@ describe('Test Snap Metrics', function () { }); // navigate to test snap page - const windowHandles = await driver.waitUntilXWindowHandles( - 1, - 1000, - 10000, - ); - await driver.switchToWindow(windowHandles[0]); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); // wait for npm installation success await driver.waitForSelector({ @@ -910,8 +889,8 @@ describe('Test Snap Metrics', function () { await driver.delay(1000); await driver.closeAlertPopup(); - // switch to metamask extension and update - await switchToNotificationWindow(driver, 2); + // switch to metamask popup and update + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector({ text: 'Update failed' }); diff --git a/test/e2e/snaps/test-snap-namelookup.spec.js b/test/e2e/snaps/test-snap-namelookup.spec.js index 4e58231256dd..94af575deb9c 100644 --- a/test/e2e/snaps/test-snap-namelookup.spec.js +++ b/test/e2e/snaps/test-snap-namelookup.spec.js @@ -50,7 +50,7 @@ describe('Test Snap Name Lookup', function () { await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -71,12 +71,18 @@ describe('Test Snap Name Lookup', function () { tag: 'p', }); + // ensure we are on Mainnet + await driver.waitForSelector('[data-testid="staking-entrypoint-0x1"]'); + // click send await driver.clickElement('[data-testid="eth-overview-send"]'); // wait for input field and enter name to lookup await driver.waitForSelector('[data-testid="ens-input"]'); - await driver.fill('[data-testid="ens-input"]', 'metamask.domain'); + await driver.pasteIntoField( + '[data-testid="ens-input"]', + 'metamask.domain', + ); // verify name output from snap await driver.waitForSelector({ diff --git a/test/e2e/snaps/test-snap-siginsights.spec.js b/test/e2e/snaps/test-snap-siginsights.spec.js index 6b43551ac182..8e90f8336b2e 100644 --- a/test/e2e/snaps/test-snap-siginsights.spec.js +++ b/test/e2e/snaps/test-snap-siginsights.spec.js @@ -4,24 +4,19 @@ const { defaultGanacheOptions, openDapp, unlockWallet, - switchToNotificationWindow, + tempToggleSettingRedesignedConfirmations, WINDOW_TITLES, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); const { TEST_SNAPS_WEBSITE_URL } = require('./enums'); describe('Test Snap Signature Insights', function () { - it('tests Signature Insights functionality', async function () { + it('tests Signature Insights functionality (New)', async function () { await withFixtures( { dapp: true, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - disabledRpcMethodPreferences: { - eth_sign: true, - }, - }) .build(), ganacheOptions: defaultGanacheOptions, failOnConsoleError: false, @@ -43,7 +38,7 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('#connectsignature-insights'); // switch to metamask extension and click connect - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.clickElement({ text: 'Connect', tag: 'button', @@ -58,7 +53,7 @@ describe('Test Snap Signature Insights', function () { await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -69,7 +64,273 @@ describe('Test Snap Signature Insights', function () { // open the test-dapp page await openDapp(driver); - // poll windowHandles and switch to test-dapp + // TEST ONE: personal sign + // find and scroll to personal sign and click sign + const personalSignButton1 = await driver.findElement('#personalSign'); + await driver.scrollToElement(personalSignButton1); + await driver.clickElement('#personalSign'); + + // switch back to MetaMask window and switch to tx insights pane + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for information from signature request screen + await driver.waitForSelector({ + text: 'Example `personal_sign` message', + tag: 'p', + }); + + // click down arrow + await driver.waitForSelector({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + await driver.clickElement({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + + // look for returned signature insights data + await driver.waitForSelector({ + text: '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', + tag: 'p', + }); + + // click sign button + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-button"]', + ); + + // switch back to test-dapp window + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // check result of test + await driver.waitForSelector({ + text: '0xa10b6707dd79e2f1f91ba243ab7abe15a46f58b052ad9cec170c5366ef5667c447a87eba2c0a9d4c9fbfa0a23e9db1fb55865d0568c32bd7cc681b8d0860e7af1b', + tag: 'span', + }); + + // TEST TWO: sign typed data + // find and scroll to sign typed data and click sign + const signTypedButton1 = await driver.findElement('#signTypedData'); + await driver.scrollToElement(signTypedButton1); + await driver.clickElement('#signTypedData'); + + // switch back to MetaMask window and switch to tx insights pane + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // wait for information from signature request screen + await driver.waitForSelector({ + text: 'Hi, Alice!', + tag: 'p', + }); + + // click down arrow + // await driver.waitForSelector('[aria-label="Scroll down"]'); + await driver.clickElementSafe('[aria-label="Scroll down"]'); + + // required: delay for scroll to render + await driver.delay(500); + + // click down arrow + await driver.waitForSelector({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + await driver.clickElement({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + + // look for returned signature insights data + await driver.waitForSelector({ + text: '1', + tag: 'p', + }); + + // click sign button + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-button"]', + ); + + // switch back to test-dapp window + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // check result of test + await driver.waitForSelector({ + text: '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', + tag: 'span', + }); + + // TEST THREE: sign typed data v3 + // find and scroll to sign typed data v3 and click sign + const signTypedV3Button1 = await driver.findElement('#signTypedDataV3'); + await driver.scrollToElement(signTypedV3Button1); + await driver.clickElement('#signTypedDataV3'); + + // switch back to MetaMask window and switch to tx insights pane + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + + // required: delay for scroll to render + await driver.delay(500); + + // wait for information from signature request screen + await driver.waitForSelector({ + text: 'Hello, Bob!', + tag: 'p', + }); + + // click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + + // required: delay for scroll to render + await driver.delay(500); + + // click signature insights + await driver.waitForSelector({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + await driver.clickElement({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + + // look for returned signature insights data + await driver.waitForSelector({ + text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', + tag: 'p', + }); + + // click sign button + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-button"]', + ); + + // switch back to test-dapp window + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // check result of test + await driver.waitForSelector({ + text: '0x0a22f7796a2a70c8dc918e7e6eb8452c8f2999d1a1eb5ad714473d36270a40d6724472e5609948c778a07216bd082b60b6f6853d6354c731fd8ccdd3a2f4af261b', + tag: 'span', + }); + + // TEST FOUR: sign typed data v4 + // find and scroll to sign typed data v4 and click sign + const signTypedV4Button1 = await driver.findElement('#signTypedDataV4'); + await driver.scrollToElement(signTypedV4Button1); + await driver.clickElement('#signTypedDataV4'); + + // switch back to MetaMask window and switch to tx insights pane + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + // click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + + // required: delay for scroll to render + await driver.delay(500); + + // wait for information from signature request screen + await driver.waitForSelector({ + text: 'Hello, Bob!', + tag: 'p', + }); + + // click down arrow + await driver.clickElementSafe('[aria-label="Scroll down"]'); + + // required: delay for scroll to render + await driver.delay(500); + + // click signature insights + await driver.waitForSelector({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + await driver.clickElement({ + text: 'Signature Insights Example Snap', + tag: 'span', + }); + + // look for returned signature insights data + await driver.waitForSelector({ + text: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC has been identified as a malicious verifying contract.', + tag: 'p', + }); + + // click sign button + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-button"]', + ); + + // switch back to test-dapp window + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + // check results of test + await driver.waitForSelector({ + text: '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', + tag: 'span', + }); + }, + ); + }); + + it('tests Signature Insights functionality (Legacy)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + ganacheOptions: defaultGanacheOptions, + failOnConsoleError: false, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); + + // navigate to test snaps page and connect + await driver.openNewPage(TEST_SNAPS_WEBSITE_URL); + await driver.delay(1000); + + // find and scroll to the transaction-insights test and connect + const snapButton1 = await driver.findElement( + '#connectsignature-insights', + ); + await driver.scrollToElement(snapButton1); + await driver.delay(1000); + await driver.clickElement('#connectsignature-insights'); + + // switch to metamask extension and click connect + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement({ + text: 'Connect', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'Confirm' }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + }); + + await driver.waitForSelector({ text: 'OK' }); + + await driver.clickElementAndWaitForWindowToClose({ + text: 'OK', + tag: 'button', + }); + + // switch to test-snaps page + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); + + // open the test-dapp page + await openDapp(driver); // TEST ONE: personal sign // find and scroll to personal sign and click sign @@ -78,7 +339,7 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('#personalSign'); // switch back to MetaMask window and switch to tx insights pane - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // wait for and click sign await clickSignOnSignatureConfirmation({ @@ -96,7 +357,9 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('.mm-checkbox__input-wrapper'); // click sign button - await driver.clickElement('[data-testid="snapInsightsButtonConfirm"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="snapInsightsButtonConfirm"]', + ); // switch back to test-dapp window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -114,7 +377,7 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('#signTypedData'); // switch back to MetaMask window and switch to tx insights pane - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // wait for and click sign await clickSignOnSignatureConfirmation({ @@ -132,7 +395,9 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('.mm-checkbox__input-wrapper'); // click sign button - await driver.clickElement('[data-testid="snapInsightsButtonConfirm"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="snapInsightsButtonConfirm"]', + ); // switch back to test-dapp window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -150,7 +415,7 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('#signTypedDataV3'); // switch back to MetaMask window and switch to tx insights pane - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click down arrow await driver.waitForSelector('.fa-arrow-down'); @@ -172,7 +437,9 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('.mm-checkbox__input-wrapper'); // click sign button - await driver.clickElement('[data-testid="snapInsightsButtonConfirm"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="snapInsightsButtonConfirm"]', + ); // switch back to test-dapp window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -190,7 +457,7 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('#signTypedDataV4'); // switch back to MetaMask window and switch to tx insights pane - await switchToNotificationWindow(driver, 4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // click down arrow await driver.waitForSelector('.fa-arrow-down'); @@ -212,7 +479,9 @@ describe('Test Snap Signature Insights', function () { await driver.clickElement('.mm-checkbox__input-wrapper'); // click sign button - await driver.clickElement('[data-testid="snapInsightsButtonConfirm"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="snapInsightsButtonConfirm"]', + ); // switch back to test-dapp window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -222,39 +491,6 @@ describe('Test Snap Signature Insights', function () { text: '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', tag: 'span', }); - - // TEST FIVE: eth_sign - - // scroll to and click eth sign button - const ethSignButton1 = await driver.findElement('#ethSign'); - await driver.scrollToElement(ethSignButton1); - await driver.clickElement('#ethSign'); - - // switch back to MetaMask window and switch to tx insights pane - await switchToNotificationWindow(driver, 4); - - // wait for and click sign - await clickSignOnSignatureConfirmation({ - driver, - snapSigInsights: true, - locatorID: '#ethSign', - }); - - // wait for and click signature warning sign button - // click checkbox to authorize signing - await driver.clickElement('.mm-checkbox__input-wrapper'); - - // click sign button - await driver.clickElement('[data-testid="snapInsightsButtonConfirm"]'); - - // switch back to test-dapp window - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - - // check results of test - await driver.waitForSelector({ - text: '"0x816ab6c5d5356548cc4e004ef35a37fdfab916742a2bbeda756cd064c3d3789a6557d41d49549be1de249e1937a8d048996dfcc70d0552111605dc7cc471e8531b"', - tag: 'span', - }); }, ); }); diff --git a/test/e2e/snaps/test-snap-ui-imgs.spec.js b/test/e2e/snaps/test-snap-ui-imgs.spec.js index d8bb8af1e1d8..4d8f17ad852a 100644 --- a/test/e2e/snaps/test-snap-ui-imgs.spec.js +++ b/test/e2e/snaps/test-snap-ui-imgs.spec.js @@ -52,7 +52,7 @@ describe('Test Snap Images', function () { // deal with OK button await driver.waitForSelector({ text: 'OK' }); - await driver.clickElement({ + await driver.clickElementAndWaitForWindowToClose({ text: 'OK', tag: 'button', }); @@ -70,31 +70,27 @@ describe('Test Snap Images', function () { await driver.clickElement('#showSVGImage'); // switch to notification window - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // check snaps ui image using waitForSelector await driver.waitForSelector('[data-testid="snaps-ui-image"]'); // click ok to close window - await driver.clickElement('[data-testid="confirmation-submit-button"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirmation-submit-button"]', + ); // switch back to test-snaps window await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); - // find and click cat image test + // find and click png image test await driver.clickElement('#showPNGImage'); // switch to notification window - await switchToNotificationWindow(driver, 2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // check snaps ui image using waitForSelector await driver.waitForSelector('[data-testid="snaps-ui-image"]'); - - // click ok to close window - await driver.clickElement('[data-testid="confirmation-submit-button"]'); - - // switch back to test-snaps window - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestSnaps); }, ); }); diff --git a/test/e2e/tests/account/account-details.spec.js b/test/e2e/tests/account/account-details.spec.js index fbf38e46250d..a21b9fad2e7d 100644 --- a/test/e2e/tests/account/account-details.spec.js +++ b/test/e2e/tests/account/account-details.spec.js @@ -104,7 +104,7 @@ describe('Show account details', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: tEn('create'), tag: 'button' }); + await driver.clickElement({ text: tEn('addAccount'), tag: 'button' }); await driver.assertElementNotPresent({ text: tEn('create'), tag: 'button', @@ -157,7 +157,7 @@ describe('Show account details', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: tEn('create'), tag: 'button' }); + await driver.clickElement({ text: tEn('addAccount'), tag: 'button' }); await driver.assertElementNotPresent({ text: tEn('create'), tag: 'button', diff --git a/test/e2e/tests/account/add-account.spec.js b/test/e2e/tests/account/add-account.spec.js index 1c66d7334d98..a7136837b01d 100644 --- a/test/e2e/tests/account/add-account.spec.js +++ b/test/e2e/tests/account/add-account.spec.js @@ -42,7 +42,7 @@ describe('Add account', function () { ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', text: '2nd account', @@ -81,7 +81,7 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); // Check address of 2nd account await locateAccountBalanceDOM(driver); @@ -166,7 +166,7 @@ describe('Add account', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); // Wait for 2nd account to be created await locateAccountBalanceDOM(driver); diff --git a/test/e2e/tests/account/import-flow.spec.js b/test/e2e/tests/account/import-flow.spec.js index e25174cfa249..3c05131090cc 100644 --- a/test/e2e/tests/account/import-flow.spec.js +++ b/test/e2e/tests/account/import-flow.spec.js @@ -16,6 +16,7 @@ const { } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); const { emptyHtmlPage } = require('../../mock-e2e'); +const { isManifestV3 } = require('../../../../shared/modules/mv3.utils'); const ganacheOptions = { accounts: [ @@ -46,7 +47,7 @@ describe('Import flow @no-mmi', function () { ganacheOptions, title: this.test.fullTitle(), }, - async ({ driver }) => { + async ({ driver, ganacheServer }) => { await driver.navigate(); await completeImportSRPOnboardingFlow( @@ -60,7 +61,7 @@ describe('Import flow @no-mmi', function () { await driver.clickElement( '[data-testid="account-list-item-menu-button"]', ); - await driver.clickElement('[data-testid="account-list-menu-details"'); + await driver.clickElement('[data-testid="account-list-menu-details"]'); await driver.findVisibleElement('.qr-code__wrapper'); // shows a QR code for the account @@ -101,12 +102,15 @@ describe('Import flow @no-mmi', function () { await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', ); - await driver.clickElement({ text: 'Add a new account', tag: 'button' }); + await driver.clickElement({ + text: 'Add a new Ethereum account', + tag: 'button', + }); // set account name await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); // should show the correct account name const accountName = await driver.isElementPresent({ @@ -125,6 +129,7 @@ describe('Import flow @no-mmi', function () { // Send ETH from inside MetaMask // starts a send transaction + await locateAccountBalanceDOM(driver, ganacheServer); await openActionMenuAndStartSendFlow(driver); await driver.fill( 'input[placeholder="Enter public address (0x) or ENS name"]', @@ -182,11 +187,21 @@ describe('Import flow @no-mmi', function () { ); await driver.clickElement('[data-testid="account-list-menu-details"'); await driver.findVisibleElement('.qr-code__wrapper'); - // shows the correct account address - await driver.findElement({ - css: '.qr-code [data-testid="address-copy-button-text"]', - text: testAddress, - }); + + // Extract address segments from the DOM + const outerSegment = await driver.findElement( + '.qr-code__address-segments', + ); + + // Get the text content of each segment + const displayedAddress = await outerSegment.getText(); + + // Assert that the displayed address matches the testAddress + assert.strictEqual( + displayedAddress.toLowerCase(), + testAddress.toLowerCase(), + 'The displayed address does not match the test address', + ); }, ); }); @@ -316,7 +331,8 @@ describe('Import flow @no-mmi', function () { '[data-testid="import-account-confirm-button"]', ); - await locateAccountBalanceDOM(driver, ganacheServer); + const importedAccount = '0x0961Ca10D49B9B8e371aA0Bcf77fE5730b18f2E4'; + await locateAccountBalanceDOM(driver, ganacheServer, importedAccount); // New imported account has correct name and label await driver.findClickableElement({ css: '[data-testid="account-menu-icon"]', @@ -410,11 +426,7 @@ describe('Import flow @no-mmi', function () { const allWindows = await driver.waitUntilXWindowHandles(2); - const isMv3Enabled = - process.env.ENABLE_MV3 === 'true' || - process.env.ENABLE_MV3 === undefined; - - assert.equal(allWindows.length, isMv3Enabled ? 3 : 2); + assert.equal(allWindows.length, isManifestV3 ? 3 : 2); }, ); }); diff --git a/test/e2e/tests/account/incremental-security.spec.js b/test/e2e/tests/account/incremental-security.spec.js index bdbde3b26101..f16fb5350b91 100644 --- a/test/e2e/tests/account/incremental-security.spec.js +++ b/test/e2e/tests/account/incremental-security.spec.js @@ -73,11 +73,10 @@ describe('Incremental Security', function () { ); await driver.clickElement('[data-testid="account-list-menu-details"'); - // gets the current accounts address - const address = await driver.findElement( - '.qr-code .multichain-address-copy-button', + const outerSegment = await driver.findElement( + '.qr-code__address-segments', ); - const publicAddress = await address.getText(); + const publicAddress = await outerSegment.getText(); // wait for account modal to be visible await driver.findVisibleElement( diff --git a/test/e2e/tests/account/lockdown.spec.js b/test/e2e/tests/account/lockdown.spec.js index 9a9dbab12ff1..c0d5d52ee412 100644 --- a/test/e2e/tests/account/lockdown.spec.js +++ b/test/e2e/tests/account/lockdown.spec.js @@ -7,6 +7,7 @@ const { const { convertToHexValue, withFixtures } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); const FixtureBuilder = require('../../fixture-builder'); +const { isManifestV3 } = require('../../../../shared/modules/mv3.utils'); const isFirefox = process.env.SELENIUM_BROWSER === Browser.FIREFOX; @@ -90,11 +91,11 @@ describe('lockdown', function () { title: this.test.fullTitle(), }, async ({ driver }) => { - if (process.env.ENABLE_MV3 === 'false') { - await driver.navigate(PAGES.BACKGROUND); - } else { + if (isManifestV3) { // TODO: add logic for testing the Service-Worker on MV3 await driver.navigate(PAGES.OFFSCREEN); + } else { + await driver.navigate(PAGES.BACKGROUND); } await driver.delay(1000); assert.equal( diff --git a/test/e2e/tests/account/metamask-responsive-ui.spec.js b/test/e2e/tests/account/metamask-responsive-ui.spec.js index b13390288b16..d9e9aa2f965c 100644 --- a/test/e2e/tests/account/metamask-responsive-ui.spec.js +++ b/test/e2e/tests/account/metamask-responsive-ui.spec.js @@ -11,7 +11,7 @@ const FixtureBuilder = require('../../fixture-builder'); describe('MetaMask Responsive UI', function () { it('Creating a new wallet @no-mmi', async function () { - const driverOptions = { openDevToolsForTabs: true }; + const driverOptions = { responsive: true }; await withFixtures( { @@ -81,7 +81,7 @@ describe('MetaMask Responsive UI', function () { }); it('Importing existing wallet from lock page', async function () { - const driverOptions = { openDevToolsForTabs: true }; + const driverOptions = { responsive: true }; await withFixtures( { @@ -115,7 +115,7 @@ describe('MetaMask Responsive UI', function () { }); it('Send Transaction from responsive window', async function () { - const driverOptions = { openDevToolsForTabs: true }; + const driverOptions = { responsive: true }; await withFixtures( { fixtures: new FixtureBuilder().build(), diff --git a/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts b/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts index 5ab35138ce7b..f0cd40cb7373 100644 --- a/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts +++ b/test/e2e/tests/api-usage/account-tracker-api-usage.spec.ts @@ -1,14 +1,13 @@ import { strict as assert } from 'assert'; -import { MockedEndpoint } from 'mockttp'; import { JsonRpcRequest } from '@metamask/utils'; +import { MockedEndpoint } from 'mockttp'; +import FixtureBuilder from '../../fixture-builder'; import { - withFixtures, defaultGanacheOptions, unlockWallet, veryLargeDelayMs, + withFixtures, } from '../../helpers'; -import FixtureBuilder from '../../fixture-builder'; -import { Driver } from '../../webdriver/driver'; import { Mockttp } from '../../mock-e2e'; async function mockInfura(mockServer: Mockttp): Promise { @@ -72,7 +71,7 @@ async function getAllInfuraJsonRpcRequests( ); for (const r of seenProviderRequests) { - const json = await r.body.getJson(); + const json = (await r.body.getJson()) as JsonRpcRequest | undefined; if (json !== undefined) { allInfuraJsonRpcRequests.push(json); } @@ -106,13 +105,7 @@ describe('Account Tracker API Usage', function () { title: this.test?.fullTitle(), testSpecificMock: mockInfura, }, - async ({ - driver, - mockedEndpoint, - }: { - driver: Driver; - mockedEndpoint: MockedEndpoint[]; - }) => { + async ({ driver, mockedEndpoint }) => { await driver.delay(veryLargeDelayMs); let allInfuraJsonRpcRequests = await getAllInfuraJsonRpcRequests( mockedEndpoint, @@ -164,13 +157,7 @@ describe('Account Tracker API Usage', function () { title: this.test?.fullTitle(), testSpecificMock: mockInfura, }, - async ({ - driver, - mockedEndpoint, - }: { - driver: Driver; - mockedEndpoint: MockedEndpoint[]; - }) => { + async ({ driver, mockedEndpoint }) => { await unlockWallet(driver); await driver.delay(veryLargeDelayMs); const initialInfuraJsonRpcRequests = await getAllInfuraJsonRpcRequests( diff --git a/test/e2e/tests/bridge/bridge-button-opens-portfolio.spec.ts b/test/e2e/tests/bridge/bridge-button-opens-portfolio.spec.ts new file mode 100644 index 000000000000..fc79bcaa3e4c --- /dev/null +++ b/test/e2e/tests/bridge/bridge-button-opens-portfolio.spec.ts @@ -0,0 +1,39 @@ +import { Suite } from 'mocha'; +import { logInWithBalanceValidation, withFixtures } from '../../helpers'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button @no-mmi', function (this: Suite) { + it('loads portfolio tab from wallet overview when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle()), + async ({ driver, ganacheServer }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + await bridgePage.navigateToBridgePage(); + await bridgePage.verifyPortfolioTab(2); + }, + ); + }); + + it('loads portfolio tab from asset overview when flag is turned off', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle(), undefined, true), + async ({ driver, ganacheServer }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + + // ETH + await bridgePage.navigateToAssetPage('ETH'); + await bridgePage.navigateToBridgePage('coin-overview'); + await bridgePage.verifyPortfolioTab(2); + + await bridgePage.reloadHome(); + + // TST + await bridgePage.navigateToAssetPage('TST'); + await bridgePage.navigateToBridgePage('token-overview'); + await bridgePage.verifyPortfolioTab(3); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-button-routing.spec.ts b/test/e2e/tests/bridge/bridge-button-routing.spec.ts new file mode 100644 index 000000000000..a3ecb70a2967 --- /dev/null +++ b/test/e2e/tests/bridge/bridge-button-routing.spec.ts @@ -0,0 +1,17 @@ +import { Suite } from 'mocha'; +import { logInWithBalanceValidation, withFixtures } from '../../helpers'; +import { BridgePage, getBridgeFixtures } from './bridge-test-utils'; + +describe('Click bridge button @no-mmi', function (this: Suite) { + it('loads placeholder swap route from wallet overview when flag is turned on', async function () { + await withFixtures( + getBridgeFixtures(this.test?.fullTitle(), { 'extension-support': true }), + async ({ driver, ganacheServer }) => { + const bridgePage = new BridgePage(driver); + await logInWithBalanceValidation(driver, ganacheServer); + await bridgePage.navigateToBridgePage(); + await bridgePage.verifySwapPage(1); + }, + ); + }); +}); diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts new file mode 100644 index 000000000000..596c7623208d --- /dev/null +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -0,0 +1,156 @@ +import { strict as assert } from 'assert'; +import { Mockttp } from 'mockttp'; +import { Browser } from 'selenium-webdriver'; +import FixtureBuilder from '../../fixture-builder'; +import { generateGanacheOptions } from '../../helpers'; +import { + BRIDGE_CLIENT_ID, + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, +} from '../../../../shared/constants/bridge'; +import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { Driver } from '../../webdriver/driver'; +import { FeatureFlagResponse } from '../../../../ui/pages/bridge/bridge.util'; +import { isManifestV3 } from '../../../../shared/modules/mv3.utils'; +import { + DEFAULT_FEATURE_FLAGS_RESPONSE, + ETH_CONVERSION_RATE_USD, + MOCK_CURRENCY_RATES, +} from './constants'; + +const IS_FIREFOX = process.env.SELENIUM_BROWSER === Browser.FIREFOX; + +export class BridgePage { + driver: Driver; + + constructor(driver: Driver) { + this.driver = driver; + } + + reloadHome = async () => { + await this.driver.navigate(); + }; + + navigateToBridgePage = async ( + location: + | 'wallet-overview' + | 'coin-overview' + | 'token-overview' = 'wallet-overview', + ) => { + // Mitigates flakiness by waiting for the feature flags to be fetched + await this.driver.delay(3000); + let bridgeButtonTestIdPrefix; + switch (location) { + case 'wallet-overview': + bridgeButtonTestIdPrefix = 'eth'; + break; + case 'coin-overview': // native asset page + bridgeButtonTestIdPrefix = 'coin'; + break; + case 'token-overview': + default: + bridgeButtonTestIdPrefix = 'token'; + } + await this.driver.clickElement( + `[data-testid="${bridgeButtonTestIdPrefix}-overview-bridge"]`, + ); + }; + + navigateToAssetPage = async (symbol: string) => { + await this.driver.clickElement({ + css: '[data-testid="multichain-token-list-button"]', + text: symbol, + }); + + await this.driver.delay(2000); + assert.ok((await this.driver.getCurrentUrl()).includes('asset')); + }; + + verifyPortfolioTab = async (expectedHandleCount: number) => { + await this.driver.delay(4000); + await this.driver.switchToWindowWithTitle('MetaMask Portfolio - Bridge'); + assert.equal( + (await this.driver.getAllWindowHandles()).length, + IS_FIREFOX || !isManifestV3 + ? expectedHandleCount + : expectedHandleCount + 1, + ); + assert.match( + await this.driver.getCurrentUrl(), + /^https:\/\/portfolio\.metamask\.io\/bridge/u, + ); + }; + + verifySwapPage = async (expectedHandleCount: number) => { + await this.driver.delay(4000); + await this.driver.waitForSelector({ + css: '.bridge__title', + text: 'Bridge', + }); + assert.equal( + (await this.driver.getAllWindowHandles()).length, + IS_FIREFOX || !isManifestV3 + ? expectedHandleCount + : expectedHandleCount + 1, + ); + assert.match(await this.driver.getCurrentUrl(), /.+cross-chain\/swaps/u); + }; +} + +const mockServer = + (featureFlagOverrides: Partial) => + async (mockServer_: Mockttp) => { + const featureFlagMocks = [ + `${BRIDGE_DEV_API_BASE_URL}/getAllFeatureFlags`, + `${BRIDGE_PROD_API_BASE_URL}/getAllFeatureFlags`, + ].map( + async (url) => + await mockServer_ + .forGet(url) + .withHeaders({ 'X-Client-Id': BRIDGE_CLIENT_ID }) + .always() + .thenCallback(() => { + return { + statusCode: 200, + json: { + ...DEFAULT_FEATURE_FLAGS_RESPONSE, + ...featureFlagOverrides, + }, + }; + }), + ); + return Promise.all(featureFlagMocks); + }; + +export const getBridgeFixtures = ( + title?: string, + featureFlags: Partial = {}, + withErc20: boolean = true, +) => { + const fixtureBuilder = new FixtureBuilder({ + inputChainId: CHAIN_IDS.MAINNET, + }) + .withNetworkControllerOnMainnet() + .withCurrencyController(MOCK_CURRENCY_RATES) + .withBridgeControllerDefaultState(); + + if (withErc20) { + fixtureBuilder.withTokensControllerERC20({ chainId: 1 }); + } + + return { + driverOptions: { + // openDevToolsForTabs: true, + }, + fixtures: fixtureBuilder.build(), + testSpecificMock: mockServer(featureFlags), + smartContract: SMART_CONTRACTS.HST, + ethConversionInUsd: ETH_CONVERSION_RATE_USD, + ganacheOptions: generateGanacheOptions({ + hardfork: 'london', + chain: { chainId: CHAIN_IDS.MAINNET }, + }), + title, + }; +}; diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts new file mode 100644 index 000000000000..e79a1dfe4553 --- /dev/null +++ b/test/e2e/tests/bridge/constants.ts @@ -0,0 +1,23 @@ +import { FeatureFlagResponse } from '../../../../ui/pages/bridge/bridge.util'; + +export const DEFAULT_FEATURE_FLAGS_RESPONSE: FeatureFlagResponse = { + 'extension-support': false, + 'src-network-allowlist': [1, 42161, 59144], + 'dest-network-allowlist': [1, 42161, 59144], +}; + +export const LOCATOR = { + MM_IMPORT_TOKENS_MODAL: (suffix: string) => + `[data-testid="import-tokens-modal-${suffix}"]`, +}; + +export const ETH_CONVERSION_RATE_USD = 3010; +export const MOCK_CURRENCY_RATES = { + currencyRates: { + ETH: { + conversionDate: 1665507609.0, + conversionRate: ETH_CONVERSION_RATE_USD, + usdConversionRate: ETH_CONVERSION_RATE_USD, + }, + }, +}; diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts new file mode 100644 index 000000000000..f8d70b8e6568 --- /dev/null +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -0,0 +1,89 @@ +import { strict as assert } from 'assert'; +import FixtureBuilder from '../../../fixture-builder'; +import { + PRIVATE_KEY, + convertETHToHexGwei, + withFixtures, + WINDOW_TITLES, +} from '../../../helpers'; +import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; +import { scrollAndConfirmAndAssertConfirm } from '../helpers'; +import { + TestSuiteArguments, + openDAppWithContract, +} from '../transactions/shared'; +import { Driver } from '../../../webdriver/driver'; + +describe('Alert for insufficient funds @no-mmi', function () { + it('Shows an alert when the user tries to send a transaction with insufficient funds', async function () { + const nftSmartContract = SMART_CONTRACTS.NFTS; + const ganacheOptions = { + accounts: [ + { + secretKey: PRIVATE_KEY, + balance: convertETHToHexGwei(0.0053), // Low balance only to create the contract and then trigger the alert for insufficient funds + }, + ], + }; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions, + smartContract: nftSmartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, nftSmartContract); + + await mintNft(driver); + + await verifyAlertForInsufficientBalance(driver); + + await scrollAndConfirmAndAssertConfirm(driver); + + await verifyConfirmationIsDisabled(driver); + }, + ); + }); +}); + +async function verifyConfirmationIsDisabled(driver: Driver) { + const confirmButton = await driver.findElement( + '[data-testid="confirm-alert-modal-submit-button"]', + ); + assert.equal(await confirmButton.isEnabled(), false); +} + +async function verifyAlertForInsufficientBalance(driver: Driver) { + const alert = await driver.findElement('[data-testid="inline-alert"]'); + assert.equal(await alert.getText(), 'Alert'); + await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); + await driver.clickElement('[data-testid="inline-alert"]'); + + const alertDescription = await driver.findElement( + '[data-testid="alert-modal__selected-alert"]', + ); + const alertDescriptionText = await alertDescription.getText(); + assert.equal( + alertDescriptionText, + 'You do not have enough ETH in your account to pay for transaction fees.', + ); + await driver.clickElement('[data-testid="alert-modal-close-button"]'); +} + +async function mintNft(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement(`#mintButton`); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} diff --git a/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts b/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts new file mode 100644 index 000000000000..6fd44e6f7af1 --- /dev/null +++ b/test/e2e/tests/confirmations/alerts/queued-confirmations.spec.ts @@ -0,0 +1,507 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { strict as assert } from 'assert'; +import { MockedEndpoint, MockttpServer } from 'mockttp'; +import { MetaMetricsEventName } from '../../../../../shared/constants/metametrics'; +import { getEventPayloads } from '../../../helpers'; +import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; +import { Driver } from '../../../webdriver/driver'; +import { + createDepositTransaction, + openDAppWithContract, + TestSuiteArguments, +} from '../transactions/shared'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; + +const FixtureBuilder = require('../../../fixture-builder'); +const { + withFixtures, + openDapp, + unlockWallet, + DAPP_URL, + DAPP_ONE_URL, + regularDelayMs, + WINDOW_TITLES, + defaultGanacheOptions, +} = require('../../../helpers'); + +const PORT = 8546; +const CHAIN_ID = 1338; +const PORT_ONE = 7777; +const CHAIN_ID_ONE = 1000; + +describe('Queued Confirmations', function () { + if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { + return; + } + + describe('Queued Requests Banner Alert', function () { + it('Banner is shown on dApp 1, but not on dApp 2 after adding transaction on dApp 1, and one on dApp 2 (old confirmation flow)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port: PORT, + chainId: CHAIN_ID, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: PORT_ONE, + chainId: CHAIN_ID_ONE, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test?.fullTitle(), + }, + async ({ driver }: TestSuiteArguments) => { + await unlockWallet(driver); + + await connectToDappOne(driver); + await connectToDappTwoAndSwitchBackToOne(driver); + + await switchChainToDappOne(driver); + + await switchToDAppAndCreateTransactionRequest(driver); + await switchToDAppTwoAndCreateSignTypedDataRequest(driver); + + await assertBannerExistsOnConfirmation(driver); + await rejectConfirmation(driver); + await assertBannerDoesNotExistOnConfirmation(driver); + }, + ); + }); + + it('Banner is shown on dApp 1, but not on dApp 2 after adding multiple transactions on dApp 1, and one on dApp 2', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port: PORT, + chainId: CHAIN_ID, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: PORT_ONE, + chainId: CHAIN_ID_ONE, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test?.fullTitle(), + }, + async ({ driver }: TestSuiteArguments) => { + await unlockWallet(driver); + + await connectToDappOne(driver); + await connectToDappTwoAndSwitchBackToOne(driver); + + await switchChainToDappOne(driver); + + await switchToDAppAndCreateTransactionRequest(driver); + await switchToDAppAndCreateTransactionRequest(driver); + await switchToDAppAndCreateTransactionRequest(driver); + + await switchToDAppTwoAndCreateSignTypedDataRequest(driver); + + await assertBannerExistsOnConfirmation(driver); + await rejectConfirmation(driver); + await rejectConfirmation(driver); + await rejectConfirmation(driver); + await assertBannerDoesNotExistOnConfirmation(driver); + }, + ); + }); + + it('Banner is shown on dApp 1, but not on dApp 2 after adding transaction on dApp 1, and one on dApp 2 (redesigned confirmation flow)', async function () { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { redesignedConfirmationsEnabled: true }, + useRequestQueue: true, + }) + .withSelectedNetworkControllerPerDomain() + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port: PORT, + chainId: CHAIN_ID, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: PORT_ONE, + chainId: CHAIN_ID_ONE, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + + await connectToDappTwoAndSwitchBackToOne(driver, contractAddress); + + // create deposit transaction in dapp 1 + await createDepositTransaction(driver); + + await driver.delay(2000); + + await switchToDAppTwoAndCreateSignTypedDataRequest(driver); + + await assertBannerExistsOnConfirmation(driver); + }, + ); + }); + }); + + describe('Navigation and Banner Metrics', function () { + it('Metric is sent from the nav bar and the banner alert (old confirmation flow)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPreferencesControllerUseRequestQueueEnabled() + .withSelectedNetworkControllerPerDomain() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port: PORT, + chainId: CHAIN_ID, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: PORT_ONE, + chainId: CHAIN_ID_ONE, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + title: this.test?.fullTitle(), + testSpecificMock: queueControllerMocks, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await connectToDappOne(driver); + await connectToDappTwoAndSwitchBackToOne(driver); + + await switchChainToDappOne(driver); + + await switchToDAppAndCreateTransactionRequest(driver); + await switchToDAppAndCreateTransactionRequest(driver); + await switchToDAppTwoAndCreateSignTypedDataRequest(driver); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + + assert.equal(events.length, 2); + + assert.equal( + events[0].event, + MetaMetricsEventName.ConfirmationQueued, + ); + assert.equal(events[0].properties.category, 'Confirmations'); + assert.equal(events[0].properties.chain_id, '0x3e8'); + assert.equal(events[0].properties.environment_type, 'notification'); + assert.equal(events[0].properties.locale, 'en'); + assert.equal(events[0].properties.queue_size, 1); + assert.equal(events[0].properties.queue_type, 'navigation_header'); + assert.equal(events[0].properties.referrer, 'http://127.0.0.1:8080'); + assert.equal(events[0].properties.confirmation_type, 'transaction'); + + assert.equal( + events[1].event, + MetaMetricsEventName.ConfirmationQueued, + ); + assert.equal(events[1].properties.category, 'Confirmations'); + assert.equal(events[1].properties.chain_id, '0x3e8'); + assert.equal(events[1].properties.environment_type, 'notification'); + assert.equal(events[1].properties.locale, 'en'); + assert.equal(events[1].properties.queue_size, 1); + assert.equal(events[1].properties.queue_type, 'queue_controller'); + assert.equal(events[1].properties.referrer, 'http://127.0.0.1:8080'); + assert.equal(events[1].properties.confirmation_type, 'transaction'); + }, + ); + }); + + it('Metric is sent from the nav bar and the banner alert (redesigned confirmation flow)', async function () { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withNetworkControllerTripleGanache() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { redesignedConfirmationsEnabled: true }, + useRequestQueue: true, + }) + .withSelectedNetworkControllerPerDomain() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + dappOptions: { numberOfDapps: 2 }, + ganacheOptions: { + ...defaultGanacheOptions, + concurrent: [ + { + port: PORT, + chainId: CHAIN_ID, + ganacheOptions2: defaultGanacheOptions, + }, + { + port: PORT_ONE, + chainId: CHAIN_ID_ONE, + ganacheOptions2: defaultGanacheOptions, + }, + ], + }, + smartContract, + title: this.test?.fullTitle(), + testSpecificMock: queueControllerMocks, + }, + async ({ + driver, + contractRegistry, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + + await connectToDappTwoAndSwitchBackToOne(driver, contractAddress); + + // create deposit transaction in dapp 1 + await createDepositTransaction(driver); + + await driver.delay(2000); + + await switchToDAppTwoAndCreateSignTypedDataRequest(driver); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + + assert.equal(events.length, 2); + + assert.equal( + events[0].event, + MetaMetricsEventName.ConfirmationQueued, + ); + assert.equal(events[0].properties.category, 'Confirmations'); + assert.equal(events[0].properties.chain_id, '0x539'); + assert.equal(events[0].properties.environment_type, 'notification'); + assert.equal(events[0].properties.locale, 'en'); + assert.equal(events[0].properties.queue_size, 1); + assert.equal(events[0].properties.queue_type, 'navigation_header'); + assert.equal(events[0].properties.referrer, 'http://127.0.0.1:8080'); + assert.equal(events[0].properties.confirmation_type, 'transaction'); + + assert.equal( + events[1].event, + MetaMetricsEventName.ConfirmationQueued, + ); + assert.equal(events[1].properties.category, 'Confirmations'); + assert.equal(events[1].properties.chain_id, '0x539'); + assert.equal(events[1].properties.environment_type, 'notification'); + assert.equal(events[1].properties.locale, 'en'); + assert.equal(events[1].properties.queue_size, 1); + assert.equal(events[1].properties.queue_type, 'queue_controller'); + assert.equal(events[1].properties.referrer, 'http://127.0.0.1:8080'); + assert.equal(events[1].properties.confirmation_type, 'transaction'); + }, + ); + }); + }); +}); + +async function connectToDappOne(driver: 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 driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); +} + +async function connectToDappTwoAndSwitchBackToOne( + driver: Driver, + contractAddress?: string, +) { + // Open Dapp Two + await openDapp(driver, undefined, DAPP_ONE_URL); + + // Connect to dapp 2 + await driver.findClickableElement({ text: 'Connect', tag: 'button' }); + await driver.clickElement('#connectButton'); + + await driver.delay(regularDelayMs); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ + text: 'Next', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + await driver.clickElement({ + text: 'Confirm', + tag: 'button', + css: '[data-testid="page-container-footer-next"]', + }); + + const url = `${DAPP_URL}${ + contractAddress ? `/?contract=${contractAddress}` : '' + }`; + + await driver.switchToWindowWithUrl(url); +} + +async function switchChainToDappOne(driver: Driver) { + // switch chain for Dapp One + const switchEthereumChainRequest = JSON.stringify({ + jsonrpc: '2.0', + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x3e8' }], + }); + + // Initiate switchEthereumChain on Dapp one + await driver.executeScript( + `window.ethereum.request(${switchEthereumChainRequest})`, + ); + + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.clickElement({ text: 'Switch network', tag: 'button' }); +} + +async function switchToDAppAndCreateTransactionRequest(driver: Driver) { + await driver.switchToWindowWithUrl(DAPP_URL); + + // eth_sendTransaction request + await driver.clickElement('#sendButton'); +} + +async function switchToDAppTwoAndCreateSignTypedDataRequest(driver: Driver) { + await driver.switchToWindowWithUrl(DAPP_ONE_URL); + + // signTypedData request + await driver.clickElement('#signTypedData'); +} + +const bannerCopy = + "To view and confirm your most recent request, you'll need to approve or reject existing requests first."; + +async function assertBannerExistsOnConfirmation(driver: Driver) { + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ css: 'p', text: bannerCopy }); +} + +async function rejectConfirmation(driver: Driver) { + await driver.clickElement({ css: 'button', text: 'Reject' }); +} + +async function assertBannerDoesNotExistOnConfirmation(driver: Driver) { + await driver.waitUntilXWindowHandles(4); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.assertElementNotPresent({ + css: 'p', + text: bannerCopy, + }); +} + +async function mockedTrackedQueueControllerEvent(mockServer: MockttpServer) { + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { type: 'track', event: MetaMetricsEventName.ConfirmationQueued }, + ], + }) + .thenCallback(() => ({ statusCode: 200 })); +} + +async function queueControllerMocks(server: MockttpServer) { + return [ + await mockedTrackedQueueControllerEvent(server), + await mockedTrackedQueueControllerEvent(server), + ]; +} diff --git a/test/e2e/tests/confirmations/contract-interaction-redesign.spec.js b/test/e2e/tests/confirmations/contract-interaction-redesign.spec.js deleted file mode 100644 index 96b0c2602476..000000000000 --- a/test/e2e/tests/confirmations/contract-interaction-redesign.spec.js +++ /dev/null @@ -1,194 +0,0 @@ -const { - defaultGanacheOptions, - openDapp, - unlockWallet, - WINDOW_TITLES, - withFixtures, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); -const { scrollAndConfirmAndAssertConfirm } = require('./helpers'); - -describe('Confirmation Redesign Contract Interaction Component', function () { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } - - it('Sends a contract interaction type 0 transaction without custom nonce editing', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - preferences: { redesignedConfirmationsEnabled: true }, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - await openDapp(driver); - - await createContractDeploymentTransaction(driver); - await confirmContractDeploymentTransaction(driver); - - await createDepositTransaction(driver); - await confirmDepositTransaction(driver); - }, - ); - }); - - it('Sends a contract interaction type 0 transaction with custom nonce editing', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - preferences: { redesignedConfirmationsEnabled: true }, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - await openDapp(driver); - - await toggleOnCustomNonce(driver); - - await createContractDeploymentTransaction(driver); - await confirmContractDeploymentTransaction(driver); - - await createDepositTransaction(driver); - await confirmDepositTransactionWithCustomNonce(driver, '10'); - }, - ); - }); -}); - -async function toggleOnCustomNonce(driver) { - // switch to metamask page and open the three dots menu - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - - // Open settings menu button - const accountOptionsMenuSelector = - '[data-testid="account-options-menu-button"]'; - await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); - - // Click settings from dropdown menu - const globalMenuSettingsSelector = '[data-testid="global-menu-settings"]'; - await driver.waitForSelector(globalMenuSettingsSelector); - await driver.clickElement(globalMenuSettingsSelector); - - // Click Advanced tab - const advancedTabRawLocator = { - text: 'Advanced', - tag: 'div', - }; - await driver.clickElement(advancedTabRawLocator); - - // Toggle on custom toggle setting (off by default) - await driver.clickElement('.custom-nonce-toggle'); - - // Close settings - await driver.clickElement( - '.settings-page__header__title-container__close-button', - ); -} - -async function createContractDeploymentTransaction(driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.clickElement(`#deployButton`); -} - -async function confirmContractDeploymentTransaction(driver) { - await driver.delay(2000); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.waitForSelector({ - css: '.confirm-page-container-summary__action__name', - text: 'Contract deployment', - }); - - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - await driver.clickElement({ text: 'Activity', tag: 'button' }); - await driver.waitForSelector( - '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', - ); -} - -async function createDepositTransaction(driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.clickElement(`#depositButton`); -} - -async function confirmDepositTransaction(driver) { - await driver.delay(2000); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.waitForSelector({ - css: 'h2', - text: 'Transaction request', - }); - - await toggleAdvancedDetails(driver); - - await driver.waitForSelector({ - css: 'p', - text: 'Nonce', - }); - - await scrollAndConfirmAndAssertConfirm(driver); -} - -async function confirmDepositTransactionWithCustomNonce(driver, customNonce) { - await driver.delay(2000); - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - - await driver.waitForSelector({ - css: 'h2', - text: 'Transaction request', - }); - - await toggleAdvancedDetails(driver); - - await driver.waitForSelector({ - css: 'p', - text: 'Nonce', - }); - - await driver.clickElement('.edit-nonce-btn'); - await driver.fill('[data-testid="custom-nonce-input"]', customNonce); - await driver.clickElement({ - text: 'Save', - tag: 'button', - }); - await scrollAndConfirmAndAssertConfirm(driver); - - // Confirm tx was submitted with the higher nonce - await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); - - await driver.delay(500); - - const sendTransactionListItem = await driver.findElement( - '.transaction-list__pending-transactions .activity-list-item', - ); - await sendTransactionListItem.click(); - - await driver.waitForSelector({ - css: '.transaction-breakdown__value', - text: customNonce, - }); -} - -async function toggleAdvancedDetails(driver) { - // TODO - Scroll button not shown in Firefox if advanced details enabled too fast. - await driver.delay(1000); - - await driver.clickElement(`[data-testid="header-advanced-details-button"]`); -} diff --git a/test/e2e/tests/confirmations/header.spec.js b/test/e2e/tests/confirmations/header.spec.js deleted file mode 100644 index c829958f8b5e..000000000000 --- a/test/e2e/tests/confirmations/header.spec.js +++ /dev/null @@ -1,112 +0,0 @@ -const { strict: assert } = require('assert'); -const { - defaultGanacheOptions, - openDapp, - unlockWallet, - WINDOW_TITLES, - withFixtures, -} = require('../../helpers'); -const FixtureBuilder = require('../../fixture-builder'); - -const SIGNATURE_CONFIRMATIONS = [ - { name: 'Personal Sign signature', testDAppBtnId: 'personalSign' }, - { name: 'Sign Typed Data signature', testDAppBtnId: 'signTypedData' }, - { name: 'Sign Typed Data v3 signature', testDAppBtnId: 'signTypedDataV3' }, - { name: 'Sign Typed Data v4 signature', testDAppBtnId: 'signTypedDataV4' }, - { name: 'Sign Permit signature', testDAppBtnId: 'signPermit' }, -]; - -const WALLET_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; -const WALLET_ETH_BALANCE = '25'; - -describe('Confirmation Header Component', function () { - SIGNATURE_CONFIRMATIONS.forEach((confirmation) => { - it(`${confirmation.name} component includes header with balance`, async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - preferences: { redesignedConfirmationsEnabled: true }, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - await openDapp(driver); - - await clickConfirmationBtnOnTestDapp( - driver, - confirmation.testDAppBtnId, - ); - await clickHeaderInfoBtn(driver); - - await assertHeaderInfoBalance(driver, WALLET_ETH_BALANCE); - }, - ); - }); - - it(`${confirmation.name} component includes copyable address element`, async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - preferences: { redesignedConfirmationsEnabled: true }, - }) - .build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }) => { - await unlockWallet(driver); - await openDapp(driver); - - await clickConfirmationBtnOnTestDapp( - driver, - confirmation.testDAppBtnId, - ); - await clickHeaderInfoBtn(driver); - await copyAddressAndPasteWalletAddress(driver); - - await assertPastedAddress(driver, WALLET_ADDRESS); - }, - ); - }); - - async function clickConfirmationBtnOnTestDapp(driver, btnId) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.clickElement(`#${btnId}`); - await driver.delay(2000); - } - - async function clickHeaderInfoBtn(driver) { - await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement('button[data-testid="header-info-button"]'); - } - - async function assertHeaderInfoBalance(driver, walletEthBalance) { - const headerBalanceEl = await driver.findElement( - '[data-testid="header-balance"]', - ); - await driver.waitForNonEmptyElement(headerBalanceEl); - assert.equal(await headerBalanceEl.getText(), `${walletEthBalance}\nETH`); - } - - async function copyAddressAndPasteWalletAddress(driver) { - await driver.clickElement('[data-testid="address-copy-button-text"]'); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - await driver.findElement('#eip747ContractAddress'); - await driver.pasteFromClipboardIntoField('#eip747ContractAddress'); - } - - async function assertPastedAddress(driver, walletAddress) { - const formFieldEl = await driver.findElement('#eip747ContractAddress'); - assert.equal(await formFieldEl.getProperty('value'), walletAddress); - } - }); -}); diff --git a/test/e2e/tests/confirmations/helpers.ts b/test/e2e/tests/confirmations/helpers.ts index b6a19a6b95a9..882990e4bf66 100644 --- a/test/e2e/tests/confirmations/helpers.ts +++ b/test/e2e/tests/confirmations/helpers.ts @@ -1,9 +1,10 @@ import FixtureBuilder from '../../fixture-builder'; import { defaultGanacheOptions, withFixtures } from '../../helpers'; +import { Mockttp } from '../../mock-e2e'; import { Driver } from '../../webdriver/driver'; export async function scrollAndConfirmAndAssertConfirm(driver: Driver) { - await driver.clickElement('.confirm-scroll-to-bottom__button'); + await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); await driver.clickElement('[data-testid="confirm-footer-button"]'); } @@ -23,6 +24,10 @@ export function withRedesignConfirmationFixtures( }, fixtures: new FixtureBuilder() .withPermissionControllerConnectedToTestDapp() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) .withPreferencesController({ preferences: { redesignedConfirmationsEnabled: true, @@ -31,7 +36,53 @@ export function withRedesignConfirmationFixtures( .build(), ganacheOptions: defaultGanacheOptions, title, + testSpecificMock: mockSegment, }, testFunction, ); } + +async function mockSegment(mockServer: Mockttp) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Signature Requested' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Signature Approved' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Signature Rejected' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Account Details Opened' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts new file mode 100644 index 000000000000..f6347981ce15 --- /dev/null +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -0,0 +1,204 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { + DAPP_HOST_ADDRESS, + WINDOW_TITLES, + openDapp, + unlockWallet, + regularDelayMs, +} from '../../helpers'; +import { Driver } from '../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from './helpers'; + +describe('Navigation Signature - Different signature types', function (this: Suite) { + it('initiates and queues multiple signatures and confirms', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await openDapp(driver); + await queueSignatures(driver); + + await verifySignTypedData(driver); + await driver.clickElement( + '[data-testid="confirm-nav__next-confirmation"]', + ); + + // Verify Sign Typed Data v3 confirmation is displayed + await verifySignedTypeV3Confirmation(driver); + + await driver.clickElement( + '[data-testid="confirm-nav__next-confirmation"]', + ); + + // Verify Sign Typed Data v4 confirmation is displayed + await verifySignedTypeV4Confirmation(driver); + + await driver.clickElement( + '[data-testid="confirm-nav__previous-confirmation"]', + ); + + // Verify Sign Typed Data v3 confirmation is displayed + await verifySignedTypeV3Confirmation(driver); + + await driver.clickElement( + '[data-testid="confirm-nav__previous-confirmation"]', + ); + // Verify Sign Typed Data v3 confirmation is displayed + await verifySignTypedData(driver); + }, + ); + }); + + it('initiates and queues a mix of signatures and transactions and navigates', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await openDapp(driver); + await queueSignaturesAndTransactions(driver); + + await verifySignTypedData(driver); + + await driver.clickElement( + '[data-testid="confirm-nav__next-confirmation"]', + ); + + // Verify Transaction Sending ETH is displayed + await verifyTransaction(driver, 'SENDING ETH'); + + await driver.clickElement('[data-testid="next-page"]'); + + // Verify Sign Typed Data v3 confirmation is displayed + await verifySignedTypeV3Confirmation(driver); + + await driver.clickElement( + '[data-testid="confirm-nav__previous-confirmation"]', + ); + + // Verify Sign Typed Data v3 confirmation is displayed + await verifyTransaction(driver, 'SENDING ETH'); + + await driver.clickElement('[data-testid="previous-page"]'); + + // Verify Sign Typed Data v3 confirmation is displayed + await verifySignTypedData(driver); + }, + ); + }); + + it('initiates multiple signatures and rejects all', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + await openDapp(driver); + await queueSignatures(driver); + await driver.delay(regularDelayMs); + + await driver.clickElement('[data-testid="confirm-nav__reject-all"]'); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await verifyRejectionResults(driver, '#signTypedDataResult'); + await verifyRejectionResults(driver, '#signTypedDataV3Result'); + await verifyRejectionResults(driver, '#signTypedDataV4Result'); + }, + ); + }); +}); + +async function verifySignTypedData(driver: Driver) { + const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); + const message = await driver.findElement({ text: 'Hi, Alice!' }); + + // Verify Sign Typed Data confirmation is displayed + assert.ok(origin, 'origin'); + assert.ok(message, 'message'); +} + +async function verifyRejectionResults(driver: Driver, verifyResultId: string) { + const rejectionResult = await driver.findElement(verifyResultId); + assert.equal( + await rejectionResult.getText(), + 'Error: User rejected the request.', + ); +} + +async function verifySignedTypeV3Confirmation(driver: Driver) { + const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); + const fromAddress = driver.findElement({ + css: '.name__value', + text: '0xCD2a3...DD826', + }); + const toAddress = driver.findElement({ + css: '.name__value', + text: '0xbBbBB...bBBbB', + }); + const contents = driver.findElement({ text: 'Hello, Bob!' }); + + assert.ok(await origin, 'origin'); + assert.ok(await fromAddress, 'fromAddress'); + assert.ok(await toAddress, 'toAddress'); + assert.ok(await contents, 'contents'); +} + +async function verifySignedTypeV4Confirmation(driver: Driver) { + verifySignedTypeV3Confirmation(driver); + const attachment = driver.findElement({ text: '0x' }); + assert.ok(await attachment, 'attachment'); +} + +async function queueSignatures(driver: Driver) { + // There is a race condition which changes the order in which signatures are displayed (#25251) + // We fix it deterministically by waiting for an element in the screen for each signature + await driver.clickElement('#signTypedData'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ text: 'Hi, Alice!' }); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#signTypedDataV3'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.findElement({ text: 'Reject all' }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#signTypedDataV4'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} + +async function queueSignaturesAndTransactions(driver: Driver) { + await driver.clickElement('#signTypedData'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.delay(2000); // Delay needed due to a race condition + // To be fixed in https://github.com/MetaMask/metamask-extension/issues/25251 + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await driver.clickElement('#sendButton'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.delay(2000); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await driver.clickElement('#signTypedDataV3'); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.delay(2000); +} + +async function verifyTransaction( + driver: Driver, + expectedTransactionType: string, +) { + const transactionType = await driver.findElement( + '.confirm-page-container-summary__action__name', + ); + assert.equal(await transactionType.getText(), expectedTransactionType); +} diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts new file mode 100644 index 000000000000..a9a6e7c57b91 --- /dev/null +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -0,0 +1,137 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { WINDOW_TITLES } from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; +import { + scrollAndConfirmAndAssertConfirm, + withRedesignConfirmationFixtures, +} from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertSignatureRejectedMetrics, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; + +describe('Malicious Confirmation Signature - Bad Domain @no-mmi', function (this: Suite) { + it('displays alert for domain binding and confirms', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ driver }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SIWE_BadDomain); + + await verifyAlertIsDisplayed(driver); + + await acknowledgeAlert(driver); + + await scrollAndConfirmAndAssertConfirm(driver); + + await confirmFromAlertModal(driver); + + await assertVerifiedMessage( + driver, + '0x24e559452c37827008633f9ae50c68cdb28e33f547f795af687839b520b022e4093c38bf1dfebda875ded715f2754d458ed62a19248e5a9bd2205bd1cb66f9b51b', + ); + }, + ); + }); + + it('initiates and rejects from confirmation screen', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SIWE_BadDomain); + + await driver.clickElement( + '[data-testid="confirm-footer-cancel-button"]', + ); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const rejectionResult = await driver.waitForSelector({ + css: '#siweResult', + text: 'Error: User rejected the request.', + }); + assert.ok(rejectionResult); + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + uiCustomizations: [ + 'redesigned_confirmation', + 'sign_in_with_ethereum', + ], + location: 'confirmation', + }); + }, + ); + }); + + it('initiates and rejects from alert friction modal', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SIWE_BadDomain); + + await scrollAndConfirmAndAssertConfirm(driver); + + await driver.clickElement( + '[data-testid="confirm-alert-modal-cancel-button"]', + ); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const rejectionResult = await driver.waitForSelector({ + css: '#siweResult', + text: 'Error: User rejected the request.', + }); + assert.ok(rejectionResult); + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + uiCustomizations: [ + 'redesigned_confirmation', + 'sign_in_with_ethereum', + ], + location: 'alert_friction_modal', + }); + }, + ); + }); +}); + +async function confirmFromAlertModal(driver: Driver) { + await driver.clickElement('[data-testid="alert-modal-acknowledge-checkbox"]'); + await driver.clickElement( + '[data-testid="confirm-alert-modal-submit-button"]', + ); +} + +async function acknowledgeAlert(driver: Driver) { + await driver.clickElement('[data-testid="alert-modal-acknowledge-checkbox"]'); + await driver.clickElement('[data-testid="alert-modal-button"]'); +} + +async function verifyAlertIsDisplayed(driver: Driver) { + await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); + const alert = await driver.findElement('[data-testid="inline-alert"]'); + assert.equal(await alert.getText(), 'Alert'); + await driver.clickElement('[data-testid="inline-alert"]'); +} + +async function assertVerifiedMessage(driver: Driver, message: string) { + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const verifySigUtil = await driver.findElement('#siweResult'); + assert.equal(await verifySigUtil.getText(), message); +} diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 03941dbecc3f..45ebc2d8d4e3 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -1,70 +1,106 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import { - scrollAndConfirmAndAssertConfirm, - withRedesignConfirmationFixtures, -} from '../helpers'; +import { MockedEndpoint } from 'mockttp'; import { DAPP_HOST_ADDRESS, WINDOW_TITLES, openDapp, - switchToNotificationWindow, unlockWallet, } from '../../../helpers'; import { Ganache } from '../../../seeder/ganache'; import { Driver } from '../../../webdriver/driver'; +import { + scrollAndConfirmAndAssertConfirm, + withRedesignConfirmationFixtures, +} from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; -describe('Confirmation Signature - Permit', function (this: Suite) { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } - - it('initiates and confirms', async function () { +describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { + it('initiates and confirms and emits the correct events', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), async ({ driver, ganacheServer, - }: { - driver: Driver; - ganacheServer: Ganache; - }) => { - const addresses = await ganacheServer.getAccounts(); + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); const publicAddress = addresses?.[0] as string; - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signPermit'); - await switchToNotificationWindow(driver); + await openDappAndTriggerSignature(driver, SignatureType.Permit); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); + + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + }); + await assertVerifiedResults(driver, publicAddress); }, ); }); - it('initiates and rejects', async function () { + it('initiates and rejects and emits the correct events', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), - async ({ driver }: { driver: Driver }) => { + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { await unlockWallet(driver); await openDapp(driver); await driver.clickElement('#signPermit'); - await switchToNotificationWindow(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.clickElement( + await driver.clickElementAndWaitForWindowToClose( '[data-testid="confirm-footer-cancel-button"]', ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.waitForSelector({ - css: '#signPermitResult', - text: 'Error: User rejected the request.', + const rejectionResult = await driver.findElement('#signPermitResult'); + assert.equal( + await rejectionResult.getText(), + 'Error: User rejected the request.', + ); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + location: 'confirmation', }); - assert.ok(rejectionResult); }, ); }); diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index 175f7459d467..a6a1f43af675 100644 --- a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts +++ b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts @@ -1,44 +1,58 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import { withRedesignConfirmationFixtures } from '../helpers'; -import { - DAPP_HOST_ADDRESS, - WINDOW_TITLES, - openDapp, - switchToNotificationWindow, - unlockWallet, -} from '../../../helpers'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; import { Ganache } from '../../../seeder/ganache'; import { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; -describe('Confirmation Signature - Personal Sign', function (this: Suite) { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } - +describe('Confirmation Signature - Personal Sign @no-mmi', function (this: Suite) { it('initiates and confirms', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), async ({ driver, ganacheServer, - }: { - driver: Driver; - ganacheServer: Ganache; - }) => { - const addresses = await ganacheServer.getAccounts(); + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); const publicAddress = addresses?.[0] as string; - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#personalSign'); - await switchToNotificationWindow(driver); + await openDappAndTriggerSignature(driver, SignatureType.PersonalSign); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'personal_sign', + ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await assertInfoValues(driver); await driver.clickElement('[data-testid="confirm-footer-button"]'); await assertVerifiedPersonalMessage(driver, publicAddress); + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + }); }, ); }); @@ -46,11 +60,11 @@ describe('Confirmation Signature - Personal Sign', function (this: Suite) { it('initiates and rejects', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#personalSign'); - await switchToNotificationWindow(driver); + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.PersonalSign); await driver.clickElement( '[data-testid="confirm-footer-cancel-button"]', @@ -64,6 +78,12 @@ describe('Confirmation Signature - Personal Sign', function (this: Suite) { text: 'Error: User rejected the request.', }); assert.ok(rejectionResult); + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + location: 'confirmation', + }); }, ); }); diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index 4dfdb9851b2a..2512bdbeeccf 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -1,44 +1,63 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; +import { Ganache } from '../../../seeder/ganache'; +import { Driver } from '../../../webdriver/driver'; import { scrollAndConfirmAndAssertConfirm, withRedesignConfirmationFixtures, } from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; import { - DAPP_HOST_ADDRESS, - WINDOW_TITLES, - openDapp, - switchToNotificationWindow, - unlockWallet, -} from '../../../helpers'; -import { Ganache } from '../../../seeder/ganache'; -import { Driver } from '../../../webdriver/driver'; - -describe('Confirmation Signature - Sign Typed Data V3', function (this: Suite) { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; +describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: Suite) { it('initiates and confirms', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), async ({ driver, ganacheServer, - }: { - driver: Driver; - ganacheServer: Ganache; - }) => { - const addresses = await ganacheServer.getAccounts(); + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); const publicAddress = addresses?.[0] as string; - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedDataV3'); - await switchToNotificationWindow(driver); + await openDappAndTriggerSignature( + driver, + SignatureType.SignTypedDataV3, + ); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v3', + ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v3', + }); await assertVerifiedResults(driver, publicAddress); }, ); @@ -47,30 +66,44 @@ describe('Confirmation Signature - Sign Typed Data V3', function (this: Suite) { it('initiates and rejects', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedDataV3'); - await switchToNotificationWindow(driver); + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature( + driver, + SignatureType.SignTypedDataV3, + ); await driver.clickElement( '[data-testid="confirm-footer-cancel-button"]', ); + await driver.delay(1000); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v3', + location: 'confirmation', + }); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.waitForSelector({ - css: '#signTypedDataV3Result', - text: 'Error: User rejected the request.', - }); - assert.ok(rejectionResult); + const rejectionResult = await driver.findElement( + '#signTypedDataV3Result', + ); + assert.equal( + await rejectionResult.getText(), + 'Error: User rejected the request.', + ); }, ); }); }); async function assertInfoValues(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); const contractPetName = driver.findElement({ css: '.name__value', @@ -102,14 +135,11 @@ async function assertInfoValues(driver: Driver) { async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.waitUntilXWindowHandles(2); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.switchToWindowWithTitle('E2E Test Dapp'); await driver.clickElement('#signTypedDataV3Verify'); + await driver.delay(500); const verifyResult = await driver.findElement('#signTypedDataV3Result'); - await driver.waitForSelector({ - css: '#signTypedDataV3VerifyResult', - text: publicAddress, - }); const verifyRecoverAddress = await driver.findElement( '#signTypedDataV3VerifyResult', ); diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts index 5c5101d5e018..24ab428581a5 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts @@ -1,44 +1,65 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; +import { Ganache } from '../../../seeder/ganache'; +import { Driver } from '../../../webdriver/driver'; import { scrollAndConfirmAndAssertConfirm, withRedesignConfirmationFixtures, } from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; import { - DAPP_HOST_ADDRESS, - WINDOW_TITLES, - openDapp, - switchToNotificationWindow, - unlockWallet, -} from '../../../helpers'; -import { Ganache } from '../../../seeder/ganache'; -import { Driver } from '../../../webdriver/driver'; - -describe('Confirmation Signature - Sign Typed Data V4', function (this: Suite) { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; +describe('Confirmation Signature - Sign Typed Data V4 @no-mmi', function (this: Suite) { it('initiates and confirms', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), async ({ driver, ganacheServer, - }: { - driver: Driver; - ganacheServer: Ganache; - }) => { - const addresses = await ganacheServer.getAccounts(); + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); const publicAddress = addresses?.[0] as string; - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedDataV4'); - await switchToNotificationWindow(driver); + await openDappAndTriggerSignature( + driver, + SignatureType.SignTypedDataV4, + ); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); await assertInfoValues(driver); await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Mail', + }); + await assertVerifiedResults(driver, publicAddress); }, ); @@ -47,15 +68,27 @@ describe('Confirmation Signature - Sign Typed Data V4', function (this: Suite) { it('initiates and rejects', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedDataV4'); - await switchToNotificationWindow(driver); + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature( + driver, + SignatureType.SignTypedDataV4, + ); await driver.clickElement( '[data-testid="confirm-footer-cancel-button"]', ); + await driver.delay(1000); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Mail', + location: 'confirmation', + }); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -71,6 +104,7 @@ describe('Confirmation Signature - Sign Typed Data V4', function (this: Suite) { }); async function assertInfoValues(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); const contractPetName = driver.findElement({ css: '.name__value', diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts index 01f807397a97..ca7c2d503977 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts @@ -1,42 +1,58 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; -import { withRedesignConfirmationFixtures } from '../helpers'; -import { - DAPP_HOST_ADDRESS, - WINDOW_TITLES, - openDapp, - switchToNotificationWindow, - unlockWallet, -} from '../../../helpers'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; import { Ganache } from '../../../seeder/ganache'; import { Driver } from '../../../webdriver/driver'; +import { withRedesignConfirmationFixtures } from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; -describe('Confirmation Signature - Sign Typed Data', function (this: Suite) { - if (!process.env.ENABLE_CONFIRMATION_REDESIGN) { - return; - } - +describe('Confirmation Signature - Sign Typed Data @no-mmi', function (this: Suite) { it('initiates and confirms', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), async ({ driver, ganacheServer, - }: { - driver: Driver; - ganacheServer: Ganache; - }) => { - const addresses = await ganacheServer.getAccounts(); + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); const publicAddress = addresses?.[0] as string; - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedData'); - await switchToNotificationWindow(driver); + await openDappAndTriggerSignature(driver, SignatureType.SignTypedData); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData', + ); await assertInfoValues(driver); await driver.clickElement('[data-testid="confirm-footer-button"]'); + await driver.delay(1000); + + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData', + }); await assertVerifiedResults(driver, publicAddress); }, @@ -46,15 +62,23 @@ describe('Confirmation Signature - Sign Typed Data', function (this: Suite) { it('initiates and rejects', async function () { await withRedesignConfirmationFixtures( this.test?.fullTitle(), - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - await openDapp(driver); - await driver.clickElement('#signTypedData'); - await switchToNotificationWindow(driver); + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SignTypedData); await driver.clickElement( '[data-testid="confirm-footer-cancel-button"]', ); + await driver.delay(1000); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData', + location: 'confirmation', + }); await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); @@ -70,6 +94,7 @@ describe('Confirmation Signature - Sign Typed Data', function (this: Suite) { }); async function assertInfoValues(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); const message = driver.findElement({ text: 'Hi, Alice!' }); diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts new file mode 100644 index 000000000000..041d1bfbf23a --- /dev/null +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -0,0 +1,196 @@ +import { strict as assert } from 'assert'; +import { MockedEndpoint } from 'mockttp'; +import { + WINDOW_TITLES, + getEventPayloads, + openDapp, + unlockWallet, +} from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; + +export const WALLET_ADDRESS = '0x5CfE73b6021E818B776b421B1c4Db2474086a7e1'; +export const WALLET_ETH_BALANCE = '25'; +export enum SignatureType { + PersonalSign = '#personalSign', + Permit = '#signPermit', + SignTypedDataV3 = '#signTypedDataV3', + SignTypedDataV4 = '#signTypedDataV4', + SignTypedData = '#signTypedData', + SIWE = '#siwe', + SIWE_BadDomain = '#siweBadDomain', +} + +type AssertSignatureMetricsOptions = { + driver: Driver; + mockedEndpoints: MockedEndpoint[]; + signatureType: string; + primaryType?: string; + uiCustomizations?: string[]; + location?: string; +}; + +type SignatureEventProperty = { + account_type: 'MetaMask'; + category: 'inpage_provider'; + chain_id: '0x539'; + environment_type: 'background'; + locale: 'en'; + security_alert_response: 'NotApplicable'; + signature_type: string; + eip712_primary_type?: string; + ui_customizations?: string[]; + location?: string; +}; + +function getSignatureEventProperty( + signatureType: string, + primaryType: string, + uiCustomizations: string[], +): SignatureEventProperty { + const signatureEventProperty: SignatureEventProperty = { + account_type: 'MetaMask', + signature_type: signatureType, + category: 'inpage_provider', + chain_id: '0x539', + environment_type: 'background', + locale: 'en', + security_alert_response: 'NotApplicable', + ui_customizations: uiCustomizations, + }; + + if (primaryType !== '') { + signatureEventProperty.eip712_primary_type = primaryType; + } + + return signatureEventProperty; +} + +function assertSignatureRequestedMetrics( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + events: any[], + signatureEventProperty: SignatureEventProperty, +) { + assert.equal(events[0].event, 'Signature Requested'); + assert.deepStrictEqual( + events[0].properties, + { + ...signatureEventProperty, + security_alert_reason: 'NotApplicable', + }, + 'Signature request event details do not match', + ); +} + +export async function assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints, + signatureType, + primaryType = '', + uiCustomizations = ['redesigned_confirmation'], +}: AssertSignatureMetricsOptions) { + const events = await getEventPayloads(driver, mockedEndpoints); + const signatureEventProperty = getSignatureEventProperty( + signatureType, + primaryType, + uiCustomizations, + ); + + assertSignatureRequestedMetrics(events, signatureEventProperty); + assert.equal(events[1].event, 'Signature Approved'); + assert.deepStrictEqual( + events[1].properties, + signatureEventProperty, + 'Signature Accepted event properties do not match', + ); +} + +export async function assertSignatureRejectedMetrics({ + driver, + mockedEndpoints, + signatureType, + primaryType = '', + uiCustomizations = ['redesigned_confirmation'], + location, +}: AssertSignatureMetricsOptions) { + const events = await getEventPayloads(driver, mockedEndpoints); + const signatureEventProperty = getSignatureEventProperty( + signatureType, + primaryType, + uiCustomizations, + ); + + assertSignatureRequestedMetrics(events, signatureEventProperty); + assert.equal(events[1].event, 'Signature Rejected'); + assert.deepStrictEqual( + events[1].properties, + { + ...signatureEventProperty, + location, + }, + 'Signature Rejected event properties do not match', + ); +} + +export async function assertAccountDetailsMetrics( + driver: Driver, + mockedEndpoints: MockedEndpoint[], + type: string, +) { + const events = await getEventPayloads(driver, mockedEndpoints); + + assert.equal(events[1].event, 'Account Details Opened'); + assert.deepStrictEqual( + events[1].properties, + { + action: 'Confirm Screen', + location: 'signature_confirmation', + signature_type: type, + category: 'Confirmations', + locale: 'en', + chain_id: '0x539', + environment_type: 'notification', + }, + 'Account Details Metrics do not match', + ); +} + +export async function clickHeaderInfoBtn(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement( + 'button[data-testid="header-info__account-details-button"]', + ); +} + +export async function assertHeaderInfoBalance(driver: Driver) { + const headerBalanceEl = await driver.findElement( + '[data-testid="confirmation-account-details-modal__account-balance"]', + ); + await driver.waitForNonEmptyElement(headerBalanceEl); + assert.equal(await headerBalanceEl.getText(), `${WALLET_ETH_BALANCE}\nETH`); +} + +export async function copyAddressAndPasteWalletAddress(driver: Driver) { + await driver.clickElement('[data-testid="address-copy-button-text"]'); + await driver.delay(500); // Added delay to avoid error Element is not clickable at point (x,y) because another element obscures it, happens as soon as the mouse hovers over the close button + await driver.clickElement( + '[data-testid="confirmation-account-details-modal__close-button"]', + ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.findElement('#eip747ContractAddress'); + await driver.pasteFromClipboardIntoField('#eip747ContractAddress'); +} + +export async function assertPastedAddress(driver: Driver) { + const formFieldEl = await driver.findElement('#eip747ContractAddress'); + assert.equal(await formFieldEl.getAttribute('value'), WALLET_ADDRESS); +} + +export async function openDappAndTriggerSignature( + driver: Driver, + type: string, +) { + await unlockWallet(driver); + await openDapp(driver); + await driver.clickElement(type); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} diff --git a/test/e2e/tests/confirmations/signatures/siwe.spec.ts b/test/e2e/tests/confirmations/signatures/siwe.spec.ts new file mode 100644 index 000000000000..ebf4f0a3f4b2 --- /dev/null +++ b/test/e2e/tests/confirmations/signatures/siwe.spec.ts @@ -0,0 +1,117 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; +import { + scrollAndConfirmAndAssertConfirm, + withRedesignConfirmationFixtures, +} from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertHeaderInfoBalance, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerSignature, + SignatureType, +} from './signature-helpers'; + +describe('Confirmation Signature - SIWE @no-mmi', function (this: Suite) { + it('initiates and confirms', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SIWE); + + await clickHeaderInfoBtn(driver); + await assertHeaderInfoBalance(driver); + + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'personal_sign', + ); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await assertInfoValues(driver); + await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + + await assertVerifiedSiweMessage( + driver, + '0xef8674a92d62a1876624547bdccaef6c67014ae821de18fa910fbff56577a65830f68848585b33d1f4b9ea1c3da1c1b11553b6aabe8446717daf7cd1e38a68271c', + ); + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + uiCustomizations: [ + 'redesigned_confirmation', + 'sign_in_with_ethereum', + ], + }); + }, + ); + }); + + it('initiates and rejects', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerSignature(driver, SignatureType.SIWE); + + await driver.clickElement( + '[data-testid="confirm-footer-cancel-button"]', + ); + + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const rejectionResult = await driver.findElement('#siweResult'); + assert.equal( + await rejectionResult.getText(), + 'Error: User rejected the request.', + ); + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'personal_sign', + uiCustomizations: [ + 'redesigned_confirmation', + 'sign_in_with_ethereum', + ], + location: 'confirmation', + }); + }, + ); + }); +}); + +async function assertInfoValues(driver: Driver) { + const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); + const message = driver.findElement({ + text: 'I accept the MetaMask Terms of Service: https://community.metamask.io/tos', + }); + + assert.ok(await origin); + assert.ok(await message); +} + +async function assertVerifiedSiweMessage(driver: Driver, message: string) { + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + const verifySigUtil = await driver.findElement('#siweResult'); + assert.equal(await verifySigUtil.getText(), message); +} diff --git a/test/e2e/tests/confirmations/transactions/contract-deployment-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/contract-deployment-redesign.spec.ts new file mode 100644 index 000000000000..7f937f4be6fc --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/contract-deployment-redesign.spec.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { openDapp, unlockWallet } from '../../../helpers'; +import { + confirmDepositTransaction, + confirmRedesignedContractDeploymentTransaction, + createContractDeploymentTransaction, + createDepositTransaction, + TestSuiteArguments, +} from './shared'; + +const { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + withFixtures, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); + +describe('Confirmation Redesign Contract Deployment Component', function () { + describe('Create a deploy transaction @no-mmi', function () { + it(`Sends a contract interaction type 0 transaction (Legacy)`, async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }: TestSuiteArguments) => { + await unlockWallet(driver); + + await openDapp(driver); + + await createContractDeploymentTransaction(driver); + + await confirmRedesignedContractDeploymentTransaction(driver); + + // confirm deposits can be made to the deployed contract + await createDepositTransaction(driver); + + await confirmDepositTransaction(driver); + }, + ); + }); + + it(`Sends a contract interaction type 2 transaction (EIP1559)`, async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + title: this.test?.fullTitle(), + }, + async ({ driver }: TestSuiteArguments) => { + await unlockWallet(driver); + + await openDapp(driver); + + await createContractDeploymentTransaction(driver); + + await confirmRedesignedContractDeploymentTransaction(driver); + + // confirm deposits can be made to the deployed contract + await createDepositTransaction(driver); + + await confirmDepositTransaction(driver); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts new file mode 100644 index 000000000000..3d28afff5790 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/contract-interaction-redesign.spec.ts @@ -0,0 +1,281 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { openDapp, unlockWallet } from '../../../helpers'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; +import { + assertAdvancedGasDetails, + confirmDepositTransaction, + confirmDepositTransactionWithCustomNonce, + createDepositTransaction, + openDAppWithContract, + TestSuiteArguments, + toggleAdvancedDetails, + toggleOnHexData, +} from './shared'; + +const { hexToNumber } = require('@metamask/utils'); +const { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + WINDOW_TITLES, + withFixtures, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); +const { CHAIN_IDS } = require('../../../../../shared/constants/network'); + +describe('Confirmation Redesign Contract Interaction Component', function () { + const smartContract = SMART_CONTRACTS.PIGGYBANK; + + describe('Create a deposit transaction @no-mmi', function () { + it(`Sends a contract interaction type 0 transaction (Legacy)`, async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + await confirmDepositTransaction(driver); + }, + ); + }); + + it(`Sends a contract interaction type 2 transaction (EIP1559)`, async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + await confirmDepositTransaction(driver); + }, + ); + }); + + it(`Opens a contract interaction type 2 transaction that includes layer 1 fees breakdown on a layer 2`, async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder({ inputChainId: CHAIN_IDS.OPTIMISM }) + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .withTransactionControllerOPLayer2Transaction() + .build(), + ganacheOptions: { + ...defaultGanacheOptionsForType2Transactions, + network_id: hexToNumber(CHAIN_IDS.OPTIMISM), + chainId: hexToNumber(CHAIN_IDS.OPTIMISM), + }, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await unlockWallet(driver); + + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + + await openDapp(driver, contractAddress); + + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + + await toggleAdvancedDetails(driver); + + await assertAdvancedGasDetails(driver); + }, + ); + }); + }); + + describe('Custom nonce editing @no-mmi', function () { + it('Sends a contract interaction type 2 transaction without custom nonce editing (EIP1559)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + + await confirmDepositTransaction(driver); + }, + ); + }); + + it('Sends a contract interaction type 2 transaction with custom nonce editing (EIP1559)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + useNonceField: true, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await confirmDepositTransactionWithCustomNonce(driver, '10'); + }, + ); + }); + }); + + describe('Advanced Gas Details @no-mmi', function () { + it('Sends a contract interaction type 2 transaction (EIP1559) and checks the advanced gas details', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await toggleAdvancedDetails(driver); + await assertAdvancedGasDetails(driver); + }, + ); + }); + + it('If nonce editing is enabled, advanced details are shown', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + useNonceField: true, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createDepositTransaction(driver); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await assertAdvancedGasDetails(driver); + }, + ); + }); + + it('If hex data is enabled, advanced details are shown', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await toggleOnHexData(driver); + + await createDepositTransaction(driver); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + // re open advanced details + await toggleAdvancedDetails(driver); + + await assertAdvancedGasDetails(driver); + }, + ); + }); + }); +}); diff --git a/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts new file mode 100644 index 000000000000..b1ed24a9c171 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/erc721-approve-redesign.spec.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { veryLargeDelayMs, WINDOW_TITLES } from '../../../helpers'; +import { Driver } from '../../../webdriver/driver'; +import { scrollAndConfirmAndAssertConfirm } from '../helpers'; +import { + openDAppWithContract, + TestSuiteArguments, + toggleAdvancedDetails, +} from './shared'; + +const { + defaultGanacheOptions, + defaultGanacheOptionsForType2Transactions, + withFixtures, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); +const { SMART_CONTRACTS } = require('../../../seeder/smart-contracts'); + +describe('Confirmation Redesign ERC721 Approve Component', function () { + const smartContract = SMART_CONTRACTS.NFTS; + + describe('Submit an Approve transaction @no-mmi', function () { + it('Sends a type 0 transaction (Legacy)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createMintTransaction(driver); + await confirmMintTransaction(driver); + + await createApproveTransaction(driver); + + await assertApproveDetails(driver); + await confirmApproveTransaction(driver); + }, + ); + }); + + it('Sends a type 2 transaction (EIP1559)', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + smartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, smartContract); + + await createMintTransaction(driver); + await confirmMintTransaction(driver); + + await createApproveTransaction(driver); + await assertApproveDetails(driver); + await confirmApproveTransaction(driver); + }, + ); + }); + }); +}); + +async function createMintTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#mintButton'); +} + +export async function confirmMintTransaction(driver: Driver) { + await driver.waitUntilXWindowHandles(3); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Transaction request', + }); + + await scrollAndConfirmAndAssertConfirm(driver); +} + +async function createApproveTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#approveButton'); +} + +async function assertApproveDetails(driver: Driver) { + await driver.delay(veryLargeDelayMs); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Allowance request', + }); + + await driver.waitForSelector({ + css: 'p', + text: 'This site wants permission to withdraw your NFTs', + }); + + await toggleAdvancedDetails(driver); + + await driver.waitForSelector({ + css: 'p', + text: 'Request from', + }); + + await driver.waitForSelector({ + css: 'p', + text: 'Interacting with', + }); + + await driver.waitForSelector({ + css: 'p', + text: 'Method', + }); +} + +async function confirmApproveTransaction(driver: Driver) { + await scrollAndConfirmAndAssertConfirm(driver); + + await driver.delay(veryLargeDelayMs); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + await driver.clickElement({ text: 'Activity', tag: 'button' }); + await driver.waitForSelector( + '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', + ); +} diff --git a/test/e2e/tests/confirmations/transactions/metrics.spec.ts b/test/e2e/tests/confirmations/transactions/metrics.spec.ts new file mode 100644 index 000000000000..aed8c0ba7f20 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/metrics.spec.ts @@ -0,0 +1,253 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { strict as assert } from 'assert'; +import { MockedEndpoint, MockttpServer } from 'mockttp'; +import { + AnonymousTransactionMetaMetricsEvent, + TransactionMetaMetricsEvent, +} from '../../../../../shared/constants/transaction'; +import { Driver } from '../../../webdriver/driver'; +import { + confirmContractDeploymentTransaction, + confirmDepositTransaction, + createContractDeploymentTransaction, + createDepositTransaction, +} from './shared'; + +const { + defaultGanacheOptionsForType2Transactions, + openDapp, + unlockWallet, + WINDOW_TITLES, + withFixtures, + getEventPayloads, +} = require('../../../helpers'); +const FixtureBuilder = require('../../../fixture-builder'); + +describe('Metrics @no-mmi', function () { + it('Sends a contract interaction type 2 transaction (EIP1559) with the right properties in the metric events', async function () { + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + ganacheOptions: defaultGanacheOptionsForType2Transactions, + title: this.test?.fullTitle(), + testSpecificMock: mocks, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: { + driver: Driver; + mockedEndpoint: MockedEndpoint; + }) => { + await unlockWallet(driver); + + await openDapp(driver); + + await createContractDeploymentTransaction(driver); + await confirmContractDeploymentTransaction(driver); + + await createDepositTransaction(driver); + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await confirmDepositTransaction(driver); + + const events = await getEventPayloads(driver, mockedEndpoints); + + assert.equal(events.length, 16); + + // deployment tx -- no ui_customizations + assert.equal( + events[0].event, + AnonymousTransactionMetaMetricsEvent.added, + ); + assert.equal( + events[0].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[0].properties.transaction_advanced_view, undefined); + assert.equal(events[1].event, TransactionMetaMetricsEvent.added); + assert.equal( + events[1].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[1].properties.transaction_advanced_view, undefined); + assert.equal( + events[2].event, + AnonymousTransactionMetaMetricsEvent.submitted, + ); + assert.equal( + events[2].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[2].properties.transaction_advanced_view, undefined); + assert.equal(events[3].event, TransactionMetaMetricsEvent.submitted); + assert.equal( + events[3].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[3].properties.transaction_advanced_view, undefined); + assert.equal( + events[4].event, + AnonymousTransactionMetaMetricsEvent.approved, + ); + assert.equal( + events[4].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[4].properties.transaction_advanced_view, undefined); + assert.equal(events[5].event, TransactionMetaMetricsEvent.approved); + assert.equal( + events[5].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[5].properties.transaction_advanced_view, undefined); + assert.equal( + events[6].event, + AnonymousTransactionMetaMetricsEvent.finalized, + ); + assert.equal( + events[6].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[6].properties.transaction_advanced_view, undefined); + assert.equal(events[7].event, TransactionMetaMetricsEvent.finalized); + assert.equal( + events[7].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[7].properties.transaction_advanced_view, undefined); + + // deposit tx (contract interaction) -- ui_customizations is set + assert.equal( + events[8].event, + AnonymousTransactionMetaMetricsEvent.added, + ); + assert.equal( + events[8].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[8].properties.transaction_advanced_view, undefined); + assert.equal(events[9].event, TransactionMetaMetricsEvent.added); + assert.equal( + events[9].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[9].properties.transaction_advanced_view, undefined); + assert.equal( + events[10].event, + AnonymousTransactionMetaMetricsEvent.submitted, + ); + assert.equal( + events[10].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[10].properties.transaction_advanced_view, true); + assert.equal(events[11].event, TransactionMetaMetricsEvent.submitted); + assert.equal( + events[11].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[11].properties.transaction_advanced_view, true); + assert.equal( + events[12].event, + AnonymousTransactionMetaMetricsEvent.approved, + ); + assert.equal( + events[12].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[12].properties.transaction_advanced_view, true); + assert.equal(events[13].event, TransactionMetaMetricsEvent.approved); + assert.equal( + events[13].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[13].properties.transaction_advanced_view, true); + assert.equal( + events[14].event, + AnonymousTransactionMetaMetricsEvent.finalized, + ); + assert.equal( + events[14].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[14].properties.transaction_advanced_view, true); + assert.equal(events[15].event, TransactionMetaMetricsEvent.finalized); + assert.equal( + events[15].properties.ui_customizations[0], + 'redesigned_confirmation', + ); + assert.equal(events[15].properties.transaction_advanced_view, true); + }, + ); + }); +}); + +async function mockedTrackedEvent(mockServer: MockttpServer, event: string) { + return await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event }], + }) + .thenCallback(() => ({ statusCode: 200 })); +} + +async function mocks(server: MockttpServer) { + return [ + // deployment tx + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.added, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.added), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.submitted, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.submitted), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.approved, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.approved), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.finalized, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.finalized), + // deposit tx + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.added, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.added), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.submitted, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.submitted), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.approved, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.approved), + await mockedTrackedEvent( + server, + AnonymousTransactionMetaMetricsEvent.finalized, + ), + await mockedTrackedEvent(server, TransactionMetaMetricsEvent.finalized), + ]; +} diff --git a/test/e2e/tests/confirmations/transactions/shared.ts b/test/e2e/tests/confirmations/transactions/shared.ts new file mode 100644 index 000000000000..178168ffc220 --- /dev/null +++ b/test/e2e/tests/confirmations/transactions/shared.ts @@ -0,0 +1,240 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */ +import { MockedEndpoint } from 'mockttp'; +import { veryLargeDelayMs } from '../../../helpers'; +import { Ganache } from '../../../seeder/ganache'; +import GanacheContractAddressRegistry from '../../../seeder/ganache-contract-address-registry'; +import { Driver } from '../../../webdriver/driver'; + +const { + logInWithBalanceValidation, + openDapp, + WINDOW_TITLES, +} = require('../../../helpers'); +const { scrollAndConfirmAndAssertConfirm } = require('../helpers'); + +export type TestSuiteArguments = { + driver: Driver; + ganacheServer?: Ganache; + contractRegistry?: GanacheContractAddressRegistry; + mockedEndpoint?: MockedEndpoint | MockedEndpoint[]; +}; + +export async function openDAppWithContract( + driver: Driver, + contractRegistry: GanacheContractAddressRegistry | undefined, + smartContract: string, +) { + const contractAddress = await ( + contractRegistry as GanacheContractAddressRegistry + ).getContractAddress(smartContract); + + await logInWithBalanceValidation(driver); + + await openDapp(driver, contractAddress); +} + +export async function createContractDeploymentTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement(`#deployButton`); +} + +export async function confirmContractDeploymentTransaction(driver: Driver) { + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Deploy a contract', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await driver.delay(2000); + await driver.waitUntilXWindowHandles(2); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + await driver.clickElement({ text: 'Activity', tag: 'button' }); + await driver.waitForSelector( + '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', + ); +} + +export async function confirmRedesignedContractDeploymentTransaction( + driver: Driver, +) { + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Deploy a contract', + }); + + await driver.waitForSelector({ + css: 'p', + text: 'This site wants you to deploy a contract', + }); + + await scrollAndConfirmAndAssertConfirm(driver); + + await driver.delay(2000); + await driver.waitUntilXWindowHandles(2); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + await driver.clickElement({ text: 'Activity', tag: 'button' }); + await driver.waitForSelector( + '.transaction-list__completed-transactions .activity-list-item:nth-of-type(1)', + ); +} + +export async function createDepositTransaction(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement(`#depositButton`); +} + +export async function confirmDepositTransaction(driver: Driver) { + await driver.waitUntilXWindowHandles(3); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Transaction request', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await toggleAdvancedDetails(driver); + + await driver.waitForSelector({ + css: 'p', + text: 'Nonce', + }); + + await driver.delay(veryLargeDelayMs); + await scrollAndConfirmAndAssertConfirm(driver); +} + +export async function confirmDepositTransactionWithCustomNonce( + driver: Driver, + customNonce: string, +) { + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'h2', + text: 'Transaction request', + }); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await driver.waitForSelector({ + css: 'p', + text: 'Nonce', + }); + + await driver.clickElement('.edit-nonce-btn'); + await driver.fill('[data-testid="custom-nonce-input"]', customNonce); + await driver.clickElement({ + text: 'Save', + tag: 'button', + }); + await driver.delay(veryLargeDelayMs); + await scrollAndConfirmAndAssertConfirm(driver); + + // Confirm tx was submitted with the higher nonce + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + await driver.clickElement('[data-testid="account-overview__activity-tab"]'); + + const sendTransactionListItem = await driver.findElement( + '.transaction-list__pending-transactions .activity-list-item', + ); + await sendTransactionListItem.click(); + + await driver.waitForSelector({ + css: '.transaction-breakdown__value', + text: customNonce, + }); +} + +export async function toggleOnCustomNonce(driver: Driver) { + // switch to metamask page and open the three dots menu + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + // Open settings menu button + const accountOptionsMenuSelector = + '[data-testid="account-options-menu-button"]'; + await driver.clickElement(accountOptionsMenuSelector); + + // Click settings from dropdown menu + const globalMenuSettingsSelector = '[data-testid="global-menu-settings"]'; + await driver.waitForSelector(globalMenuSettingsSelector); + await driver.clickElement(globalMenuSettingsSelector); + + // Click Advanced tab + const advancedTabRawLocator = { + text: 'Advanced', + tag: 'div', + }; + await driver.clickElement(advancedTabRawLocator); + + // Toggle on custom toggle setting (off by default) + await driver.clickElement('.custom-nonce-toggle'); + + // Close settings + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); +} + +export async function toggleOnHexData(driver: Driver) { + // switch to metamask page and open the three dots menu + await driver.switchToWindowWithTitle(WINDOW_TITLES.ExtensionInFullScreenView); + + // Open settings menu button + const accountOptionsMenuSelector = + '[data-testid="account-options-menu-button"]'; + await driver.clickElement(accountOptionsMenuSelector); + + // Click settings from dropdown menu + const globalMenuSettingsSelector = '[data-testid="global-menu-settings"]'; + await driver.waitForSelector(globalMenuSettingsSelector); + await driver.clickElement(globalMenuSettingsSelector); + + // Click Advanced tab + const advancedTabRawLocator = { + text: 'Advanced', + tag: 'div', + }; + await driver.clickElement(advancedTabRawLocator); + + // Toggle on custom toggle setting (off by default) + await driver.clickElement('.hex-data-toggle'); + + // Close settings + await driver.clickElement( + '.settings-page__header__title-container__close-button', + ); +} + +export async function toggleAdvancedDetails(driver: Driver) { + // TODO - Scroll button not shown in Firefox if advanced details enabled too fast. + await driver.delay(1000); + + await driver.clickElement(`[data-testid="header-advanced-details-button"]`); +} + +export async function assertAdvancedGasDetails(driver: Driver) { + await driver.waitForSelector({ css: 'p', text: 'Estimated fee' }); + await driver.waitForSelector({ css: 'p', text: 'Speed' }); + await driver.waitForSelector({ css: 'p', text: 'Max fee' }); +} + +export async function assertAdvancedGasDetailsWithL2Breakdown(driver: Driver) { + await driver.waitForSelector({ css: 'p', text: 'Estimated fee' }); + await driver.waitForSelector({ css: 'p', text: 'L1 fee' }); + await driver.waitForSelector({ css: 'p', text: 'L2 fee' }); + await driver.waitForSelector({ css: 'p', text: 'Speed' }); + await driver.waitForSelector({ css: 'p', text: 'Max fee' }); +} diff --git a/test/e2e/tests/dapp-interactions/block-explorer.spec.js b/test/e2e/tests/dapp-interactions/block-explorer.spec.js index a94e0518c6d4..bde8eb0cf485 100644 --- a/test/e2e/tests/dapp-interactions/block-explorer.spec.js +++ b/test/e2e/tests/dapp-interactions/block-explorer.spec.js @@ -1,4 +1,6 @@ const { strict: assert } = require('assert'); +const { mockNetworkState } = require('../../../stub/networks'); + const { defaultGanacheOptions, withFixtures, @@ -12,11 +14,15 @@ describe('Block Explorer', function () { await withFixtures( { fixtures: new FixtureBuilder() - .withNetworkController({ - providerConfig: { - rpcPrefs: { blockExplorerUrl: 'https://etherscan.io/' }, - }, - }) + .withNetworkController( + mockNetworkState({ + chainId: '0x539', + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io/', + }), + ) .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), @@ -54,11 +60,15 @@ describe('Block Explorer', function () { { dapp: true, fixtures: new FixtureBuilder() - .withNetworkController({ - providerConfig: { - rpcPrefs: { blockExplorerUrl: 'https://etherscan.io/' }, - }, - }) + .withNetworkController( + mockNetworkState({ + chainId: '0x539', + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io/', + }), + ) .withTokensControllerERC20() .build(), ganacheOptions: defaultGanacheOptions, @@ -104,11 +114,16 @@ describe('Block Explorer', function () { await withFixtures( { fixtures: new FixtureBuilder() - .withNetworkController({ - providerConfig: { - rpcPrefs: { blockExplorerUrl: 'https://etherscan.io/' }, - }, - }) + .withNetworkController( + mockNetworkState({ + id: 'localhost-client-id', + chainId: '0x539', + nickname: 'Localhost 8545', + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io', + }), + ) .withTransactionControllerCompletedTransaction() .build(), ganacheOptions: defaultGanacheOptions, diff --git a/test/e2e/tests/dapp-interactions/signin-with-ethereum.spec.js b/test/e2e/tests/dapp-interactions/signin-with-ethereum.spec.js index 5c6e3bb4803c..b2fe384f9523 100644 --- a/test/e2e/tests/dapp-interactions/signin-with-ethereum.spec.js +++ b/test/e2e/tests/dapp-interactions/signin-with-ethereum.spec.js @@ -4,6 +4,7 @@ const { withFixtures, openDapp, DAPP_URL, + tempToggleSettingRedesignedConfirmations, unlockWallet, WINDOW_TITLES, } = require('../../helpers'); @@ -28,6 +29,7 @@ describe('Sign in with ethereum', function () { }, async ({ driver }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); // Create a signin with ethereum request in test dapp await openDapp(driver); diff --git a/test/e2e/tests/hardware-wallets/trezor.spec.js b/test/e2e/tests/hardware-wallets/trezor.spec.js new file mode 100644 index 000000000000..1333d84b5525 --- /dev/null +++ b/test/e2e/tests/hardware-wallets/trezor.spec.js @@ -0,0 +1,95 @@ +const { strict: assert } = require('assert'); +const FixtureBuilder = require('../../fixture-builder'); +const { + defaultGanacheOptions, + unlockWallet, + withFixtures, + regularDelayMs, +} = require('../../helpers'); +const { shortenAddress } = require('../../../../ui/helpers/utils/util'); +const { KNOWN_PUBLIC_KEY_ADDRESSES } = require('../../../stub/keyring-bridge'); + +/** + * Connect Trezor hardware wallet without selecting an account + * + * @param {*} driver - Selenium driver + */ +async function connectTrezor(driver) { + // Open add hardware wallet modal + await driver.clickElement('[data-testid="account-menu-icon"]'); + await driver.clickElement( + '[data-testid="multichain-account-menu-popover-action-button"]', + ); + await driver.clickElement({ text: 'Add hardware wallet' }); + // This delay is needed to mitigate an existing bug in FF + // See https://github.com/metamask/metamask-extension/issues/25851 + await driver.delay(regularDelayMs); + // Select Trezor + await driver.clickElement('[data-testid="connect-trezor-btn"]'); + await driver.clickElement({ text: 'Continue' }); +} + +describe('Trezor Hardware', function () { + it('derives the correct accounts', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await connectTrezor(driver); + + // Check that the first page of accounts is correct + for (const { address, index } of KNOWN_PUBLIC_KEY_ADDRESSES.slice( + 0, + 4, + )) { + const shortenedAddress = `${address.slice(0, 4)}...${address.slice( + -4, + )}`; + assert( + await driver.isElementPresent({ + text: shortenedAddress, + }), + `Known account ${index} not found`, + ); + } + }, + ); + }); + + it('unlocks the first account', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await connectTrezor(driver); + + // Select first account of first page and unlock + await driver.clickElement('.hw-account-list__item__checkbox'); + await driver.clickElement({ text: 'Unlock' }); + + // Check that the correct account has been added + await driver.clickElement('[data-testid="account-menu-icon"]'); + assert( + await driver.isElementPresent({ + text: 'Trezor 1', + }), + 'Trezor account not found', + ); + assert( + await driver.isElementPresent({ + text: shortenAddress(KNOWN_PUBLIC_KEY_ADDRESSES[0].address), + }), + 'Unlocked account is wrong', + ); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/dapp-viewed.spec.js b/test/e2e/tests/metrics/dapp-viewed.spec.js index f88d7b754bf5..b9b4b08ca73e 100644 --- a/test/e2e/tests/metrics/dapp-viewed.spec.js +++ b/test/e2e/tests/metrics/dapp-viewed.spec.js @@ -49,7 +49,7 @@ async function createTwoAccounts(driver) { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.findElement({ css: '[data-testid="account-menu-icon"]', text: '2nd account', diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 54e7c2b0e6aa..464dbedc51fd 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -14,6 +14,7 @@ const { logInWithBalanceValidation, withFixtures, } = require('../../helpers'); +const { PAGES } = require('../../webdriver/driver'); /** * Derive a UI state field from a background state field. @@ -55,6 +56,7 @@ const removedBackgroundFields = [ // These properties are set to undefined, causing inconsistencies between Chrome and Firefox 'AppStateController.currentPopupId', 'AppStateController.timeoutMinutes', + 'AppStateController.lastInteractedConfirmationInfo', 'PPOMController.chainStatus.0x539.lastVisited', 'PPOMController.versionInfo', ]; @@ -174,7 +176,7 @@ function getMissingProperties(complete, object) { describe('Sentry errors', function () { const migrationError = process.env.SELENIUM_BROWSER === Browser.CHROME - ? `Cannot read properties of undefined (reading 'version')` + ? `"type":"TypeError","value":"Cannot read properties of undefined (reading 'version')` : 'meta is undefined'; async function mockSentryMigratorError(mockServer) { return await mockServer @@ -240,7 +242,8 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryMigratorError, }, async ({ driver, mockedEndpoint }) => { - await driver.navigate(); + // we don't wait for the controllers to be loaded + await driver.navigate(PAGES.HOME, { waitForControllers: false }); // Wait for Sentry request await driver.delay(3000); @@ -305,23 +308,27 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryMigratorError, }, async ({ driver, mockedEndpoint }) => { - await driver.navigate(); - + // we don't wait for the controllers to be loaded + await driver.navigate(PAGES.HOME, { waitForControllers: false }); // Wait for Sentry request await driver.wait(async () => { const isPending = await mockedEndpoint.isPending(); return isPending === false; - }, 3000); + }, 8000); const [mockedRequest] = await mockedEndpoint.getSeenRequests(); const mockTextBody = (await mockedRequest.body.getText()).split('\n'); const mockJsonBody = JSON.parse(mockTextBody[2]); - const { level } = mockJsonBody; - const [{ type, value }] = mockJsonBody.exception.values; // Verify request - assert.equal(type, 'TypeError'); - assert(value.includes(migrationError)); - assert.equal(level, 'error'); + const escapedMigrationError = migrationError.replace( + /[.*+?^${}()|[\]\\]/gu, + '\\$&', + ); + const migrationErrorRegex = new RegExp(escapedMigrationError, 'u'); + assert.match( + JSON.stringify(mockJsonBody.exception), + migrationErrorRegex, + ); }, ); }); @@ -344,13 +351,14 @@ describe('Sentry errors', function () { testSpecificMock: mockSentryMigratorError, }, async ({ driver, mockedEndpoint }) => { - await driver.navigate(); + // we don't wait for the controllers to be loaded + await driver.navigate(PAGES.HOME, { waitForControllers: false }); // Wait for Sentry request await driver.wait(async () => { const isPending = await mockedEndpoint.isPending(); return isPending === false; - }, 3000); + }, 8000); const [mockedRequest] = await mockedEndpoint.getSeenRequests(); const mockTextBody = (await mockedRequest.body.getText()).split('\n'); @@ -460,7 +468,7 @@ describe('Sentry errors', function () { await driver.wait(async () => { const isPending = await mockedEndpoint.isPending(); return isPending === false; - }, 3000); + }, 8000); const [mockedRequest] = await mockedEndpoint.getSeenRequests(); const mockTextBody = (await mockedRequest.body.getText()).split('\n'); const mockJsonBody = JSON.parse(mockTextBody[2]); @@ -502,7 +510,7 @@ describe('Sentry errors', function () { await driver.wait(async () => { const isPending = await mockedEndpoint.isPending(); return isPending === false; - }, 3000); + }, 8000); const [mockedRequest] = await mockedEndpoint.getSeenRequests(); const mockTextBody = (await mockedRequest.body.getText()).split('\n'); const mockJsonBody = JSON.parse(mockTextBody[2]); @@ -658,6 +666,7 @@ describe('Sentry errors', function () { async ({ driver, ganacheServer, mockedEndpoint }) => { await logInWithBalanceValidation(driver, ganacheServer); + await driver.delay(2000); // Trigger error await driver.executeScript( 'window.stateHooks.throwTestBackgroundError()', @@ -757,6 +766,8 @@ describe('Sentry errors', function () { async ({ driver, ganacheServer, mockedEndpoint }) => { await logInWithBalanceValidation(driver, ganacheServer); + await driver.delay(2000); + // Trigger error await driver.executeScript('window.stateHooks.throwTestError()'); @@ -799,30 +810,6 @@ describe('Sentry errors', function () { }); }); - it('should have no policy gaps for UI controller state @no-mmi', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions, - title: this.test.fullTitle(), - }, - async ({ driver }) => { - await driver.navigate(); - await driver.findElement('#password'); - - const fullUiState = await driver.executeScript(() => - window.stateHooks?.getCleanAppState?.(), - ); - - const missingState = getMissingProperties( - fullUiState.metamask, - SENTRY_UI_STATE.metamask, - ); - assert.deepEqual(missingState, {}); - }, - ); - }); - it('should not have extra properties in UI state mask @no-mmi', async function () { const expectedMissingState = { currentPopupId: false, // Initialized as undefined @@ -831,6 +818,7 @@ describe('Sentry errors', function () { lastFetchedBlockNumbers: false, preferences: { autoLockTimeLimit: true, // Initialized as undefined + showConfirmationAdvancedDetails: true, }, smartTransactionsState: { fees: { @@ -852,6 +840,7 @@ describe('Sentry errors', function () { opts: true, store: true, configurationClient: true, + lastInteractedConfirmationInfo: undefined, }; await withFixtures( { @@ -871,6 +860,7 @@ describe('Sentry errors', function () { SENTRY_UI_STATE.metamask, fullUiState.metamask, ); + const unexpectedExtraMaskProperties = getMissingProperties( extraMaskProperties, expectedMissingState, diff --git a/test/e2e/tests/metrics/nft-detection-metrics.spec.js b/test/e2e/tests/metrics/nft-detection-metrics.spec.js new file mode 100644 index 000000000000..5b635a725e20 --- /dev/null +++ b/test/e2e/tests/metrics/nft-detection-metrics.spec.js @@ -0,0 +1,117 @@ +const { strict: assert } = require('assert'); +const { + defaultGanacheOptions, + withFixtures, + WALLET_PASSWORD, + onboardingBeginCreateNewWallet, + onboardingChooseMetametricsOption, + onboardingCreatePassword, + onboardingRevealAndConfirmSRP, + onboardingCompleteWalletCreation, + onboardingPinExtension, + getEventPayloads, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param {import('mockttp').Mockttp} mockServer + * @returns {Promise[]} + */ +async function mockSegment(mockServer) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Wallet Setup Selected' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Wallet Created' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'nft_autodetection_enabled' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +describe('Nft detection event @no-mmi', function () { + it('is sent when onboarding user', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .withPreferencesController({ + useTokenDetection: true, + useNftDetection: true, + }) + .build(), + defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + await driver.navigate(); + + await onboardingBeginCreateNewWallet(driver); + await onboardingChooseMetametricsOption(driver, true); + await onboardingCreatePassword(driver, WALLET_PASSWORD); + await onboardingRevealAndConfirmSRP(driver); + await onboardingCompleteWalletCreation(driver); + await onboardingPinExtension(driver); + + const events = await getEventPayloads(driver, mockedEndpoints); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + account_type: 'metamask', + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + assert.deepStrictEqual(events[1].properties, { + method: 'create', + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + is_profile_syncing_enabled: null, + is_signed_in: false, + }); + assert.deepStrictEqual(events[2].properties, { + nft_autodetection_enabled: true, + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'background', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/signature-approved.spec.js b/test/e2e/tests/metrics/signature-approved.spec.js index c5b4a2ac1165..c629955b5db0 100644 --- a/test/e2e/tests/metrics/signature-approved.spec.js +++ b/test/e2e/tests/metrics/signature-approved.spec.js @@ -3,11 +3,11 @@ const { defaultGanacheOptions, switchToNotificationWindow, withFixtures, - regularDelayMs, openDapp, unlockWallet, getEventPayloads, clickSignOnSignatureConfirmation, + tempToggleSettingRedesignedConfirmations, validateContractDetails, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -65,6 +65,7 @@ describe('Signature Approved Event @no-mmi', function () { }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); // creates a sign typed data signature request @@ -116,6 +117,7 @@ describe('Signature Approved Event @no-mmi', function () { }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); // creates a sign typed data signature request @@ -163,6 +165,7 @@ describe('Signature Approved Event @no-mmi', function () { }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); // creates a sign typed data signature request @@ -209,6 +212,7 @@ describe('Signature Approved Event @no-mmi', function () { }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); // creates a sign typed data signature request @@ -238,59 +242,4 @@ describe('Signature Approved Event @no-mmi', function () { }, ); }); - it('Successfully tracked for eth_sign', async function () { - await withFixtures( - { - dapp: true, - fixtures: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .withPreferencesController({ - disabledRpcMethodPreferences: { - eth_sign: true, - }, - }) - .withMetaMetricsController({ - metaMetricsId: 'fake-metrics-id', - participateInMetaMetrics: true, - }) - .build(), - defaultGanacheOptions, - title: this.test.fullTitle(), - testSpecificMock: mockSegment, - }, - async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await unlockWallet(driver); - await openDapp(driver); - - // creates a sign typed data signature request - await driver.clickElement('#ethSign'); - await switchToNotificationWindow(driver); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="page-container-footer-next"]'); - await driver.clickElement( - '[data-testid="signature-warning-sign-button"]', - ); - const events = await getEventPayloads(driver, mockedEndpoints); - assert.deepStrictEqual(events[0].properties, { - account_type: 'MetaMask', - signature_type: 'eth_sign', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_reason: 'NotApplicable', - security_alert_response: 'NotApplicable', - }); - assert.deepStrictEqual(events[1].properties, { - account_type: 'MetaMask', - signature_type: 'eth_sign', - category: 'inpage_provider', - locale: 'en', - chain_id: '0x539', - environment_type: 'background', - security_alert_response: 'NotApplicable', - }); - }, - ); - }); }); diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index fc8db2d5aa47..38473a52b36c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -18,8 +18,7 @@ "currentAppVersion": "string", "previousAppVersion": "", "previousMigrationVersion": 0, - "currentMigrationVersion": "number", - "showTokenAutodetectModalOnUpgrade": "object" + "currentMigrationVersion": "number" }, "AppStateController": { "connectedStatusPopoverHasBeenShown": true, @@ -38,7 +37,7 @@ "showNetworkBanner": true, "showAccountBanner": true, "trezorModel": null, - "onboardingDate": "object", + "onboardingDate": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, @@ -49,7 +48,7 @@ "snapsInstallPrivacyWarningShown": true, "surveyLinkLastClickedOrClosed": "object", "signatureSecurityAlertResponses": "object", - "switchedNetworkDetails": "object", + "switchedNetworkDetails": null, "switchedNetworkNeverShowMessage": "boolean", "currentExtensionPopupId": "number" }, @@ -59,6 +58,23 @@ "approvalFlows": "object" }, "AuthenticationController": { "isSignedIn": "boolean" }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "CronjobController": { "jobs": "object" }, "CurrencyController": { "currencyRates": { @@ -105,21 +121,8 @@ "traits": "object", "previousUserTraits": "object", "fragments": "object", - "dataCollectionForMarketing": "boolean", "segmentApiCalls": "object" }, - "MetamaskNotificationsController": { - "subscriptionAccountsSeen": "object", - "isMetamaskNotificationsFeatureSeen": "boolean", - "isMetamaskNotificationsEnabled": "boolean", - "isFeatureAnnouncementsEnabled": "boolean", - "metamaskNotificationsList": "object", - "metamaskNotificationsReadList": "object", - "isUpdatingMetamaskNotifications": "boolean", - "isFetchingMetamaskNotifications": "boolean", - "isUpdatingMetamaskNotificationsAccount": "object", - "isCheckingAccountsPresence": "boolean" - }, "MultichainBalancesController": { "balances": "object" }, "MultichainRatesController": { "fiatCurrency": "usd", @@ -129,15 +132,6 @@ "NameController": { "names": "object", "nameSources": "object" }, "NetworkController": { "selectedNetworkClientId": "string", - "providerConfig": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcPrefs": "object", - "rpcUrl": "string", - "ticker": "ETH", - "type": "rpc", - "id": "networkConfigurationId" - }, "networksMetadata": { "networkConfigurationId": { "EIPS": { "1559": false }, @@ -155,6 +149,19 @@ "ignoredNfts": "object" }, "NotificationController": { "notifications": "object" }, + "NotificationServicesController": { + "subscriptionAccountsSeen": "object", + "isMetamaskNotificationsFeatureSeen": "boolean", + "isNotificationServicesEnabled": "boolean", + "isFeatureAnnouncementsEnabled": "boolean", + "metamaskNotificationsList": "object", + "metamaskNotificationsReadList": "object", + "isUpdatingMetamaskNotifications": "boolean", + "isFetchingMetamaskNotifications": "boolean", + "isUpdatingMetamaskNotificationsAccount": "object", + "isCheckingAccountsPresence": "boolean" + }, + "NotificationServicesPushController": { "fcmToken": "string" }, "OnboardingController": { "seedPhraseBackedUp": true, "firstTimeFlowType": "import", @@ -172,9 +179,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, - "disabledRpcMethodPreferences": { "eth_sign": false }, "useMultiAccountBalanceChecker": true, - "hasDismissedOpenSeaToBlockaidBanner": false, "useSafeChainsListValidation": "boolean", "useTokenDetection": false, "useNftDetection": false, @@ -183,6 +188,9 @@ "useRequestQueue": true, "openSeaEnabled": false, "securityAlertsEnabled": "boolean", + "watchEthereumAccountEnabled": "boolean", + "bitcoinSupportEnabled": "boolean", + "bitcoinTestnetSupportEnabled": "boolean", "addSnapAccountEnabled": "boolean", "advancedGasFee": {}, "featureFlags": {}, @@ -200,7 +208,9 @@ "smartTransactionsOptInStatus": false, "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": true, - "showTokenAutodetectModal": "boolean" + "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "redesignedConfirmationsEnabled": true, + "redesignedTransactionsEnabled": "boolean" }, "ipfsGateway": "string", "isIpfsGatewayEnabled": "boolean", @@ -215,16 +225,11 @@ "useExternalServices": "boolean", "selectedAddress": "string" }, - "PushPlatformNotificationsController": { "fcmToken": "string" }, - "QueuedRequestController": { - "queuedRequestCount": 0 - }, + "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SignatureController": { - "unapprovedMsgs": "object", "unapprovedPersonalMsgs": "object", "unapprovedTypedMessages": "object", - "unapprovedMsgCount": 0, "unapprovedPersonalMsgCount": 0, "unapprovedTypedMessagesCount": 0 }, @@ -240,10 +245,11 @@ "snapStates": "object", "unencryptedSnapStates": "object" }, - "SnapInterfaceController": "object", + "SnapInsightsController": { "insights": "object" }, + "SnapInterfaceController": { "interfaces": "object" }, "SnapsRegistry": { - "database": "object", - "lastUpdated": "object", + "database": null, + "lastUpdated": null, "databaseUnavailable": "boolean" }, "SubjectMetadataController": { "subjectMetadata": "object" }, @@ -252,9 +258,9 @@ "quotes": "object", "quotesPollingLimitEnabled": false, "fetchParams": null, - "tokens": "object", - "tradeTxId": "object", - "approveTxId": "object", + "tokens": null, + "tradeTxId": null, + "approveTxId": null, "quotesLastFetched": null, "customMaxGas": "", "customGasPrice": null, @@ -264,7 +270,7 @@ "selectedAggId": null, "customApproveTxData": "string", "errorKey": "", - "topAggId": "object", + "topAggId": null, "routeState": "", "swapsFeatureIsLive": true, "saveFetchedQuotes": false, @@ -273,7 +279,8 @@ "swapsStxBatchStatusRefreshTime": 10000, "swapsStxStatusDeadline": 180, "swapsStxGetTransactionsRefreshTime": 10000, - "swapsStxMaxFeeMultiplier": 2 + "swapsStxMaxFeeMultiplier": 2, + "swapsFeatureFlags": {} } }, "TokenListController": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 5157b6c88405..524e5fad9294 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -2,6 +2,7 @@ "DNS": "object", "activeTab": "object", "appState": "object", + "bridge": "object", "confirm": "object", "confirmAlerts": "object", "confirmTransaction": "object", @@ -33,7 +34,9 @@ "smartTransactionsOptInStatus": false, "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": true, - "showTokenAutodetectModal": "boolean" + "isRedesignedConfirmationsDeveloperEnabled": "boolean", + "redesignedConfirmationsEnabled": true, + "redesignedTransactionsEnabled": "boolean" }, "firstTimeFlowType": "import", "completedOnboarding": true, @@ -49,15 +52,6 @@ "usdConversionRate": 1700 } }, - "providerConfig": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcPrefs": "object", - "rpcUrl": "string", - "ticker": "ETH", - "type": "rpc", - "id": "networkConfigurationId" - }, "connectedStatusPopoverHasBeenShown": true, "defaultHomeActiveTabName": null, "browserEnvironment": { "os": "string", "browser": "string" }, @@ -74,26 +68,24 @@ "showNetworkBanner": true, "showAccountBanner": true, "trezorModel": null, - "onboardingDate": "object", + "onboardingDate": null, "newPrivacyPolicyToastClickedOrClosed": "boolean", "newPrivacyPolicyToastShownDate": "number", "hadAdvancedGasFeesSetPriorToMigration92_3": false, "nftsDropdownState": {}, "termsOfUseLastAgreed": "number", "qrHardware": {}, - "queuedRequestCount": 0, "usedNetworks": { "0x1": true, "0x5": true, "0x539": true }, "snapsInstallPrivacyWarningShown": true, "surveyLinkLastClickedOrClosed": "object", "signatureSecurityAlertResponses": "object", - "switchedNetworkDetails": "object", + "switchedNetworkDetails": null, "switchedNetworkNeverShowMessage": "boolean", "currentExtensionPopupId": "number", "currentAppVersion": "string", "previousAppVersion": "", "previousMigrationVersion": 0, "currentMigrationVersion": "number", - "showTokenAutodetectModalOnUpgrade": "object", "balances": "object", "selectedNetworkClientId": "string", "networksMetadata": { @@ -106,9 +98,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, - "disabledRpcMethodPreferences": { "eth_sign": false }, "useMultiAccountBalanceChecker": true, - "hasDismissedOpenSeaToBlockaidBanner": false, "useSafeChainsListValidation": true, "useTokenDetection": false, "useNftDetection": false, @@ -116,6 +106,9 @@ "useRequestQueue": true, "openSeaEnabled": false, "securityAlertsEnabled": "boolean", + "watchEthereumAccountEnabled": "boolean", + "bitcoinSupportEnabled": "boolean", + "bitcoinTestnetSupportEnabled": "boolean", "addSnapAccountEnabled": "boolean", "advancedGasFee": {}, "incomingTransactionsPreferences": {}, @@ -187,11 +180,12 @@ "snapStates": "object", "unencryptedSnapStates": "object", "jobs": "object", - "database": "object", - "lastUpdated": "object", + "database": null, + "lastUpdated": null, "databaseUnavailable": "boolean", "notifications": "object", "interfaces": "object", + "insights": "object", "names": "object", "nameSources": "object", "userOperations": "object", @@ -200,7 +194,7 @@ "isProfileSyncingUpdateLoading": "boolean", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", - "isMetamaskNotificationsEnabled": "boolean", + "isNotificationServicesEnabled": "boolean", "isFeatureAnnouncementsEnabled": "boolean", "metamaskNotificationsList": "object", "metamaskNotificationsReadList": "object", @@ -208,6 +202,7 @@ "isFetchingMetamaskNotifications": "boolean", "isUpdatingMetamaskNotificationsAccount": "object", "isCheckingAccountsPresence": "boolean", + "queuedRequestCount": 0, "fcmToken": "string", "accounts": "object", "accountsByChainId": "object", @@ -216,19 +211,17 @@ "unapprovedDecryptMsgCount": 0, "unapprovedEncryptionPublicKeyMsgs": "object", "unapprovedEncryptionPublicKeyMsgCount": 0, - "unapprovedMsgs": "object", "unapprovedPersonalMsgs": "object", "unapprovedTypedMessages": "object", - "unapprovedMsgCount": 0, "unapprovedPersonalMsgCount": 0, "unapprovedTypedMessagesCount": 0, "swapsState": { "quotes": "object", "quotesPollingLimitEnabled": false, "fetchParams": null, - "tokens": "object", - "tradeTxId": "object", - "approveTxId": "object", + "tokens": null, + "tradeTxId": null, + "approveTxId": null, "quotesLastFetched": null, "customMaxGas": "", "customGasPrice": null, @@ -238,7 +231,7 @@ "selectedAggId": null, "customApproveTxData": "string", "errorKey": "", - "topAggId": "object", + "topAggId": null, "routeState": "", "swapsFeatureIsLive": true, "saveFetchedQuotes": false, @@ -247,7 +240,23 @@ "swapsStxBatchStatusRefreshTime": 10000, "swapsStxStatusDeadline": 180, "swapsStxGetTransactionsRefreshTime": 10000, - "swapsStxMaxFeeMultiplier": 2 + "swapsStxMaxFeeMultiplier": 2, + "swapsFeatureFlags": {} + }, + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 2909bf1d3597..c30e331583cd 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -2,10 +2,10 @@ "data": { "AuthenticationController": { "isSignedIn": "boolean" }, "UserStorageController": { "isProfileSyncingEnabled": true }, - "MetamaskNotificationsController": { + "NotificationServicesController": { "subscriptionAccountsSeen": "object", "isFeatureAnnouncementsEnabled": "boolean", - "isMetamaskNotificationsEnabled": "boolean", + "isNotificationServicesEnabled": "boolean", "isMetamaskNotificationsFeatureSeen": "boolean", "metamaskNotificationsList": "object", "metamaskNotificationsReadList": "object" @@ -40,14 +40,14 @@ "recoveryPhraseReminderLastShown": "number", "showTestnetMessageInDropdown": true, "trezorModel": null, + "newPrivacyPolicyToastClickedOrClosed": "boolean", + "newPrivacyPolicyToastShownDate": "number", "usedNetworks": { "0x1": true, "0xe708": true, "0x5": true, "0x539": true }, - "newPrivacyPolicyToastClickedOrClosed": "boolean", - "newPrivacyPolicyToastShownDate": "number", "snapsInstallPrivacyWarningShown": true }, "CurrencyController": { @@ -79,15 +79,7 @@ "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcPrefs": "object", - "rpcUrl": "string", - "ticker": "ETH", - "type": "rpc", - "id": "networkConfigurationId" - }, + "providerConfig": "object", "networkConfigurations": "object" }, "OnboardingController": { @@ -118,7 +110,8 @@ "smartTransactionsOptInStatus": false, "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": true, - "showTokenAutodetectModal": "boolean" + "showConfirmationAdvancedDetails": false, + "isRedesignedConfirmationsDeveloperEnabled": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -131,9 +124,7 @@ "useMultiAccountBalanceChecker": true, "useRequestQueue": true }, - "QueuedRequestController": { - "queuedRequestCount": 0 - }, + "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SmartTransactionsController": { "smartTransactionsState": { @@ -142,6 +133,23 @@ "smartTransactions": "object" } }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "SubjectMetadataController": { "subjectMetadata": "object" }, "TokensController": { "allDetectedTokens": {}, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index b41e6d6333d1..16204cb379fc 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -2,10 +2,10 @@ "data": { "AuthenticationController": { "isSignedIn": "boolean" }, "UserStorageController": { "isProfileSyncingEnabled": true }, - "MetamaskNotificationsController": { + "NotificationServicesController": { "subscriptionAccountsSeen": "object", "isFeatureAnnouncementsEnabled": "boolean", - "isMetamaskNotificationsEnabled": "boolean", + "isNotificationServicesEnabled": "boolean", "isMetamaskNotificationsFeatureSeen": "boolean", "metamaskNotificationsList": "object", "metamaskNotificationsReadList": "object" @@ -40,14 +40,14 @@ "recoveryPhraseReminderLastShown": "number", "showTestnetMessageInDropdown": true, "trezorModel": null, + "newPrivacyPolicyToastClickedOrClosed": "boolean", + "newPrivacyPolicyToastShownDate": "number", "usedNetworks": { "0x1": true, "0xe708": true, "0x5": true, "0x539": true }, - "newPrivacyPolicyToastClickedOrClosed": "boolean", - "newPrivacyPolicyToastShownDate": "number", "snapsInstallPrivacyWarningShown": true }, "CurrencyController": { @@ -79,15 +79,7 @@ "networksMetadata": { "networkConfigurationId": { "EIPS": {}, "status": "available" } }, - "providerConfig": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcPrefs": "object", - "rpcUrl": "string", - "ticker": "ETH", - "type": "rpc", - "id": "networkConfigurationId" - }, + "providerConfig": "object", "networkConfigurations": "object" }, "OnboardingController": { @@ -118,7 +110,8 @@ "smartTransactionsOptInStatus": false, "useNativeCurrencyAsPrimaryCurrency": true, "petnamesEnabled": true, - "showTokenAutodetectModal": "boolean" + "showConfirmationAdvancedDetails": false, + "isRedesignedConfirmationsDeveloperEnabled": "boolean" }, "selectedAddress": "string", "theme": "light", @@ -131,9 +124,7 @@ "useMultiAccountBalanceChecker": true, "useRequestQueue": true }, - "QueuedRequestController": { - "queuedRequestCount": 0 - }, + "QueuedRequestController": { "queuedRequestCount": 0 }, "SelectedNetworkController": { "domains": "object" }, "SmartTransactionsController": { "smartTransactionsState": { @@ -151,6 +142,23 @@ "ignoredTokens": "object", "tokens": "object" }, + "BridgeController": { + "bridgeState": { + "bridgeFeatureFlags": { + "extensionSupport": "boolean", + "srcNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + }, + "destNetworkAllowlist": { + "0": "string", + "1": "string", + "2": "string" + } + } + } + }, "TransactionController": { "transactions": "object" }, "config": "object", "firstTimeInfo": "object" diff --git a/test/e2e/tests/metrics/token-detection-metrics.spec.js b/test/e2e/tests/metrics/token-detection-metrics.spec.js new file mode 100644 index 000000000000..5115343f5a39 --- /dev/null +++ b/test/e2e/tests/metrics/token-detection-metrics.spec.js @@ -0,0 +1,114 @@ +const { strict: assert } = require('assert'); +const { + defaultGanacheOptions, + withFixtures, + WALLET_PASSWORD, + onboardingBeginCreateNewWallet, + onboardingChooseMetametricsOption, + onboardingCreatePassword, + onboardingRevealAndConfirmSRP, + onboardingCompleteWalletCreation, + onboardingPinExtension, + getEventPayloads, +} = require('../../helpers'); +const FixtureBuilder = require('../../fixture-builder'); + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param {import('mockttp').Mockttp} mockServer + * @returns {Promise[]} + */ +async function mockSegment(mockServer) { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Wallet Setup Selected' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'Wallet Created' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [{ type: 'track', event: 'token_detection_enabled' }], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + ]; +} + +describe('Token detection event @no-mmi', function () { + it('is sent when onboarding user', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder({ onboarding: true }) + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .withPreferencesController({ useTokenDetection: true }) + .build(), + defaultGanacheOptions, + title: this.test.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver, mockedEndpoint: mockedEndpoints }) => { + await driver.navigate(); + + await onboardingBeginCreateNewWallet(driver); + await onboardingChooseMetametricsOption(driver, true); + await onboardingCreatePassword(driver, WALLET_PASSWORD); + await onboardingRevealAndConfirmSRP(driver); + await onboardingCompleteWalletCreation(driver); + await onboardingPinExtension(driver); + + const events = await getEventPayloads(driver, mockedEndpoints); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + account_type: 'metamask', + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + assert.deepStrictEqual(events[1].properties, { + method: 'create', + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + is_profile_syncing_enabled: null, + is_signed_in: false, + }); + assert.deepStrictEqual(events[2].properties, { + token_detection_enabled: true, + category: 'Onboarding', + locale: 'en', + chain_id: '0x539', + environment_type: 'background', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/multichain/connection-page.spec.js b/test/e2e/tests/multichain/connection-page.spec.js index 2d1d2f2d106c..122a83e718fa 100644 --- a/test/e2e/tests/multichain/connection-page.spec.js +++ b/test/e2e/tests/multichain/connection-page.spec.js @@ -127,7 +127,7 @@ describe('Connections page', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', accountLabel2); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( '[data-testid="multichain-account-menu-popover-action-button"]', @@ -136,7 +136,7 @@ describe('Connections page', function () { '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 3"]', accountLabel3); - await driver.clickElement({ text: 'Create', tag: 'button' }); + await driver.clickElement({ text: 'Add account', tag: 'button' }); await locateAccountBalanceDOM(driver); await driver.clickElement( '[data-testid ="account-options-menu-button"]', diff --git a/test/e2e/tests/network/add-custom-network.spec.js b/test/e2e/tests/network/add-custom-network.spec.js index fd133fa47e47..df87c915a742 100644 --- a/test/e2e/tests/network/add-custom-network.spec.js +++ b/test/e2e/tests/network/add-custom-network.spec.js @@ -1,10 +1,12 @@ const { strict: assert } = require('assert'); const { toHex } = require('@metamask/controller-utils'); +const { mockNetworkState } = require('../../../stub/networks'); const FixtureBuilder = require('../../fixture-builder'); const { defaultGanacheOptions, withFixtures, openDapp, + openMenuSafe, regularDelayMs, unlockWallet, WINDOW_TITLES, @@ -66,7 +68,7 @@ const MOCK_CHAINLIST_RESPONSE = [ const selectors = { accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', informationSymbol: '[data-testid="info-tooltip"]', - settingsOption: { text: 'Settings', tag: 'div' }, + settingsOption: '[data-testid="global-menu-settings"]', networkOption: { text: 'Networks', tag: 'div' }, addNetwork: { text: 'Add a network', tag: 'button' }, addNetworkManually: { text: 'Add a network manually', tag: 'h6' }, @@ -79,11 +81,9 @@ const selectors = { saveButton: { text: 'Save', tag: 'button' }, updatedNetworkDropDown: { tag: 'span', text: 'Update Network' }, errorMessageInvalidUrl: { - tag: 'h6', text: 'URLs require the appropriate HTTP/HTTPS prefix.', }, warningSymbol: { - tag: 'h6', text: 'URLs require the appropriate HTTP/HTTPS prefix.', }, suggestedTicker: '[data-testid="network-form-ticker-suggestion"]', @@ -112,7 +112,8 @@ const selectors = { }; async function navigateToAddNetwork(driver) { - await driver.clickElement(selectors.accountOptionsMenuButton); + await openMenuSafe(driver); + await driver.clickElement(selectors.settingsOption); await driver.clickElement(selectors.networkOption); await driver.clickElement(selectors.addNetwork); @@ -586,15 +587,13 @@ describe('Custom network', function () { { fixtures: new FixtureBuilder() .withNetworkController({ - networkConfigurations: { - networkConfigurationId: { - rpcUrl: networkURL, - chainId: chainID, - nickname: networkNAME, - ticker: currencySYMBOL, - rpcPrefs: {}, - }, - }, + ...mockNetworkState({ + rpcUrl: networkURL, + chainId: chainID, + nickname: networkNAME, + ticker: currencySYMBOL, + }), + selectedNetworkClientId: 'mainnet', }) .build(), ganacheOptions: defaultGanacheOptions, @@ -603,10 +602,9 @@ describe('Custom network', function () { async ({ driver }) => { await unlockWallet(driver); - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - await driver.clickElement({ text: 'Settings', tag: 'div' }); + await openMenuSafe(driver); + + await driver.clickElement('[data-testid="global-menu-settings"]'); await driver.clickElement({ text: 'Networks', tag: 'div' }); const arbitrumNetwork = await driver.clickElement({ @@ -753,8 +751,7 @@ describe('Custom network', function () { assert.equal(suggestedTicker, false); assert.equal(tickerWarning, false); - driver.clickElement(selectors.tickerButton); - driver.clickElement(selectors.saveButton); + await driver.clickElement(selectors.saveButton); // Validate the network was added const networkAdded = await driver.isElementPresent( @@ -802,6 +799,8 @@ describe('Custom network', function () { ); await driver.fill(selectors.chainIdInputField, '1'); await driver.fill(selectors.tickerInputField, 'TST'); + // fix flaky test + await driver.delay(regularDelayMs); await driver.fill(selectors.explorerInputField, 'https://test.com'); const suggestedTicker = await driver.isElementPresent( @@ -849,6 +848,9 @@ describe('Custom network', function () { inputData.networkName, ); await driver.fill(selectors.rpcUrlInputField, inputData.rpcUrl); + + // fix flaky test + await driver.delay(regularDelayMs); await driver.fill(selectors.chainIdInputField, inputData.chainId); await driver.fill(selectors.tickerInputField, inputData.ticker); @@ -863,8 +865,8 @@ describe('Custom network', function () { assert.equal(suggestedTicker, true); assert.equal(tickerWarning, true); - driver.clickElement(selectors.tickerButton); - driver.clickElement(selectors.saveButton); + await driver.clickElement(selectors.tickerButton); + await driver.clickElement(selectors.saveButton); // Validate the network was added const networkAdded = await driver.isElementPresent( @@ -881,7 +883,7 @@ async function checkThatSafeChainsListValidationToggleIsOn(driver) { const accountOptionsMenuSelector = '[data-testid="account-options-menu-button"]'; await driver.waitForSelector(accountOptionsMenuSelector); - await driver.clickElement(accountOptionsMenuSelector); + await openMenuSafe(driver); const globalMenuSettingsSelector = '[data-testid="global-menu-settings"]'; await driver.waitForSelector(globalMenuSettingsSelector); @@ -945,7 +947,6 @@ async function failCandidateNetworkValidation(driver) { const chainIdValidationMessageRawLocator = { text: 'Could not fetch chain ID. Is your RPC URL correct?', - tag: 'h6', }; await driver.waitForSelector(chainIdValidationMessageRawLocator); await driver.waitForSelector('[data-testid="network-form-ticker-warning"]'); @@ -1011,7 +1012,7 @@ async function toggleOffSafeChainsListValidation(driver) { 'Safe chains list validation toggle is ON', ); - driver.delay(regularDelayMs); + await driver.delay(regularDelayMs); // return to the home screen const appHeaderSelector = '[data-testid="app-header-logo"]'; @@ -1047,6 +1048,8 @@ async function candidateNetworkIsNotValidated(driver) { await driver.fill('[data-testid="network-form-ticker-input"]', 'cTH'); await blockExplorerURLInputEl.fill('https://block-explorer.url'); + // fix flaky test + await driver.delay(regularDelayMs); const saveButtonRawLocator = { text: 'Save', tag: 'button', diff --git a/test/e2e/tests/network/chain-interactions.spec.js b/test/e2e/tests/network/chain-interactions.spec.js index c03387c93155..ba774ffecdb1 100644 --- a/test/e2e/tests/network/chain-interactions.spec.js +++ b/test/e2e/tests/network/chain-interactions.spec.js @@ -3,8 +3,8 @@ const { generateGanacheOptions, withFixtures, openDapp, - unlockWallet, WINDOW_TITLES, + logInWithBalanceValidation, } = require('../../helpers'); const FixtureBuilder = require('../../fixture-builder'); @@ -23,18 +23,13 @@ describe('Chain Interactions', function () { title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // trigger add chain confirmation await openDapp(driver); await driver.clickElement('#addEthereumChain'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // verify chain details const [networkName, networkUrl, chainIdElement] = @@ -47,9 +42,9 @@ describe('Chain Interactions', function () { await driver.clickElement({ text: 'Approve', tag: 'button' }); await driver.clickElement({ text: 'Cancel', tag: 'button' }); - // switch to extension - await driver.waitUntilXWindowHandles(3); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // verify networks await driver.findElement({ @@ -76,26 +71,22 @@ describe('Chain Interactions', function () { title: this.test.fullTitle(), }, async ({ driver }) => { - await unlockWallet(driver); + await logInWithBalanceValidation(driver); // trigger add chain confirmation await openDapp(driver); await driver.clickElement('#addEthereumChain'); - await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - await driver.switchToWindowWithTitle( - WINDOW_TITLES.Dialog, - windowHandles, - ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // approve and switch chain await driver.clickElement({ text: 'Approve', tag: 'button' }); await driver.clickElement({ text: 'Switch network', tag: 'button' }); // switch to extension - await driver.waitUntilXWindowHandles(2); - await driver.switchToWindow(extension); + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); // verify current network await driver.findElement({ diff --git a/test/e2e/tests/network/custom-rpc-history.spec.js b/test/e2e/tests/network/custom-rpc-history.spec.js index 7df8746a2e62..b8bda1a2f14f 100644 --- a/test/e2e/tests/network/custom-rpc-history.spec.js +++ b/test/e2e/tests/network/custom-rpc-history.spec.js @@ -1,4 +1,6 @@ const { strict: assert } = require('assert'); +const { mockNetworkState } = require('../../../stub/networks'); + const { defaultGanacheOptions, generateGanacheOptions, @@ -102,7 +104,6 @@ describe('Custom RPC history', function () { await rpcUrlInput.sendKeys(duplicateRpcUrl); await driver.findElement({ text: 'This URL is currently used by the mainnet network.', - tag: 'h6', }); }, ); @@ -144,7 +145,6 @@ describe('Custom RPC history', function () { await chainIdInput.sendKeys(duplicateChainId); await driver.findElement({ text: 'This Chain ID is currently used by the mainnet network.', - tag: 'h6', }); await rpcUrlInput.clear(); @@ -160,7 +160,6 @@ describe('Custom RPC history', function () { await driver.findElement({ text: 'Could not fetch chain ID. Is your RPC URL correct?', - tag: 'h6', }); }, ); @@ -185,29 +184,26 @@ describe('Custom RPC history', function () { }); it('finds all recent RPCs in history', async function () { + const networkState = mockNetworkState( + { + rpcUrl: 'http://127.0.0.1:8545/1', + chainId: '0x539', + ticker: 'ETH', + nickname: 'http://127.0.0.1:8545/1', + }, + { + rpcUrl: 'http://127.0.0.1:8545/2', + chainId: '0x539', + ticker: 'ETH', + nickname: 'http://127.0.0.1:8545/2', + }, + ); + delete networkState.selectedNetworkClientId; + await withFixtures( { fixtures: new FixtureBuilder() - .withNetworkController({ - networkConfigurations: { - networkConfigurationIdOne: { - rpcUrl: 'http://127.0.0.1:8545/1', - chainId: '0x539', - ticker: 'ETH', - nickname: 'http://127.0.0.1:8545/1', - rpcPrefs: {}, - type: 'rpc', - }, - networkConfigurationIdTwo: { - rpcUrl: 'http://127.0.0.1:8545/2', - chainId: '0x539', - ticker: 'ETH', - nickname: 'http://127.0.0.1:8545/2', - rpcPrefs: {}, - type: 'rpc', - }, - }, - }) + .withNetworkController(networkState) .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), @@ -237,27 +233,26 @@ describe('Custom RPC history', function () { }); it('deletes a custom RPC', async function () { + const networkState = mockNetworkState( + { + rpcUrl: 'http://127.0.0.1:8545/1', + chainId: '0x539', + ticker: 'ETH', + nickname: 'http://127.0.0.1:8545/1', + }, + { + rpcUrl: 'http://127.0.0.1:8545/2', + chainId: '0x539', + ticker: 'ETH', + nickname: 'http://127.0.0.1:8545/2', + }, + ); + delete networkState.selectedNetworkClientId; + await withFixtures( { fixtures: new FixtureBuilder() - .withNetworkController({ - networkConfigurations: { - networkConfigurationIdOne: { - rpcUrl: 'http://127.0.0.1:8545/1', - chainId: '0x539', - ticker: 'ETH', - nickname: 'http://127.0.0.1:8545/1', - rpcPrefs: {}, - }, - networkConfigurationIdTwo: { - rpcUrl: 'http://127.0.0.1:8545/2', - chainId: '0x539', - ticker: 'ETH', - nickname: 'http://127.0.0.1:8545/2', - rpcPrefs: {}, - }, - }, - }) + .withNetworkController(networkState) .build(), ganacheOptions: defaultGanacheOptions, title: this.test.fullTitle(), @@ -294,7 +289,9 @@ describe('Custom RPC history', function () { value: customNetworkName, }); // delete custom network in a modal - await driver.clickElement('.networks-tab__network-form .btn-danger'); + await driver.clickElement( + '.networks-tab__network-form-footer .btn-danger', + ); await driver.findVisibleElement( '[data-testid="confirm-delete-network-modal"]', ); diff --git a/test/e2e/tests/network/update-network.spec.ts b/test/e2e/tests/network/update-network.spec.ts index 1c09b88a621d..4242dfe1533f 100644 --- a/test/e2e/tests/network/update-network.spec.ts +++ b/test/e2e/tests/network/update-network.spec.ts @@ -22,7 +22,6 @@ const selectors = { saveButton: { text: 'Save', tag: 'button' }, updatedNetworkDropDown: { tag: 'span', text: 'Update Network' }, errorMessageInvalidUrl: { - tag: 'h6', text: 'URLs require the appropriate HTTP/HTTPS prefix.', }, networkNameInputField: '[data-testid="network-form-network-name"]', diff --git a/test/e2e/tests/notifications/mocks.ts b/test/e2e/tests/notifications/mocks.ts index cb14382f8763..7a2c3fdfef92 100644 --- a/test/e2e/tests/notifications/mocks.ts +++ b/test/e2e/tests/notifications/mocks.ts @@ -1,26 +1,17 @@ import { Mockttp, RequestRuleBuilder } from 'mockttp'; import { - getMockAuthNonceResponse, - getMockAuthLoginResponse, - getMockAuthAccessTokenResponse, -} from '../../../../app/scripts/controllers/authentication/mocks/mockResponses'; + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; import { - getMockUserStorageGetResponse, - getMockUserStoragePutResponse, -} from '../../../../app/scripts/controllers/user-storage/mocks/mockResponses'; -import { - getMockFeatureAnnouncementResponse, - getMockBatchCreateTriggersResponse, - getMockBatchDeleteTriggersResponse, - getMockListNotificationsResponse, - getMockMarkNotificationsAsReadResponse, -} from '../../../../app/scripts/controllers/metamask-notifications/mocks/mockResponses'; -import { - getMockRetrievePushNotificationLinksResponse, - getMockUpdatePushNotificationLinksResponse, - getMockCreateFCMRegistrationTokenResponse, - getMockDeleteFCMRegistrationTokenResponse, -} from '../../../../app/scripts/controllers/push-platform-notifications/mocks/mockResponse'; + NotificationServicesController, + NotificationsServicesPushController, +} from '@metamask/notification-services-controller'; + +const AuthMocks = AuthenticationController.Mocks; +const StorageMocks = UserStorageController.Mocks; +const NotificationMocks = NotificationServicesController.Mocks; +const PushMocks = NotificationsServicesPushController.Mocks; type MockResponse = { url: string | RegExp; @@ -35,26 +26,29 @@ type MockResponse = { */ export function mockNotificationServices(server: Mockttp) { // Auth - mockAPICall(server, getMockAuthNonceResponse()); - mockAPICall(server, getMockAuthLoginResponse()); - mockAPICall(server, getMockAuthAccessTokenResponse()); + mockAPICall(server, AuthMocks.getMockAuthNonceResponse()); + mockAPICall(server, AuthMocks.getMockAuthLoginResponse()); + mockAPICall(server, AuthMocks.getMockAuthAccessTokenResponse()); // Storage - mockAPICall(server, getMockUserStorageGetResponse()); - mockAPICall(server, getMockUserStoragePutResponse()); + mockAPICall(server, StorageMocks.getMockUserStorageGetResponse()); + mockAPICall(server, StorageMocks.getMockUserStoragePutResponse()); // Notifications - mockAPICall(server, getMockFeatureAnnouncementResponse()); - mockAPICall(server, getMockBatchCreateTriggersResponse()); - mockAPICall(server, getMockBatchDeleteTriggersResponse()); - mockAPICall(server, getMockListNotificationsResponse()); - mockAPICall(server, getMockMarkNotificationsAsReadResponse()); + mockAPICall(server, NotificationMocks.getMockFeatureAnnouncementResponse()); + mockAPICall(server, NotificationMocks.getMockBatchCreateTriggersResponse()); + mockAPICall(server, NotificationMocks.getMockBatchDeleteTriggersResponse()); + mockAPICall(server, NotificationMocks.getMockListNotificationsResponse()); + mockAPICall( + server, + NotificationMocks.getMockMarkNotificationsAsReadResponse(), + ); // Push Notifications - mockAPICall(server, getMockRetrievePushNotificationLinksResponse()); - mockAPICall(server, getMockUpdatePushNotificationLinksResponse()); - mockAPICall(server, getMockCreateFCMRegistrationTokenResponse()); - mockAPICall(server, getMockDeleteFCMRegistrationTokenResponse()); + mockAPICall(server, PushMocks.getMockRetrievePushNotificationLinksResponse()); + mockAPICall(server, PushMocks.getMockUpdatePushNotificationLinksResponse()); + mockAPICall(server, PushMocks.getMockCreateFCMRegistrationTokenResponse()); + mockAPICall(server, PushMocks.getMockDeleteFCMRegistrationTokenResponse()); } function mockAPICall(server: Mockttp, response: MockResponse) { diff --git a/test/e2e/tests/petnames/petnames-signatures.spec.js b/test/e2e/tests/petnames/petnames-signatures.spec.js index ecc972ca9f6b..ba6cf7642c59 100644 --- a/test/e2e/tests/petnames/petnames-signatures.spec.js +++ b/test/e2e/tests/petnames/petnames-signatures.spec.js @@ -2,6 +2,7 @@ const { openDapp, switchToNotificationWindow, withFixtures, + tempToggleSettingRedesignedConfirmations, unlockWallet, defaultGanacheOptions, } = require('../../helpers'); @@ -108,6 +109,7 @@ describe('Petnames - Signatures', function () { }, async ({ driver }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); await createSignatureRequest(driver, SIGNATURE_TYPE.TYPED_V3); await switchToNotificationWindow(driver, 3); @@ -144,6 +146,7 @@ describe('Petnames - Signatures', function () { }, async ({ driver }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); await createSignatureRequest(driver, SIGNATURE_TYPE.TYPED_V4); await switchToNotificationWindow(driver, 3); @@ -185,6 +188,7 @@ describe('Petnames - Signatures', function () { }, async ({ driver }) => { await unlockWallet(driver); + await tempToggleSettingRedesignedConfirmations(driver); await openDapp(driver); await openTestSnaps(driver); await installNameLookupSnap(driver); diff --git a/test/e2e/tests/phishing-controller/helpers.js b/test/e2e/tests/phishing-controller/helpers.js index a00d5ddb398f..bc0703af760d 100644 --- a/test/e2e/tests/phishing-controller/helpers.js +++ b/test/e2e/tests/phishing-controller/helpers.js @@ -1,6 +1,7 @@ const { METAMASK_STALELIST_URL, METAMASK_HOTLIST_DIFF_URL, + C2_DOMAIN_BLOCKLIST_URL, ListNames, } = require('@metamask/phishing-controller'); @@ -10,16 +11,15 @@ const { * @enum {BlockProvider} * @readonly * @property {string} MetaMask - The name of the MetaMask block provider. - * @property {string} PhishFort - The name of the PhishFort block provider. */ const BlockProvider = { MetaMask: 'metamask', - PhishFort: 'phishfort', }; module.exports = { METAMASK_HOTLIST_DIFF_URL, METAMASK_STALELIST_URL, + C2_DOMAIN_BLOCKLIST_URL, BlockProvider, ListNames, }; diff --git a/test/e2e/tests/phishing-controller/mock-page-with-iframe-but-disable-early-detection/index.html b/test/e2e/tests/phishing-controller/mock-page-with-iframe-but-disable-early-detection/index.html new file mode 100644 index 000000000000..2f0cf22b3dc5 --- /dev/null +++ b/test/e2e/tests/phishing-controller/mock-page-with-iframe-but-disable-early-detection/index.html @@ -0,0 +1,16 @@ + + + + Mock E2E Phishing Page + + +
Hello
+ +