diff --git a/.circleci/config.yml b/.circleci/config.yml index 3097e060ff6f..47095e4e3bd1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,6 +67,18 @@ aliases: git checkout -B "$CIRCLE_BRANCH" "$CIRCLE_SHA1" fi + # Check if MMI Optional tests should run + - &check-mmi-optional + name: Check if MMI Optional tests should run + command: | + RUN_MMI_OPTIONAL=$(cat ./RUN_MMI_OPTIONAL) + if [[ "${CIRCLE_BRANCH}" == "develop" || "${RUN_MMI_OPTIONAL}" == "true" ]]; then + echo "Running MMI Optional tests" + else + echo "Skipping MMI Optional tests" + circleci step halt + fi + workflows: test_and_release: jobs: @@ -77,6 +89,7 @@ workflows: - trigger-beta-build: requires: - prep-deps + - check-pr-tag - prep-deps - test-deps-audit: requires: @@ -120,6 +133,9 @@ workflows: - prep-build-multichain-test: requires: - prep-deps + - prep-build-confirmation-redesign-test: + requires: + - prep-deps - prep-build-test-mv3: requires: - prep-deps @@ -132,6 +148,7 @@ workflows: - prep-build-test-mmi-playwright: requires: - prep-deps + - check-pr-tag - prep-build-storybook: requires: - prep-deps @@ -154,9 +171,15 @@ workflows: - test-e2e-chrome-multichain: requires: - prep-build-multichain-test + - test-e2e-chrome-confirmation-redesign: + requires: + - prep-build-confirmation-redesign-test - test-e2e-firefox: requires: - prep-build-test + - test-e2e-firefox-confirmation-redesign: + requires: + - prep-build-confirmation-redesign-test - test-e2e-chrome-rpc: requires: - prep-build-test @@ -269,6 +292,8 @@ workflows: - test-e2e-chrome - test-e2e-chrome-multichain - 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 @@ -373,6 +398,37 @@ jobs: name: Create GitHub Pull Request for version command: .circleci/scripts/release-create-release-pr.sh + check-pr-tag: + docker: + - image: cimg/base:stable + steps: + - run: + name: Check for MMI Team Tag + command: | + #!/bin/bash + + GH_LABEL=team-mmi + if [ -z "$CIRCLE_PULL_REQUESTS" ]; then + exit 0 + fi + + echo $CIRCLE_PULL_REQUESTS | sed 's/,/\n/g' + + # See if any associated PRs have matching label + HAS_MATCHING_PR=$(echo $CIRCLE_PULL_REQUESTS \ + | sed -e 's#,#\n#g' -e 's#/github.com/#/api.github.com/repos/#g' -e 's#/pull/#/pulls/#g' \ + | xargs -n1 curl -s \ + | jq -s "map((.labels|map(select(.name==\"${GH_LABEL}\"))))|flatten|length > 0") + + echo "${GH_LABEL} tag presence: ${HAS_MATCHING_PR}" + + # assign the RUN_MMI_OPTIONAL variable + echo "${HAS_MATCHING_PR}" > ./RUN_MMI_OPTIONAL + - persist_to_workspace: + root: . + paths: + - RUN_MMI_OPTIONAL + prep-deps: executor: node-browsers-medium steps: @@ -640,6 +696,7 @@ jobs: - run: *shallow-git-clone - attach_workspace: at: . + - run: *check-mmi-optional - run: name: Build MMI extension for Playwright e2e command: | @@ -654,6 +711,7 @@ jobs: - persist_to_workspace: root: . paths: + - RUN_MMI_OPTIONAL - dist-test-mmi-playwright - builds-test-mmi-playwright - store_artifacts: @@ -725,6 +783,27 @@ jobs: - dist-test-multichain - builds-test-multichain + prep-build-confirmation-redesign-test: + executor: node-browsers-medium-plus + steps: + - run: *shallow-git-clone + - attach_workspace: + at: . + - run: + name: Build extension for testing + command: ENABLE_CONFIRMATION_REDESIGN=1 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-storybook: executor: node-browsers-medium-plus steps: @@ -875,7 +954,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome --retries 2 --debug + timeout 20m yarn test:e2e:chrome --retries 2 fi no_output_timeout: 5m - store_artifacts: @@ -902,7 +981,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome --retries 2 --debug + timeout 20m yarn test:e2e:chrome --retries 2 fi no_output_timeout: 5m environment: @@ -913,6 +992,35 @@ jobs: - 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 + - 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 --retries 2 + fi + no_output_timeout: 5m + environment: + ENABLE_CONFIRMATION_REDESIGN: 1 + - store_artifacts: + path: test-artifacts + destination: test-artifacts + - store_test_results: + path: test/test-results/e2e + test-e2e-chrome-mv3: executor: node-browsers-medium-plus parallelism: 16 @@ -931,7 +1039,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome --retries 2 --debug || echo "Temporarily suppressing MV3 e2e test failures" + timeout 20m yarn test:e2e:chrome --retries 2 || echo "Temporarily suppressing MV3 e2e test failures" fi no_output_timeout: 5m - store_artifacts: @@ -1009,7 +1117,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome:rpc --retries 2 --debug --build-type=mmi + timeout 20m yarn test:e2e:chrome:rpc --retries 2 --build-type=mmi fi no_output_timeout: 5m - store_artifacts: @@ -1029,7 +1137,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - yarn test:e2e:single test/e2e/vault-decryption-chrome.spec.js --browser chrome --retries 2 --debug + yarn test:e2e:single test/e2e/vault-decryption-chrome.spec.js --browser chrome --retries 2 fi no_output_timeout: 5m @@ -1051,7 +1159,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:firefox:flask --retries 2 --debug + timeout 20m yarn test:e2e:firefox:flask --retries 2 fi no_output_timeout: 5m - store_artifacts: @@ -1078,7 +1186,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome:flask --retries 2 --debug + timeout 20m yarn test:e2e:chrome:flask --retries 2 fi no_output_timeout: 5m - store_artifacts: @@ -1105,7 +1213,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:chrome:mmi --retries 2 --debug --build-type=mmi + timeout 20m yarn test:e2e:chrome:mmi --retries 2 --build-type=mmi fi no_output_timeout: 5m - store_artifacts: @@ -1121,6 +1229,7 @@ jobs: - run: *shallow-git-clone - attach_workspace: at: . + - run: *check-mmi-optional - run: name: Move test build to dist command: mv ./dist-test-mmi-playwright ./dist @@ -1172,7 +1281,7 @@ jobs: command: | if .circleci/scripts/test-run-e2e.sh then - timeout 20m yarn test:e2e:firefox --retries 2 --debug + timeout 20m yarn test:e2e:firefox --retries 2 fi no_output_timeout: 5m - store_artifacts: @@ -1181,6 +1290,36 @@ jobs: - store_test_results: path: test/test-results/e2e + test-e2e-firefox-confirmation-redesign: + executor: node-browsers-medium-plus + parallelism: 20 + steps: + - run: *shallow-git-clone + - 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:firefox-confirmation-redesign + command: | + if .circleci/scripts/test-run-e2e.sh + then + timeout 20m yarn test:e2e:firefox --retries 2 + fi + no_output_timeout: 5m + environment: + ENABLE_CONFIRMATION_REDESIGN: 1 + - store_artifacts: + path: test-artifacts + destination: test-artifacts + - store_test_results: + path: test/test-results/e2e + + benchmark: executor: node-browsers-small steps: @@ -1265,7 +1404,7 @@ jobs: - test-artifacts job-publish-prerelease: - executor: node-browsers-small + executor: node-browsers-medium steps: - checkout - attach_workspace: @@ -1305,13 +1444,9 @@ jobs: path: test-artifacts destination: test-artifacts # important: generate lavamoat viz AFTER uploading builds as artifacts - # Temporarily disabled until we can update to a version of `sesify` with - # this fix included: https://github.com/LavaMoat/LavaMoat/pull/121 - # Disabled 2024-03-25 due to flakiness. - # - see: https://github.com/MetaMask/metamask-extension/issues/23704 - #- run: - # name: build:lavamoat-viz - # command: ./.circleci/scripts/create-lavamoat-viz.sh + - run: + name: build:lavamoat-viz + command: ./.circleci/scripts/create-lavamoat-viz.sh - store_artifacts: path: build-artifacts destination: build-artifacts @@ -1571,4 +1706,4 @@ jobs: steps: - run: name: All Tests Passed - command: echo 'weew - everything passed!' + command: echo 'whew - everything passed!' diff --git a/.circleci/scripts/create-cherry-pick-pr.sh b/.circleci/scripts/create-cherry-pick-pr.sh new file mode 100644 index 000000000000..da401345ac79 --- /dev/null +++ b/.circleci/scripts/create-cherry-pick-pr.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +# Takes in 3 args +# - 1 - Base PR Branch Name +# - 2 - Commit Hash +# - 3 - PR Number + +BASE_PR_BRANCH_NAME="${1}" +COMMIT_HASH_TO_CHERRY_PICK="${2}" +PR_BRANCH_NAME="chore/cherry-pick-${3}" +PR_TITLE="chore: cherry-pick #${3}" +PR_BODY="This PR cherry-picks #${3}" + +git config user.name "MetaMask Bot" +git config user.email "metamaskbot@users.noreply.github.com" + +git checkout "${BASE_PR_BRANCH_NAME}" +git pull +git checkout -b "${PR_BRANCH_NAME}" +git cherry-pick "${COMMIT_HASH_TO_CHERRY_PICK}" + +git push --set-upstream origin "${PR_BRANCH_NAME}" + +gh pr create \ + --draft \ + --title "${PR_TITLE}" \ + --body "${PR_BODY}" \ + --head "${BASE_PR_BRANCH_NAME}" \ No newline at end of file diff --git a/.circleci/scripts/create-lavamoat-viz.sh b/.circleci/scripts/create-lavamoat-viz.sh index 4f2df2cdb1ce..135f8a461263 100755 --- a/.circleci/scripts/create-lavamoat-viz.sh +++ b/.circleci/scripts/create-lavamoat-viz.sh @@ -10,8 +10,33 @@ BUILD_DEST="./build-artifacts/build-viz/" # prepare artifacts dir mkdir -p "${BUILD_DEST}" -# generate lavamoat debug config +# generate lavamoat debug configs yarn lavamoat:debug:build +yarn lavamoat:debug:webapp --parallel=false +# generate entries for all present policy dirs under lavamoat/browserify +# static entry for build-system +POLICY_DIR_NAMES=$(find lavamoat/browserify -maxdepth 1 -mindepth 1 -type d -printf '%f ') + +POLICY_FILE_PATHS_JSON=$(echo -n "${POLICY_DIR_NAMES}" \ + | jq --raw-input --slurp --indent 0 ' + rtrimstr(" ") + | split(" ") + | map({ + "key": ., + "value": { + "debug": ("lavamoat/browserify/"+.+"/policy-debug.json"), + "override":"lavamoat/browserify/policy-override.json", + "primary":("lavamoat/browserify/"+.+"/policy.json") + } + }) + | from_entries + |."build-system"= { + "debug": "lavamoat/build-system/policy-debug.json", + "override":"lavamoat/build-system/policy-override.json", + "primary": "lavamoat/build-system/policy.json" + }' +) # generate viz -npx lavamoat-viz --dest "${BUILD_DEST}" +# shellcheck disable=SC2086 +yarn lavamoat-viz --dest "${BUILD_DEST}" --policyNames build-system ${POLICY_DIR_NAMES} --policyFilePathsJson "${POLICY_FILE_PATHS_JSON}" diff --git a/.eslintrc.js b/.eslintrc.js index d57cc7bf7aef..a2207e6ba560 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,10 @@ module.exports = { ignorePatterns: readFileSync('.prettierignore', 'utf8').trim().split('\n'), // eslint's parser, esprima, is not compatible with ESM, so use the babel parser instead parser: '@babel/eslint-parser', + plugins: ['@metamask/design-tokens'], + rules: { + '@metamask/design-tokens/color-no-hex': 'warn', + }, overrides: [ /** * == Modules == @@ -135,6 +139,7 @@ module.exports = { path.resolve(__dirname, '.eslintrc.typescript-compat.js'), ], rules: { + '@typescript-eslint/no-explicit-any': 'error', // this rule is new, but we didn't use it before, so it's off now '@typescript-eslint/no-duplicate-enum-values': 'off', '@typescript-eslint/no-shadow': [ @@ -438,5 +443,18 @@ module.exports = { ], }, }, + /** + * Don't check for static hex values in .test, .spec or .stories files + */ + { + files: [ + '**/*.test.{js,ts,tsx}', + '**/*.spec.{js,ts,tsx}', + '**/*.stories.{js,ts,tsx}', + ], + rules: { + '@metamask/design-tokens/color-no-hex': 'off', + }, + }, ], }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 00c3678ffb6b..8eee07734755 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,3 +51,12 @@ privacy-snapshot.json @MetaMask/extension-privacy-reviewers # For now, restricting approvals inside the .devcontainer folder to devs # who were involved with the Codespaces project. .devcontainer/ @MetaMask/library-admins @HowardBraham @plasmacorral @brad-decker + +# Confirmations UX team to own code for confirmations on UI. +ui/pages/confirmations @MetaMask/confirmations-ux @MetaMask/confirmations-system-team + +# MMI team is responsible for code related with Institutioanl version of MetaMask +ui/pages/institutional @MetaMask/mmi +ui/components/institutional @MetaMask/mmi +ui/ducks/institutional @MetaMask/mmi +ui/selectors/institutional @MetaMask/mmi \ No newline at end of file diff --git a/.github/guidelines/LABELING_GUIDELINES.md b/.github/guidelines/LABELING_GUIDELINES.md index 054cec4aff24..dbdd3b2ed9b8 100644 --- a/.github/guidelines/LABELING_GUIDELINES.md +++ b/.github/guidelines/LABELING_GUIDELINES.md @@ -12,7 +12,8 @@ It's essential to ensure that PRs have the appropriate labels before they are co - **release-x.y.z**: This label is automatically added to a PR and its linked issues upon the PR's merge. The `x.y.z` in the label represents the version in which the changes from the PR will be included. This label is auto-generated by a [GitHub action](../workflows/add-release-label.yml), which determines the version by incrementing the minor version number from the most recent release. Manual intervention is only required in specific cases. For instance, if a merged PR is cherry-picked into a release branch, typically done to address Release Candidate (RC) bugs, the label would need to be manually updated to reflect the correct version. - **regression-prod-x.y.z**: This label is automatically added to a bug report issue at the time of its creation. The `x.y.z` in the label represents the version in which the bug first appeared. This label is auto-generated by a [GitHub action](../workflows/check-template-and-add-labels.yml), which determines the `x.y.z` value based on the version information provided in the bug report issue form. Manual intervention is only necessary under certain circumstances. For example, if a user submits a bug report and specifies the version they are currently using, but the bug was actually introduced in a prior version, the label would need to be manually updated to accurately reflect the version where the bug originated. -### Optional QA labels: +### Optional labels: +- **regression-develop**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on development branch (i.e. `develop`), but is not yet released in production. - **needs-qa**: If the PR includes a new features, complex testing steps, or large refactors, this label must be added to indicated PR requires a full manual QA prior being merged and added to a release. ### Labels prohibited when PR needs to be merged: diff --git a/.github/scripts/check-pr-has-required-labels.ts b/.github/scripts/check-pr-has-required-labels.ts index 15ef77022ceb..354dc2c2aa7d 100644 --- a/.github/scripts/check-pr-has-required-labels.ts +++ b/.github/scripts/check-pr-has-required-labels.ts @@ -73,7 +73,7 @@ async function main(): Promise { if (!hasTeamLabel) { errorMessage += 'No team labels found on the PR. '; } - errorMessage += `Please make sure the PR is appropriately labeled before merging it.\n\nSee labeling guidelines for more detail: https://github.com/MetaMask/metamask-extension/blob/develop/.github/LABELING_GUIDELINES.md`; + errorMessage += `Please make sure the PR is appropriately labeled before merging it.\n\nSee labeling guidelines for more detail: https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md`; core.setFailed(errorMessage); process.exit(1); } diff --git a/.github/workflows/add-mmi-reviewer-and-notify.yml b/.github/workflows/add-mmi-reviewer-and-notify.yml new file mode 100644 index 000000000000..8821ccbd36e9 --- /dev/null +++ b/.github/workflows/add-mmi-reviewer-and-notify.yml @@ -0,0 +1,48 @@ +name: Notify MMI team via Slack + +on: + pull_request_target: + branches: + - develop + types: + - opened + - reopened + - synchronize + - labeled + +jobs: + process-label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + steps: + - name: Notify MMI team via Slack + if: contains(github.event.pull_request.labels.*.name, 'team-mmi') + uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844 + with: + status: custom + fields: repo,message,commit,author,action + payload: | + { + "text": "A PR with label 'team-mmi' was added and requires review: ${{ github.event.pull_request.html_url }} in ${{ github.repository }}", + "attachments": [ + { + "color": "#2eb886", + "fields": [ + { + "title": "Repository", + "value": "${{ github.repository }}", + "short": true + }, + { + "title": "PR", + "value": "#${{ github.event.pull_request.number }}", + "short": true + } + ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.MMI_LABEL_SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/add-release-label.yml b/.github/workflows/add-release-label.yml index 3acc4e22af10..1c66daa4eb72 100644 --- a/.github/workflows/add-release-label.yml +++ b/.github/workflows/add-release-label.yml @@ -13,12 +13,12 @@ jobs: if: github.event.pull_request.merged == true steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # This is needed to checkout all branches - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: yarn diff --git a/.github/workflows/check-pr-labels.yml b/.github/workflows/check-pr-labels.yml index cfd48e22c8b6..4d36c48650a7 100644 --- a/.github/workflows/check-pr-labels.yml +++ b/.github/workflows/check-pr-labels.yml @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 # This retrieves only the latest commit. - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: yarn diff --git a/.github/workflows/check-template-and-add-labels.yml b/.github/workflows/check-template-and-add-labels.yml index e5311b70d22e..85391473b72e 100644 --- a/.github/workflows/check-template-and-add-labels.yml +++ b/.github/workflows/check-template-and-add-labels.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 # This retrieves only the latest commit. - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: yarn diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 0f7977000c63..c6fc7e658eea 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -14,7 +14,7 @@ jobs: contents: write steps: - name: "CLA Signature Bot" - uses: MetaMask/cla-signature-bot@v3.0.2 + uses: MetaMask/cla-signature-bot@v4.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/close-bug-report.yml b/.github/workflows/close-bug-report.yml index 7d520c30fafc..977700e1ca5d 100644 --- a/.github/workflows/close-bug-report.yml +++ b/.github/workflows/close-bug-report.yml @@ -13,12 +13,12 @@ jobs: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'Version-v') steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 1 # This retrieves only the latest commit. - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: yarn diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e5c1eb06b588..73670c46d75d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/create-cherry-pick-pr.yml b/.github/workflows/create-cherry-pick-pr.yml new file mode 100644 index 000000000000..7773a3e42b0f --- /dev/null +++ b/.github/workflows/create-cherry-pick-pr.yml @@ -0,0 +1,31 @@ +name: Cherry Pick Commit + +on: + workflow_dispatch: + inputs: + branch_name: + description: 'Branch name you want the cherry-pick branch to be based from' + required: true + commit_hash: + description: 'Commit Hash' + required: true + PR_number: + description: 'PR # Associated with Cherry Pick' + required: true + + +jobs: + cherry-pick: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Create Cherry Pick PR + id: create-cherry-pick-pr + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ./scripts/create-cherry-pick-pr.sh ${{ github.event.inputs.branch_name }} ${{ github.event.inputs.commit_hash }} ${{ github.event.inputs.PR_number }} \ No newline at end of file diff --git a/.github/workflows/crowdin-action.yml b/.github/workflows/crowdin-action.yml index a77592321684..94bd8016cd4f 100644 --- a/.github/workflows/crowdin-action.yml +++ b/.github/workflows/crowdin-action.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: crowdin action uses: crowdin/github-action@a3160b9e5a9e00739392c23da5e580c6cabe526d diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d28f7886f3a..a1c5cc29a575 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: name: Check workflows runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - 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 diff --git a/.github/workflows/security-code-scanner.yml b/.github/workflows/security-code-scanner.yml index 6b75b0d98bd0..7da1773d666c 100644 --- a/.github/workflows/security-code-scanner.yml +++ b/.github/workflows/security-code-scanner.yml @@ -32,5 +32,5 @@ jobs: node_modules rules_excluded: example - mixpanel_project_token: ${{secrets.SECURITY_CODE_SCANNER_MIXPANEL_TOKEN}} + project_metrics_token: ${{secrets.SECURITY_SCAN_METRICS_TOKEN}} slack_webhook: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index afe3fe367504..61d549728d99 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -4,17 +4,27 @@ on: secrets: SONAR_TOKEN: required: true +# pull_request: +# branches: +# - develop +# types: +# - opened +# - reopened +# - synchronize +# - labeled +# - unlabeled + jobs: sonarcloud: name: SonarCloud runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: SonarCloud Scan - # v1.9.1 - uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c + # This is SonarSource/sonarcloud-github-action@v2.0.0 + uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 with: args: > -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info diff --git a/.github/workflows/stale-issues-pr.yml b/.github/workflows/stale-issues-pr.yml index 42da445b1376..5ad068a64fbb 100644 --- a/.github/workflows/stale-issues-pr.yml +++ b/.github/workflows/stale-issues-pr.yml @@ -12,7 +12,8 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@72afbce2b0dbd1d903bb142cebe2d15dc307ae57 + # this is a hash for actions/stale@v9.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e with: stale-issue-label: 'stale' only-issue-labels: 'type-bug' diff --git a/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml index 593b3692c57c..fa0740b72930 100644 --- a/.github/workflows/update-lavamoat-policies.yml +++ b/.github/workflows/update-lavamoat-policies.yml @@ -12,7 +12,7 @@ jobs: outputs: IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Determine whether this PR is from a fork id: is-fork run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" @@ -28,7 +28,7 @@ jobs: if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: React to the comment run: | gh api \ @@ -52,14 +52,14 @@ jobs: COMMIT_SHA: ${{ steps.commit-sha.outputs.COMMIT_SHA }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout pull request run: gh pr checkout "${PR_NUMBER}" env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -76,14 +76,14 @@ jobs: - prepare steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout pull request run: gh pr checkout "${PR_NUMBER}" env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -110,14 +110,14 @@ jobs: - update-lavamoat-build-policy steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout pull request run: gh pr checkout "${PR_NUMBER}" env: GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} PR_NUMBER: ${{ github.event.issue.number }} - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' cache: 'yarn' @@ -151,7 +151,7 @@ jobs: if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Use PAT to ensure that the commit later can trigger status check workflows token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} @@ -251,7 +251,7 @@ jobs: - is-fork-pull-request - check-status steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} - name: Post comment if the update failed diff --git a/.github/workflows/validate-conventional-commits.yml b/.github/workflows/validate-conventional-commits.yml index aaed4ee1e6ec..8cb416844339 100644 --- a/.github/workflows/validate-conventional-commits.yml +++ b/.github/workflows/validate-conventional-commits.yml @@ -3,13 +3,13 @@ on: pull_request: branches: - develop - types: [opened, edited, reopened] + types: [opened, edited, reopened, synchronize] jobs: pr-title-linter: runs-on: ubuntu-latest steps: - # this is a hash for amannn/action-semantic-pull-request@v5.2.0 - - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 + # this is a hash for amannn/action-semantic-pull-request@v5.4.0 + - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 642526e93a27..5949ba633724 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ test/e2e/mmi/playwright.config.ts test/e2e/mmi/specs/**/*-darwin.png test/e2e/mmi/dist/ +lavamoat/**/policy-debug.json diff --git a/.metamaskrc.dist b/.metamaskrc.dist index d2663f882a52..e45a7740afc3 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -21,3 +21,9 @@ BLOCKAID_PUBLIC_KEY= ; SELENIUM_HEADLESS= ; Set this to 1 to make chrome e2e tests disable DoH/DoT and use system DNS ; SELENIUM_USE_SYSTEM_DNS= + + +ENABLE_CONFIRMATION_REDESIGN= + +; Enables the Settings Page - Developer Options +; ENABLE_SETTINGS_PAGE_DEV_OPTIONS=true diff --git a/.storybook/main.js b/.storybook/main.js index 67b47fd72c04..2a48b3b88654 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -64,6 +64,7 @@ module.exports = { { loader: 'css-loader', options: { + esModule: false, import: false, url: false, }, @@ -83,6 +84,15 @@ module.exports = { config.plugins.push( new CopyWebpackPlugin({ patterns: [ + { + from: path.join( + 'ui', + 'css', + 'utilities', + 'fonts/', + ), + to: 'fonts', + }, { from: path.join( 'node_modules', diff --git a/.vscode/cspell.json b/.vscode/cspell.json new file mode 100644 index 000000000000..24e3747bfa9b --- /dev/null +++ b/.vscode/cspell.json @@ -0,0 +1,84 @@ +{ + "ignorePaths": ["app/images", "package.json"], + "ignoreWords": [ + "acitores", + "autofetch", + "azuretools", + "Brainstem", + "C01LUJL3T98", + "C05QXJA7NP8", + "cids", + "eamodio", + "initialisation", + "koalaman", + "mockttp", + "multibase", + "multicodec", + "namelookup", + "pluggable", + "protobufjs", + "regadas", + "remotedev", + "rvest", + "sesify", + "siginsights", + "testrpc", + "txinsights", + "webextension", + "xvfb" + ], + "useGitignore": true, + "version": "0.2", + "words": [ + "bignumber", + "blockaid", + "browserlistrc", + "cimg", + "codecov", + "codespace", + "codespaces", + "corepack", + "datetime", + "datetimes", + "dedupe", + "depcheck", + "devcontainer", + "devcontainers", + "endregion", + "ensdomains", + "flamegraph", + "FONTCONFIG", + "hardfork", + "hexstring", + "jazzicon", + "keccak", + "lavadome", + "lavamoat", + "lavapack", + "lockdown", + "metamaskbot", + "metamaskrc", + "metametrics", + "mocharc", + "MULTICHAIN", + "MULTIPROVIDER", + "npmcli", + "onboarded", + "pageload", + "petnames", + "pipefail", + "quickstart", + "recompiles", + "shellcheck", + "sourcemaps", + "sprintf", + "testcase", + "TESTFILES", + "testid", + "tsbuildinfo", + "tsconfigs", + "typecheck", + "yargs", + "yarnpkg" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 49dda3b55d2d..e801fb65f8b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,7 +38,7 @@ "type": "pickString", "id": "browserToUse", "description": "Which browser do you want to test with?", - "options": ["chrome", "firefox"], + "options": ["chrome", "firefox", "all"], "default": "chrome" } ], diff --git a/.vscode/settings.json b/.vscode/settings.json index 5eca75a9f539..02c20a82219a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "cSpell.words": ["blockaid", "lavamoat"], "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", "editor.tabSize": 2, "files.associations": { diff --git a/.yarn/patches/@metamask-assets-controllers-patch-0f46262fea.patch b/.yarn/patches/@metamask-assets-controllers-patch-0f46262fea.patch new file mode 100644 index 000000000000..2c0f9d1abbbc --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-patch-0f46262fea.patch @@ -0,0 +1,40 @@ +diff --git a/dist/NftDetectionController.js b/dist/NftDetectionController.js +index 24373e328d3600d1168914a3dc0bbbd905b19ebe..3877bebee24d1ad5cd2183b50547e8cef1846558 100644 +--- a/dist/NftDetectionController.js ++++ b/dist/NftDetectionController.js +@@ -36,7 +36,7 @@ class NftDetectionController extends polling_controller_1.StaticIntervalPollingC + * @param config - Initial options used to configure this controller. + * @param state - Initial state to set on this controller. + */ +- constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, }, config, state) { ++ constructor({ chainId: initialChainId, getNetworkClientById, onPreferencesStateChange, onNetworkStateChange, getOpenSeaApiKey, addNft, getNftApi, getNftState, disabled: initialDisabled, selectedAddress: initialSelectedAddress }, config, state) { + super(config, state); + /** + * Name of this controller used during composition +@@ -54,8 +54,8 @@ class NftDetectionController extends polling_controller_1.StaticIntervalPollingC + this.defaultConfig = { + interval: DEFAULT_INTERVAL, + chainId: initialChainId, +- selectedAddress: '', +- disabled: true, ++ selectedAddress: initialSelectedAddress, ++ disabled: initialDisabled, + }; + this.initialize(); + this.getNftState = getNftState; +diff --git a/dist/Standards/NftStandards/ERC721/ERC721Standard.js b/dist/Standards/NftStandards/ERC721/ERC721Standard.js +index d9286b0c0e607d2857f3ee7dad40d13a6c11d7d7..4e12e4b590b1f34a66602d63035f1905917f8c93 100644 +--- a/dist/Standards/NftStandards/ERC721/ERC721Standard.js ++++ b/dist/Standards/NftStandards/ERC721/ERC721Standard.js +@@ -66,7 +66,10 @@ class ERC721Standard { + const contract = new contracts_1.Contract(address, metamask_eth_abis_1.abiERC721, this.provider); + const supportsMetadata = yield this.contractSupportsMetadataInterface(address); + if (!supportsMetadata) { +- throw new Error('Contract does not support ERC721 metadata interface.'); ++ // Do not throw error here, supporting Metadata interface is optional even though majority of ERC721 nfts do support it. ++ // This change is made because of instances of NFTs that are ERC404( mixed ERC20 / ERC721 implementation). ++ // As of today, ERC404 is unofficial but some people use it, the contract does not support Metadata interface, but it has the tokenURI() fct. ++ console.error('Contract does not support ERC721 metadata interface.'); + } + return contract.tokenURI(tokenId); + }); diff --git a/.yarn/patches/@metamask-assets-controllers-patch-7616cc1669.patch b/.yarn/patches/@metamask-assets-controllers-patch-7616cc1669.patch new file mode 100644 index 000000000000..6231d02d46f1 --- /dev/null +++ b/.yarn/patches/@metamask-assets-controllers-patch-7616cc1669.patch @@ -0,0 +1,57 @@ +diff --git a/dist/AssetsContractController.js b/dist/AssetsContractController.js +index e8bae0bc514db69398fc2c378ca42eeb8f135e60..33c1e894f0318a59ec43f1b0a51500118c49bef6 100644 +--- a/dist/AssetsContractController.js ++++ b/dist/AssetsContractController.js +@@ -40,6 +40,13 @@ exports.SINGLE_CALL_BALANCES_ADDRESS_BY_CHAINID = { + [assetsUtil_1.SupportedTokenDetectionNetworks.optimism]: '0xB1c568e9C3E6bdaf755A60c7418C269eb11524FC', + [assetsUtil_1.SupportedTokenDetectionNetworks.base]: '0x6AA75276052D96696134252587894ef5FFA520af', + [assetsUtil_1.SupportedTokenDetectionNetworks.zksync]: '0x458fEd3144680a5b8bcfaa0F9594aa19B4Ea2D34', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.cronos]: '0x768ca200f0fc702ac9ea502498c18f5eff176378', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.celo]: '0x6aa75276052d96696134252587894ef5ffa520af', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.gnosis]: '0x6aa75276052d96696134252587894ef5ffa520af', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.fantom]: '0x6aa75276052d96696134252587894ef5ffa520af', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.polygon_zkevm]: '0x6aa75276052d96696134252587894ef5ffa520af', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.moonbeam]: '0x6aa75276052d96696134252587894ef5ffa520af', ++ [assetsUtil_1.SupportedTokenDetectionNetworks.moonriver]: '0x6aa75276052d96696134252587894ef5ffa520af', + }; + exports.MISSING_PROVIDER_ERROR = 'AssetsContractController failed to set the provider correctly. A provider must be set for this method to be available'; + /** +diff --git a/dist/assetsUtil.d.ts b/dist/assetsUtil.d.ts +index 85c85697971f08cd74cbb0fd9b305ee844a0519e..5d90c63f0cf8ab5d694467afc003c7e83ed8e64b 100644 +--- a/dist/assetsUtil.d.ts ++++ b/dist/assetsUtil.d.ts +@@ -53,7 +53,15 @@ export declare enum SupportedTokenDetectionNetworks { + arbitrum = "0xa4b1", + optimism = "0xa", + base = "0x2105", +- zksync = "0x144" ++ zksync = "0x144", ++ zksync = "0x144", ++ cronos = "0x19", ++ celo = "0xa4ec", ++ gnosis = "0x64", ++ fantom = "0xfa", ++ polygon_zkevm = "0x44d", ++ moonbeam = "0x504", ++ moonriver = "0x505" + } + /** + * Check if token detection is enabled for certain networks. +diff --git a/dist/assetsUtil.js b/dist/assetsUtil.js +index 27689263f0af453ac35c6f3240cc11e6b307458e..8d7098eff7080a4846c6b8c55650df102a6d8f2f 100644 +--- a/dist/assetsUtil.js ++++ b/dist/assetsUtil.js +@@ -119,6 +119,13 @@ var SupportedTokenDetectionNetworks; + SupportedTokenDetectionNetworks["optimism"] = "0xa"; + SupportedTokenDetectionNetworks["base"] = "0x2105"; + SupportedTokenDetectionNetworks["zksync"] = "0x144"; ++ SupportedTokenDetectionNetworks["cronos"] = "0x19"; ++ SupportedTokenDetectionNetworks["celo"] = "0xa4ec"; ++ SupportedTokenDetectionNetworks["gnosis"] = "0x64"; ++ SupportedTokenDetectionNetworks["fantom"] = "0xfa"; ++ SupportedTokenDetectionNetworks["polygon_zkevm"] = "0x44d"; ++ SupportedTokenDetectionNetworks["moonbeam"] = "0x504"; ++ SupportedTokenDetectionNetworks["moonriver"] = "0x505"; + })(SupportedTokenDetectionNetworks = exports.SupportedTokenDetectionNetworks || (exports.SupportedTokenDetectionNetworks = {})); + /** + * Check if token detection is enabled for certain networks. diff --git a/.yarn/patches/@metamask-controller-utils-patch-a87ddc3d4b.patch b/.yarn/patches/@metamask-controller-utils-patch-a87ddc3d4b.patch new file mode 100644 index 000000000000..4d2e10d38361 --- /dev/null +++ b/.yarn/patches/@metamask-controller-utils-patch-a87ddc3d4b.patch @@ -0,0 +1,12 @@ +diff --git a/dist/types.js b/dist/types.js +index c59368ae1b156162acec2aacb6d593c5122e9b09..012bb5197bbeaa5738b8144a540d3db8aa8cb85c 100644 +--- a/dist/types.js ++++ b/dist/types.js +@@ -9,6 +9,7 @@ exports.InfuraNetworkType = { + goerli: 'goerli', + sepolia: 'sepolia', + 'linea-goerli': 'linea-goerli', ++ "linea-sepolia": "linea-sepolia", + 'linea-mainnet': 'linea-mainnet', + }; + /** diff --git a/.yarn/patches/@metamask-gas-fee-controller-npm-14.0.1-59e9d16a4e.patch b/.yarn/patches/@metamask-gas-fee-controller-npm-14.0.1-59e9d16a4e.patch deleted file mode 100644 index fa91530b9cce..000000000000 --- a/.yarn/patches/@metamask-gas-fee-controller-npm-14.0.1-59e9d16a4e.patch +++ /dev/null @@ -1,1665 +0,0 @@ -diff --git a/PATCH.txt b/PATCH.txt -new file mode 100644 -index 0000000000000000000000000000000000000000..990650b52e8bad8a30a9ae51d5a9e0fd5704cd8c ---- /dev/null -+++ b/PATCH.txt -@@ -0,0 +1,7 @@ -+============= PATCH NOTES ================ -+This patch is to ensure `state.gasFeeEstimates` always reflects the gas estiomtes -+for the globally selected chain. -+ -+Core PR: https://github.com/MetaMask/core/pull/4214/files -+ -+Patch available on core branch `extension-gas-fee-controller-14.0.1-patch` -diff --git a/dist/GasFeeController.js b/dist/GasFeeController.js -index 96938dadea567b0e37e3fcf1ca957724429ff044..ae29d8b953c95871ea3ea0ec95aeaeed283ffd0e 100644 ---- a/dist/GasFeeController.js -+++ b/dist/GasFeeController.js -@@ -3,7 +3,7 @@ - - - --var _chunkN5BANBTWjs = require('./chunk-N5BANBTW.js'); -+var _chunkIBADKXI6js = require('./chunk-IBADKXI6.js'); - require('./chunk-EZVGDV5H.js'); - require('./chunk-5INBFZXY.js'); - require('./chunk-F46NZXRQ.js'); -@@ -16,5 +16,5 @@ require('./chunk-Z4BLTVTB.js'); - - - --exports.GAS_ESTIMATE_TYPES = _chunkN5BANBTWjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkN5BANBTWjs.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkN5BANBTWjs.LEGACY_GAS_PRICES_API_URL; exports.default = _chunkN5BANBTWjs.GasFeeController_default; -+exports.GAS_ESTIMATE_TYPES = _chunkIBADKXI6js.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkIBADKXI6js.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkIBADKXI6js.LEGACY_GAS_PRICES_API_URL; exports.default = _chunkIBADKXI6js.GasFeeController_default; - //# sourceMappingURL=GasFeeController.js.map -\ No newline at end of file -diff --git a/dist/GasFeeController.mjs b/dist/GasFeeController.mjs -index 3e9cd997b462360cf221132b947fd38575579c64..061377459d0ddd218bded9ba2ce3318419a522cd 100644 ---- a/dist/GasFeeController.mjs -+++ b/dist/GasFeeController.mjs -@@ -3,7 +3,7 @@ import { - GasFeeController, - GasFeeController_default, - LEGACY_GAS_PRICES_API_URL --} from "./chunk-4T54ULFA.mjs"; -+} from "./chunk-L45HVISM.mjs"; - import "./chunk-EXCWMMNV.mjs"; - import "./chunk-AQN4AQEF.mjs"; - import "./chunk-CCRUODGE.mjs"; -diff --git a/dist/chunk-4T54ULFA.mjs b/dist/chunk-4T54ULFA.mjs -deleted file mode 100644 -index 049ff5858719fd487665c5705ca815d99a21c10d..0000000000000000000000000000000000000000 ---- a/dist/chunk-4T54ULFA.mjs -+++ /dev/null -@@ -1,367 +0,0 @@ --import { -- fetchGasEstimatesViaEthFeeHistory --} from "./chunk-EXCWMMNV.mjs"; --import { -- calculateTimeEstimate, -- fetchEthGasPriceEstimate, -- fetchGasEstimates, -- fetchLegacyGasPriceEstimates --} from "./chunk-CCRUODGE.mjs"; --import { -- __privateAdd, -- __privateGet, -- __privateMethod, -- __privateSet --} from "./chunk-XUI43LEZ.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 } --}; --var name = "GasFeeController"; --var defaultState = { -- gasFeeEstimatesByChainId: {}, -- gasFeeEstimates: {}, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.NONE --}; --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}` -- ), -- fetchGasEstimatesViaEthFeeHistory, -- fetchLegacyGasPriceEstimates, -- fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchEthGasPriceEstimate, -- calculateTimeEstimate, -- clientId: this.clientId, -- ethQuery -- }); -- if (shouldUpdateState) { -- this.update((state) => { -- state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -- state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -- state.gasEstimateType = gasFeeCalculations.gasEstimateType; -- state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -- state.gasFeeEstimatesByChainId[toHex(decimalChainId)] = { -- 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 -- ); -- } --}; --_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({ -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- fetchGasEstimates: fetchGasEstimates2, -- fetchGasEstimatesUrl, -- fetchGasEstimatesViaEthFeeHistory: fetchGasEstimatesViaEthFeeHistory2, -- fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -- fetchLegacyGasPriceEstimatesUrl, -- fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2, -- calculateTimeEstimate: calculateTimeEstimate2, -- clientId, -- ethQuery --}) { -- try { -- if (isEIP1559Compatible) { -- let estimates; -- try { -- estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -- } catch { -- estimates = await fetchGasEstimatesViaEthFeeHistory2(ethQuery); -- } -- const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -- const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -- suggestedMaxPriorityFeePerGas, -- suggestedMaxFeePerGas, -- estimates -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds, -- gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -- }; -- } else if (isLegacyGasAPICompatible) { -- const estimates = await fetchLegacyGasPriceEstimates2( -- fetchLegacyGasPriceEstimatesUrl, -- clientId -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -- }; -- } -- throw new Error("Main gas fee/price estimation failed. Use fallback"); -- } catch { -- try { -- const estimates = await fetchEthGasPriceEstimate2(ethQuery); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -- }; -- } catch (error) { -- if (error instanceof Error) { -- throw new Error( -- `Gas fee/price estimation failed. Message: ${error.message}` -- ); -- } -- throw error; -- } -- } --} -- --export { -- determineGasFeeCalculations, -- LEGACY_GAS_PRICES_API_URL, -- GAS_ESTIMATE_TYPES, -- GasFeeController, -- GasFeeController_default --}; --//# sourceMappingURL=chunk-4T54ULFA.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-4T54ULFA.mjs.map b/dist/chunk-4T54ULFA.mjs.map -deleted file mode 100644 -index afa8fb46c485be85d49f57b63ff8211979da31e4..0000000000000000000000000000000000000000 ---- a/dist/chunk-4T54ULFA.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 fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';\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};\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 & SingleChainGasFeeState;\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};\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 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 fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n });\n\n if (shouldUpdateState) {\n this.update((state) => {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[toHex(decimalChainId)] = {\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\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\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.fetchGasEstimatesViaEthFeeHistory - A function that fetches gas estimates using\n * `eth_feeHistory` (an EIP-1559 feature).\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 * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId,\n ethQuery,\n}: {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchGasEstimatesViaEthFeeHistory: (\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any,\n ) => Promise;\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}): Promise {\n try {\n if (isEIP1559Compatible) {\n let estimates: GasFeeEstimates;\n try {\n estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n } catch {\n estimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);\n }\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n } else if (isLegacyGasAPICompatible) {\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n }\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n try {\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n throw error;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAWtB,IAAM,4BAA4B;AA0BlC,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AACrD;AAkDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AACtC;AA1PA;AA+PO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,EACF,GAaG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAkPH,uBAAM;AA3SN;AA0DE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB;AAC1B,SAAK,oBAAoB;AACzB,SAAK,WAAW;AAEhB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,mBAAmB;AACrB,WAAK,OAAO,CAAC,UAAU;AACrB,cAAM,kBAAkB,mBAAmB;AAC3C,cAAM,4BACJ,mBAAmB;AACrB,cAAM,kBAAkB,mBAAmB;AAC3C,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,MAAM,cAAc,CAAC,IAAI;AAAA,UACtD,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAYF;AArTE;AA2SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAGF,IAAO,2BAAQ;;;AC9iBf,eAAO,4BAAmD;AAAA,EACxD;AAAA,EACA;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,mCAAAC;AAAA,EACA,8BAAAC;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA,uBAAAC;AAAA,EACA;AAAA,EACA;AACF,GA8BgC;AAC9B,MAAI;AACF,QAAI,qBAAqB;AACvB,UAAI;AACJ,UAAI;AACF,oBAAY,MAAMJ,mBAAkB,sBAAsB,QAAQ;AAAA,MACpE,QAAQ;AACN,oBAAY,MAAMC,mCAAkC,QAAQ;AAAA,MAC9D;AACA,YAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AACZ,YAAM,4BAA4BG;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB;AAAA,QACA,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,WAAW,0BAA0B;AACnC,YAAM,YAAY,MAAMF;AAAA,QACtB;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,QAAI;AACF,YAAM,YAAY,MAAMC,0BAAyB,QAAQ;AACzD,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,OAAO;AAC1B,cAAM,IAAI;AAAA,UACR,6CAA6C,MAAM,OAAO;AAAA,QAC5D;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":["fetchGasEstimates","fetchGasEstimatesViaEthFeeHistory","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate","calculateTimeEstimate"]} -\ No newline at end of file -diff --git a/dist/chunk-IBADKXI6.js b/dist/chunk-IBADKXI6.js -new file mode 100644 -index 0000000000000000000000000000000000000000..d69fcfd95d4e2de007c82ab6743ca18a2af45a76 ---- /dev/null -+++ b/dist/chunk-IBADKXI6.js -@@ -0,0 +1,370 @@ -+"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -+ -+var _chunkEZVGDV5Hjs = require('./chunk-EZVGDV5H.js'); -+ -+ -+ -+ -+ -+var _chunkF46NZXRQjs = require('./chunk-F46NZXRQ.js'); -+ -+ -+ -+ -+ -+var _chunkZ4BLTVTBjs = require('./chunk-Z4BLTVTB.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 } -+}; -+var name = "GasFeeController"; -+var defaultState = { -+ gasFeeEstimatesByChainId: {}, -+ gasFeeEstimates: {}, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.NONE -+}; -+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 } -+ }); -+ _chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _onNetworkControllerDidChange); -+ _chunkZ4BLTVTBjs.__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; -+ _chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _getProvider, getProvider); -+ this.EIP1559APIEndpoint = EIP1559APIEndpoint; -+ this.legacyAPIEndpoint = legacyAPIEndpoint; -+ this.clientId = clientId; -+ this.ethQuery = new (0, _ethquery2.default)(_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getProvider).call(this)); -+ if (onNetworkDidChange && getChainId) { -+ this.currentChainId = getChainId(); -+ onNetworkDidChange(async (networkControllerState) => { -+ await _chunkZ4BLTVTBjs.__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 _chunkZ4BLTVTBjs.__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: _chunkF46NZXRQjs.fetchGasEstimates, -+ fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchGasEstimatesViaEthFeeHistory: _chunkEZVGDV5Hjs.fetchGasEstimatesViaEthFeeHistory, -+ fetchLegacyGasPriceEstimates: _chunkF46NZXRQjs.fetchLegacyGasPriceEstimates, -+ fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchEthGasPriceEstimate: _chunkF46NZXRQjs.fetchEthGasPriceEstimate, -+ calculateTimeEstimate: _chunkF46NZXRQjs.calculateTimeEstimate, -+ clientId: this.clientId, -+ ethQuery -+ }); -+ 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 _chunkF46NZXRQjs.calculateTimeEstimate.call(void 0, -+ maxPriorityFeePerGas, -+ maxFeePerGas, -+ this.state.gasFeeEstimates -+ ); -+ } -+}; -+_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)(_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getProvider).call(this)); -+ await this.resetPolling(); -+ this.currentChainId = newChainId; -+ } -+}; -+var GasFeeController_default = GasFeeController; -+ -+// src/determineGasFeeCalculations.ts -+async function determineGasFeeCalculations({ -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ fetchGasEstimates: fetchGasEstimates2, -+ fetchGasEstimatesUrl, -+ fetchGasEstimatesViaEthFeeHistory: fetchGasEstimatesViaEthFeeHistory2, -+ fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -+ fetchLegacyGasPriceEstimatesUrl, -+ fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2, -+ calculateTimeEstimate: calculateTimeEstimate2, -+ clientId, -+ ethQuery -+}) { -+ try { -+ if (isEIP1559Compatible) { -+ let estimates; -+ try { -+ estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -+ } catch { -+ estimates = await fetchGasEstimatesViaEthFeeHistory2(ethQuery); -+ } -+ const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -+ const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -+ suggestedMaxPriorityFeePerGas, -+ suggestedMaxFeePerGas, -+ estimates -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds, -+ gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -+ }; -+ } else if (isLegacyGasAPICompatible) { -+ const estimates = await fetchLegacyGasPriceEstimates2( -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -+ }; -+ } -+ throw new Error("Main gas fee/price estimation failed. Use fallback"); -+ } catch { -+ try { -+ const estimates = await fetchEthGasPriceEstimate2(ethQuery); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -+ }; -+ } catch (error) { -+ if (error instanceof Error) { -+ throw new Error( -+ `Gas fee/price estimation failed. Message: ${error.message}` -+ ); -+ } -+ throw error; -+ } -+ } -+} -+ -+ -+ -+ -+ -+ -+ -+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-IBADKXI6.js.map -\ No newline at end of file -diff --git a/dist/chunk-IBADKXI6.js.map b/dist/chunk-IBADKXI6.js.map -new file mode 100644 -index 0000000000000000000000000000000000000000..7d650730cb2225437c3dda8b0603243cd1586082 ---- /dev/null -+++ b/dist/chunk-IBADKXI6.js.map -@@ -0,0 +1 @@ -+{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","fetchGasEstimatesViaEthFeeHistory","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate","calculateTimeEstimate"],"mappings":";;;;;;;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAWtB,IAAM,4BAA4B;AA0BlC,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AACrD;AAkDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AACtC;AA1PA;AA+PO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,EACF,GAaG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAqPH,uBAAM;AA9SN;AA0DE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB;AAC1B,SAAK,oBAAoB;AACzB,SAAK,WAAW;AAEhB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAYF;AAxTE;AA8SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAGF,IAAO,2BAAQ;;;ACjjBf,eAAO,4BAAmD;AAAA,EACxD;AAAA,EACA;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,mCAAAC;AAAA,EACA,8BAAAC;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA,uBAAAC;AAAA,EACA;AAAA,EACA;AACF,GA8BgC;AAC9B,MAAI;AACF,QAAI,qBAAqB;AACvB,UAAI;AACJ,UAAI;AACF,oBAAY,MAAMJ,mBAAkB,sBAAsB,QAAQ;AAAA,MACpE,QAAQ;AACN,oBAAY,MAAMC,mCAAkC,QAAQ;AAAA,MAC9D;AACA,YAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AACZ,YAAM,4BAA4BG;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB;AAAA,QACA,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,WAAW,0BAA0B;AACnC,YAAM,YAAY,MAAMF;AAAA,QACtB;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,QAAI;AACF,YAAM,YAAY,MAAMC,0BAAyB,QAAQ;AACzD,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,OAAO;AAC1B,cAAM,IAAI;AAAA,UACR,6CAA6C,MAAM,OAAO;AAAA,QAC5D;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF","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 fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';\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};\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 & SingleChainGasFeeState;\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};\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 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 fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\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\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\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.fetchGasEstimatesViaEthFeeHistory - A function that fetches gas estimates using\n * `eth_feeHistory` (an EIP-1559 feature).\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 * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId,\n ethQuery,\n}: {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchGasEstimatesViaEthFeeHistory: (\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any,\n ) => Promise;\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}): Promise {\n try {\n if (isEIP1559Compatible) {\n let estimates: GasFeeEstimates;\n try {\n estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n } catch {\n estimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);\n }\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n } else if (isLegacyGasAPICompatible) {\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n }\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n try {\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n throw error;\n }\n }\n}\n"]} -\ No newline at end of file -diff --git a/dist/chunk-L45HVISM.mjs b/dist/chunk-L45HVISM.mjs -new file mode 100644 -index 0000000000000000000000000000000000000000..219a2f2e453cc4104da16c60a54c4cdd27dbb504 ---- /dev/null -+++ b/dist/chunk-L45HVISM.mjs -@@ -0,0 +1,370 @@ -+import { -+ fetchGasEstimatesViaEthFeeHistory -+} from "./chunk-EXCWMMNV.mjs"; -+import { -+ calculateTimeEstimate, -+ fetchEthGasPriceEstimate, -+ fetchGasEstimates, -+ fetchLegacyGasPriceEstimates -+} from "./chunk-CCRUODGE.mjs"; -+import { -+ __privateAdd, -+ __privateGet, -+ __privateMethod, -+ __privateSet -+} from "./chunk-XUI43LEZ.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 } -+}; -+var name = "GasFeeController"; -+var defaultState = { -+ gasFeeEstimatesByChainId: {}, -+ gasFeeEstimates: {}, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.NONE -+}; -+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}` -+ ), -+ fetchGasEstimatesViaEthFeeHistory, -+ fetchLegacyGasPriceEstimates, -+ fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -+ "", -+ `${decimalChainId}` -+ ), -+ fetchEthGasPriceEstimate, -+ calculateTimeEstimate, -+ clientId: this.clientId, -+ ethQuery -+ }); -+ 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 -+ ); -+ } -+}; -+_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({ -+ isEIP1559Compatible, -+ isLegacyGasAPICompatible, -+ fetchGasEstimates: fetchGasEstimates2, -+ fetchGasEstimatesUrl, -+ fetchGasEstimatesViaEthFeeHistory: fetchGasEstimatesViaEthFeeHistory2, -+ fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -+ fetchLegacyGasPriceEstimatesUrl, -+ fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2, -+ calculateTimeEstimate: calculateTimeEstimate2, -+ clientId, -+ ethQuery -+}) { -+ try { -+ if (isEIP1559Compatible) { -+ let estimates; -+ try { -+ estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -+ } catch { -+ estimates = await fetchGasEstimatesViaEthFeeHistory2(ethQuery); -+ } -+ const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -+ const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -+ suggestedMaxPriorityFeePerGas, -+ suggestedMaxFeePerGas, -+ estimates -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds, -+ gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -+ }; -+ } else if (isLegacyGasAPICompatible) { -+ const estimates = await fetchLegacyGasPriceEstimates2( -+ fetchLegacyGasPriceEstimatesUrl, -+ clientId -+ ); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -+ }; -+ } -+ throw new Error("Main gas fee/price estimation failed. Use fallback"); -+ } catch { -+ try { -+ const estimates = await fetchEthGasPriceEstimate2(ethQuery); -+ return { -+ gasFeeEstimates: estimates, -+ estimatedGasFeeTimeBounds: {}, -+ gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -+ }; -+ } catch (error) { -+ if (error instanceof Error) { -+ throw new Error( -+ `Gas fee/price estimation failed. Message: ${error.message}` -+ ); -+ } -+ throw error; -+ } -+ } -+} -+ -+export { -+ determineGasFeeCalculations, -+ LEGACY_GAS_PRICES_API_URL, -+ GAS_ESTIMATE_TYPES, -+ GasFeeController, -+ GasFeeController_default -+}; -+//# sourceMappingURL=chunk-L45HVISM.mjs.map -\ No newline at end of file -diff --git a/dist/chunk-L45HVISM.mjs.map b/dist/chunk-L45HVISM.mjs.map -new file mode 100644 -index 0000000000000000000000000000000000000000..d45d573d02b1be6f8da2687a772e7dac7884d8ff ---- /dev/null -+++ b/dist/chunk-L45HVISM.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 fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';\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};\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 & SingleChainGasFeeState;\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};\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 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 fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\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\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\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.fetchGasEstimatesViaEthFeeHistory - A function that fetches gas estimates using\n * `eth_feeHistory` (an EIP-1559 feature).\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 * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId,\n ethQuery,\n}: {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchGasEstimatesViaEthFeeHistory: (\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any,\n ) => Promise;\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}): Promise {\n try {\n if (isEIP1559Compatible) {\n let estimates: GasFeeEstimates;\n try {\n estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n } catch {\n estimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);\n }\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n } else if (isLegacyGasAPICompatible) {\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n }\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n try {\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n throw error;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAWtB,IAAM,4BAA4B;AA0BlC,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AACrD;AAkDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AACtC;AA1PA;AA+PO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,EACF,GAaG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAqPH,uBAAM;AA9SN;AA0DE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB;AAC1B,SAAK,oBAAoB;AACzB,SAAK,WAAW;AAEhB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAYF;AAxTE;AA8SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAGF,IAAO,2BAAQ;;;ACjjBf,eAAO,4BAAmD;AAAA,EACxD;AAAA,EACA;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,mCAAAC;AAAA,EACA,8BAAAC;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA,uBAAAC;AAAA,EACA;AAAA,EACA;AACF,GA8BgC;AAC9B,MAAI;AACF,QAAI,qBAAqB;AACvB,UAAI;AACJ,UAAI;AACF,oBAAY,MAAMJ,mBAAkB,sBAAsB,QAAQ;AAAA,MACpE,QAAQ;AACN,oBAAY,MAAMC,mCAAkC,QAAQ;AAAA,MAC9D;AACA,YAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AACZ,YAAM,4BAA4BG;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB;AAAA,QACA,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,WAAW,0BAA0B;AACnC,YAAM,YAAY,MAAMF;AAAA,QACtB;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,QAAI;AACF,YAAM,YAAY,MAAMC,0BAAyB,QAAQ;AACzD,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,OAAO;AAC1B,cAAM,IAAI;AAAA,UACR,6CAA6C,MAAM,OAAO;AAAA,QAC5D;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF;","names":["fetchGasEstimates","fetchGasEstimatesViaEthFeeHistory","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate","calculateTimeEstimate"]} -\ No newline at end of file -diff --git a/dist/chunk-N5BANBTW.js b/dist/chunk-N5BANBTW.js -deleted file mode 100644 -index 1af6409114a301a745ababc04fc13602ab97fa5e..0000000000000000000000000000000000000000 ---- a/dist/chunk-N5BANBTW.js -+++ /dev/null -@@ -1,367 +0,0 @@ --"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -- --var _chunkEZVGDV5Hjs = require('./chunk-EZVGDV5H.js'); -- -- -- -- -- --var _chunkF46NZXRQjs = require('./chunk-F46NZXRQ.js'); -- -- -- -- -- --var _chunkZ4BLTVTBjs = require('./chunk-Z4BLTVTB.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 } --}; --var name = "GasFeeController"; --var defaultState = { -- gasFeeEstimatesByChainId: {}, -- gasFeeEstimates: {}, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.NONE --}; --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 } -- }); -- _chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _onNetworkControllerDidChange); -- _chunkZ4BLTVTBjs.__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; -- _chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _getProvider, getProvider); -- this.EIP1559APIEndpoint = EIP1559APIEndpoint; -- this.legacyAPIEndpoint = legacyAPIEndpoint; -- this.clientId = clientId; -- this.ethQuery = new (0, _ethquery2.default)(_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getProvider).call(this)); -- if (onNetworkDidChange && getChainId) { -- this.currentChainId = getChainId(); -- onNetworkDidChange(async (networkControllerState) => { -- await _chunkZ4BLTVTBjs.__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 _chunkZ4BLTVTBjs.__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: _chunkF46NZXRQjs.fetchGasEstimates, -- fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchGasEstimatesViaEthFeeHistory: _chunkEZVGDV5Hjs.fetchGasEstimatesViaEthFeeHistory, -- fetchLegacyGasPriceEstimates: _chunkF46NZXRQjs.fetchLegacyGasPriceEstimates, -- fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace( -- "", -- `${decimalChainId}` -- ), -- fetchEthGasPriceEstimate: _chunkF46NZXRQjs.fetchEthGasPriceEstimate, -- calculateTimeEstimate: _chunkF46NZXRQjs.calculateTimeEstimate, -- clientId: this.clientId, -- ethQuery -- }); -- if (shouldUpdateState) { -- this.update((state) => { -- state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates; -- state.estimatedGasFeeTimeBounds = gasFeeCalculations.estimatedGasFeeTimeBounds; -- state.gasEstimateType = gasFeeCalculations.gasEstimateType; -- state.gasFeeEstimatesByChainId ?? (state.gasFeeEstimatesByChainId = {}); -- state.gasFeeEstimatesByChainId[_controllerutils.toHex.call(void 0, decimalChainId)] = { -- 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 _chunkF46NZXRQjs.calculateTimeEstimate.call(void 0, -- maxPriorityFeePerGas, -- maxFeePerGas, -- this.state.gasFeeEstimates -- ); -- } --}; --_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)(_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getProvider).call(this)); -- await this.resetPolling(); -- this.currentChainId = newChainId; -- } --}; --var GasFeeController_default = GasFeeController; -- --// src/determineGasFeeCalculations.ts --async function determineGasFeeCalculations({ -- isEIP1559Compatible, -- isLegacyGasAPICompatible, -- fetchGasEstimates: fetchGasEstimates2, -- fetchGasEstimatesUrl, -- fetchGasEstimatesViaEthFeeHistory: fetchGasEstimatesViaEthFeeHistory2, -- fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, -- fetchLegacyGasPriceEstimatesUrl, -- fetchEthGasPriceEstimate: fetchEthGasPriceEstimate2, -- calculateTimeEstimate: calculateTimeEstimate2, -- clientId, -- ethQuery --}) { -- try { -- if (isEIP1559Compatible) { -- let estimates; -- try { -- estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); -- } catch { -- estimates = await fetchGasEstimatesViaEthFeeHistory2(ethQuery); -- } -- const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; -- const estimatedGasFeeTimeBounds = calculateTimeEstimate2( -- suggestedMaxPriorityFeePerGas, -- suggestedMaxFeePerGas, -- estimates -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds, -- gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET -- }; -- } else if (isLegacyGasAPICompatible) { -- const estimates = await fetchLegacyGasPriceEstimates2( -- fetchLegacyGasPriceEstimatesUrl, -- clientId -- ); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY -- }; -- } -- throw new Error("Main gas fee/price estimation failed. Use fallback"); -- } catch { -- try { -- const estimates = await fetchEthGasPriceEstimate2(ethQuery); -- return { -- gasFeeEstimates: estimates, -- estimatedGasFeeTimeBounds: {}, -- gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE -- }; -- } catch (error) { -- if (error instanceof Error) { -- throw new Error( -- `Gas fee/price estimation failed. Message: ${error.message}` -- ); -- } -- throw error; -- } -- } --} -- -- -- -- -- -- -- --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-N5BANBTW.js.map -\ No newline at end of file -diff --git a/dist/chunk-N5BANBTW.js.map b/dist/chunk-N5BANBTW.js.map -deleted file mode 100644 -index 344544fa436adddd331b2152926ecaf81ae94bc2..0000000000000000000000000000000000000000 ---- a/dist/chunk-N5BANBTW.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","fetchGasEstimatesViaEthFeeHistory","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate","calculateTimeEstimate"],"mappings":";;;;;;;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAWtB,IAAM,4BAA4B;AA0BlC,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AACrD;AAkDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AACtC;AA1PA;AA+PO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,IACA;AAAA,EACF,GAaG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAkPH,uBAAM;AA3SN;AA0DE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB;AAC1B,SAAK,oBAAoB;AACzB,SAAK,WAAW;AAEhB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,mBAAmB;AACrB,WAAK,OAAO,CAAC,UAAU;AACrB,cAAM,kBAAkB,mBAAmB;AAC3C,cAAM,4BACJ,mBAAmB;AACrB,cAAM,kBAAkB,mBAAmB;AAC3C,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,MAAM,cAAc,CAAC,IAAI;AAAA,UACtD,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAYF;AArTE;AA2SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAGF,IAAO,2BAAQ;;;AC9iBf,eAAO,4BAAmD;AAAA,EACxD;AAAA,EACA;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,mCAAAC;AAAA,EACA,8BAAAC;AAAA,EACA;AAAA,EACA,0BAAAC;AAAA,EACA,uBAAAC;AAAA,EACA;AAAA,EACA;AACF,GA8BgC;AAC9B,MAAI;AACF,QAAI,qBAAqB;AACvB,UAAI;AACJ,UAAI;AACF,oBAAY,MAAMJ,mBAAkB,sBAAsB,QAAQ;AAAA,MACpE,QAAQ;AACN,oBAAY,MAAMC,mCAAkC,QAAQ;AAAA,MAC9D;AACA,YAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AACZ,YAAM,4BAA4BG;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB;AAAA,QACA,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,WAAW,0BAA0B;AACnC,YAAM,YAAY,MAAMF;AAAA,QACtB;AAAA,QACA;AAAA,MACF;AACA,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF;AACA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,QAAI;AACF,YAAM,YAAY,MAAMC,0BAAyB,QAAQ;AACzD,aAAO;AAAA,QACL,iBAAiB;AAAA,QACjB,2BAA2B,CAAC;AAAA,QAC5B,iBAAiB,mBAAmB;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,UAAI,iBAAiB,OAAO;AAC1B,cAAM,IAAI;AAAA,UACR,6CAA6C,MAAM,OAAO;AAAA,QAC5D;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AACF","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 fetchGasEstimatesViaEthFeeHistory from './fetchGasEstimatesViaEthFeeHistory';\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};\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 & SingleChainGasFeeState;\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};\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 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 fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n });\n\n if (shouldUpdateState) {\n this.update((state) => {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[toHex(decimalChainId)] = {\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\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\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.fetchGasEstimatesViaEthFeeHistory - A function that fetches gas estimates using\n * `eth_feeHistory` (an EIP-1559 feature).\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 * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n fetchGasEstimatesViaEthFeeHistory,\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId,\n ethQuery,\n}: {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchGasEstimatesViaEthFeeHistory: (\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any,\n ) => Promise;\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}): Promise {\n try {\n if (isEIP1559Compatible) {\n let estimates: GasFeeEstimates;\n try {\n estimates = await fetchGasEstimates(fetchGasEstimatesUrl, clientId);\n } catch {\n estimates = await fetchGasEstimatesViaEthFeeHistory(ethQuery);\n }\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n } else if (isLegacyGasAPICompatible) {\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n clientId,\n );\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n }\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n try {\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n throw error;\n }\n }\n}\n"]} -\ No newline at end of file -diff --git a/dist/determineGasFeeCalculations.js b/dist/determineGasFeeCalculations.js -index 4c2cccb6b615673a89aa2335a34106eed46ca92e..efd3b433637e166ebdca72a76fb07fd049b57f9a 100644 ---- a/dist/determineGasFeeCalculations.js -+++ b/dist/determineGasFeeCalculations.js -@@ -1,6 +1,6 @@ - "use strict";Object.defineProperty(exports, "__esModule", {value: true}); - --var _chunkN5BANBTWjs = require('./chunk-N5BANBTW.js'); -+var _chunkIBADKXI6js = require('./chunk-IBADKXI6.js'); - require('./chunk-EZVGDV5H.js'); - require('./chunk-5INBFZXY.js'); - require('./chunk-F46NZXRQ.js'); -@@ -10,5 +10,5 @@ require('./chunk-LO7OP5FM.js'); - require('./chunk-Z4BLTVTB.js'); - - --exports.default = _chunkN5BANBTWjs.determineGasFeeCalculations; -+exports.default = _chunkIBADKXI6js.determineGasFeeCalculations; - //# sourceMappingURL=determineGasFeeCalculations.js.map -\ No newline at end of file -diff --git a/dist/determineGasFeeCalculations.mjs b/dist/determineGasFeeCalculations.mjs -index 62648b3e32104072a90f4a94c2b8390ebc5a0738..7b1b6485a9ae37b0011c1b2ea7ea2d0260002319 100644 ---- a/dist/determineGasFeeCalculations.mjs -+++ b/dist/determineGasFeeCalculations.mjs -@@ -1,6 +1,6 @@ - import { - determineGasFeeCalculations --} from "./chunk-4T54ULFA.mjs"; -+} from "./chunk-L45HVISM.mjs"; - import "./chunk-EXCWMMNV.mjs"; - import "./chunk-AQN4AQEF.mjs"; - import "./chunk-CCRUODGE.mjs"; -diff --git a/dist/index.js b/dist/index.js -index 75ffb368cb40043c84c5534a8834d7a8d4ded148..6b6ba4cc47889d4191f2bc816835488f132d0ac0 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -2,7 +2,7 @@ - - - --var _chunkN5BANBTWjs = require('./chunk-N5BANBTW.js'); -+var _chunkIBADKXI6js = require('./chunk-IBADKXI6.js'); - require('./chunk-EZVGDV5H.js'); - require('./chunk-5INBFZXY.js'); - require('./chunk-F46NZXRQ.js'); -@@ -14,5 +14,5 @@ require('./chunk-Z4BLTVTB.js'); - - - --exports.GAS_ESTIMATE_TYPES = _chunkN5BANBTWjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkN5BANBTWjs.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkN5BANBTWjs.LEGACY_GAS_PRICES_API_URL; -+exports.GAS_ESTIMATE_TYPES = _chunkIBADKXI6js.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkIBADKXI6js.GasFeeController; exports.LEGACY_GAS_PRICES_API_URL = _chunkIBADKXI6js.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 af8c3102b9230eb4e4eb7c1fdd3a19627eadeaf8..1bcc754361c9be01145c6f96438da3fc89d973ab 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -2,7 +2,7 @@ import { - GAS_ESTIMATE_TYPES, - GasFeeController, - LEGACY_GAS_PRICES_API_URL --} from "./chunk-4T54ULFA.mjs"; -+} from "./chunk-L45HVISM.mjs"; - import "./chunk-EXCWMMNV.mjs"; - import "./chunk-AQN4AQEF.mjs"; - import "./chunk-CCRUODGE.mjs"; -diff --git a/dist/tsconfig.build.tsbuildinfo b/dist/tsconfig.build.tsbuildinfo -index 3a0c1e4bc5a06afea527dcd7882f1baf27162e8f..d03af3cd1923f60769878b6942fe2947810551f1 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.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/ts4.8/assert.d.ts","../../../node_modules/@types/node/ts4.8/assert/strict.d.ts","../../../node_modules/@types/node/ts4.8/globals.d.ts","../../../node_modules/@types/node/ts4.8/async_hooks.d.ts","../../../node_modules/@types/node/ts4.8/buffer.d.ts","../../../node_modules/@types/node/ts4.8/child_process.d.ts","../../../node_modules/@types/node/ts4.8/cluster.d.ts","../../../node_modules/@types/node/ts4.8/console.d.ts","../../../node_modules/@types/node/ts4.8/constants.d.ts","../../../node_modules/@types/node/ts4.8/crypto.d.ts","../../../node_modules/@types/node/ts4.8/dgram.d.ts","../../../node_modules/@types/node/ts4.8/diagnostics_channel.d.ts","../../../node_modules/@types/node/ts4.8/dns.d.ts","../../../node_modules/@types/node/ts4.8/dns/promises.d.ts","../../../node_modules/@types/node/ts4.8/domain.d.ts","../../../node_modules/@types/node/ts4.8/events.d.ts","../../../node_modules/@types/node/ts4.8/fs.d.ts","../../../node_modules/@types/node/ts4.8/fs/promises.d.ts","../../../node_modules/@types/node/ts4.8/http.d.ts","../../../node_modules/@types/node/ts4.8/http2.d.ts","../../../node_modules/@types/node/ts4.8/https.d.ts","../../../node_modules/@types/node/ts4.8/inspector.d.ts","../../../node_modules/@types/node/ts4.8/module.d.ts","../../../node_modules/@types/node/ts4.8/net.d.ts","../../../node_modules/@types/node/ts4.8/os.d.ts","../../../node_modules/@types/node/ts4.8/path.d.ts","../../../node_modules/@types/node/ts4.8/perf_hooks.d.ts","../../../node_modules/@types/node/ts4.8/process.d.ts","../../../node_modules/@types/node/ts4.8/punycode.d.ts","../../../node_modules/@types/node/ts4.8/querystring.d.ts","../../../node_modules/@types/node/ts4.8/readline.d.ts","../../../node_modules/@types/node/ts4.8/repl.d.ts","../../../node_modules/@types/node/ts4.8/stream.d.ts","../../../node_modules/@types/node/ts4.8/stream/promises.d.ts","../../../node_modules/@types/node/ts4.8/stream/consumers.d.ts","../../../node_modules/@types/node/ts4.8/stream/web.d.ts","../../../node_modules/@types/node/ts4.8/string_decoder.d.ts","../../../node_modules/@types/node/ts4.8/test.d.ts","../../../node_modules/@types/node/ts4.8/timers.d.ts","../../../node_modules/@types/node/ts4.8/timers/promises.d.ts","../../../node_modules/@types/node/ts4.8/tls.d.ts","../../../node_modules/@types/node/ts4.8/trace_events.d.ts","../../../node_modules/@types/node/ts4.8/tty.d.ts","../../../node_modules/@types/node/ts4.8/url.d.ts","../../../node_modules/@types/node/ts4.8/util.d.ts","../../../node_modules/@types/node/ts4.8/v8.d.ts","../../../node_modules/@types/node/ts4.8/vm.d.ts","../../../node_modules/@types/node/ts4.8/wasi.d.ts","../../../node_modules/@types/node/ts4.8/worker_threads.d.ts","../../../node_modules/@types/node/ts4.8/zlib.d.ts","../../../node_modules/@types/node/ts4.8/globals.global.d.ts","../../../node_modules/@types/node/ts4.8/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/regex.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","../../polling-controller/dist/types/AbstractPollingController.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/fetchBlockFeeHistory.ts","../src/fetchGasEstimatesViaEthFeeHistory/medianOf.ts","../src/fetchGasEstimatesViaEthFeeHistory/calculateGasFeeEstimatesForPriorityLevels.ts","../src/fetchGasEstimatesViaEthFeeHistory/types.ts","../src/fetchGasEstimatesViaEthFeeHistory/fetchLatestBlock.ts","../src/fetchGasEstimatesViaEthFeeHistory.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":"f20c05dbfe50a208301d2a1da37b9931bce0466eb5a1f4fe240971b4ecc82b67","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","e6b724280c694a9f588847f754198fb96c43d805f065c3a5b28bbc9594541c84","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"9b087de7268e4efc5f215347a62656663933d63c0b1d7b624913240367b999ea","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"0d5f52b3174bee6edb81260ebcd792692c32c81fd55499d69531496f3f2b25e7","affectsGlobalScope":true},{"version":"55f400eec64d17e888e278f4def2f254b41b89515d3b88ad75d5e05f019daddd","affectsGlobalScope":true},{"version":"181f1784c6c10b751631b24ce60c7f78b20665db4550b335be179217bacc0d5f","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"75ec0bdd727d887f1b79ed6619412ea72ba3c81d92d0787ccb64bab18d261f14","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":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"775d9c9fd150d5de79e0450f35bc8b8f94ae64e3eb5da12725ff2a649dccc777","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","869b0f507115c42896d917642f821752e8a84827bfe9ed74c23d76fb0c64c681","e475453e7140e95542332943d3052fe4c7430ad1efce42b3e9157f1fee8cbc5f","ebfdf904255ce746c9d30117c2edef355fb19bf7650478d2405f39f0e4f302e6","f3f63b48addb8e2ea9d20bb671c3c306413b3daa39996d0ae52f63d8e32158e1","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","b4000a0a525fa921e896cbdb32ae802c9684f0fd371b5fc69e7310f7918cc2c3","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","affb9dc7079c3a3522e046c5dc1325950a843b1ebd7dc0f0386aeb2397b9f0db","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","34693fb4a5e771e11668219221344dd1bd7d8b77ed005a1c1d965fb559be8406","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"8c61d5fc50490af59daf69c4e601cc76de260ee5b2ff057d608a78d6acb0b61a","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"a7289d79eb84a59d2475b4d0136b4404be3cfdd17c3ea46b9194add1d645df01","affectsGlobalScope":true},"0bb26fa2a90ee890eed57ee812c71fa84d3d07850163ec4a204de86412cc57c1","132ca47da601c60141dd6f10bd08c70d0620177e5638439df2464ec3945b6d98",{"version":"55d2bbae076fed7269c3e16faeb32f988f558427b7a1c3bf04aa7551ab86ae90","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","543c6e3a2353e9ad08b4090e1fb88a95cefb756d0d173b6ec045d7dc70a79964","3a41ebe7f089d50f447466b35b6cabb8b584c0994fc9809d0cd0a4ebc41e1239","f5350b93f10178838c23b3a81f3791d839bc44d1a2a89edb60069250fab90899","aa07f7230bcc5733919c941753d067cb8816dcad6651edb815cb302ae8ddd931","649ba4638a25c54a18dffe37367c6b7848a0bca53fa42fbbd7300b0c61aa861c","5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"a34d65f61ec5aac5b53502c8b0bd4e00d217bccb95bf94d449e2571baa11fb8c","affectsGlobalScope":true},"8d42e5af5fb0a96a77e135ce84cc60636c9bad39d9dba043a4efe9d1bdeb3cc3","56fcc451e9065eb121c9cc4c1b9994a816306f3b0b3b1fce7ad59f0ac97a9999","d4a13a5a2e6df8ef02a84ac6fd5bad4a1ec54fdef47f33250da386ea6d5c1864","c3759b5bc5cc40f5988d86a497741a80fa91258629ae50a2b3735e774cd377cc","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","45dd82fb5aea9b12b2a90b427b28f3a014e8b2ee9b74087a5ab882841cb5fbc5",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"48b2f9302651eb31acd5be69bb4e6b35797a7fcd6b77391d10a4ccadf7dc3609","605bed8af3052e790865a35e3d538a5447764a4ff01989c0f6b084a96f40e1cb","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"2db274de1088f268805043df72e21258eae845e6418dada65331d2898998f330","affectsGlobalScope":true},{"version":"2923dee3c897f03e91b54a210cdbefea7290562f0ac4b948667d4c9ee844b79e","affectsGlobalScope":true},"79169698d09a2be54b14f3bcad2575b414bf3525063fde0a1e4fcd5d6efd380e","051d939bcf77caa3cef3282708ab3a6fdfb741a7366e1d74a9e7603b67417ec3","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","f6877afc4b9d0b72e15378b187492a6547e48783980e9ff1301163b7b1c15b2e","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},"e3685a8957b4e2af64c3f04a58289ee0858a649dbcd963a2b897fe85858ae18a","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","3140d587067e55ce4028275cf71b40a3fd431863ad148efc3106af84a0794cf9","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","55fcfa6909a227963d5ad3d59ea9f99480106ebd9c69d87fce8a66f8f66e23c3","7f03c7ae3f6cedb6d9261d31d0fb518d940fb2f1b8d2b02b306c6d4b7e1bc8aa","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","b59e5a5f93b7bbf729cacd0bc86106498e58f07114868350265a3de3598312c7","7c1b05262a4623e5d2a5832433582e0d6b453a3db8aa799b98f38d105812296f","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","530fb1c53338ac6618f8b8c4aa30acf89a9fe13b2bf92184ad966c824350d827","c1e87ae9c6982a1628be91d9f3a52ec5a73ed1a6d42e4e8119bf96c453b14cc0","161a4d1342b99b095a9531e65ba773ab33df0d906d7ae9f4f39180320fe23237","722cf9592e0d42cb903e40aa725f3e8e7cde91271f6be8c318bfbd7e337ff886","6a6aaf58d8ccff1409f921c37d0eb6cec3113df483c485385af3d90414e8eb07","ac9b69620b356f5bcfbc17d6a2a0591354eade1c5febb05cb1079b30ea0094c6","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","8b497c8cdd875848164f60712378fb15fbc2d625b67d29285845a51fcca57aff","0be91c7eb27de7e2b84c2caa3f89ac2c314de7e00d142c01b3baa0c88163bba4","0a0658c71cfa72984205a2f33b1e28e5e5fdbce0e4fb88186aed4e5a658065dc","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","a12eaa942232703a8a8477a2f240ad5a2c26c595012ea8f128224e77984099c4","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","b0a1e66ed5a0e5dbcd308bccc2e8ba2aac2e17306a3e6888f74bc450c9806a9e","e09f372cbf6582b5b271ef8de9a896d7785aa353dd5f9c574714dcd41636462a","8e13478e90645f18e32db1e7d82729f9b8adf2cfe9e8bb445e6d0c3f8249aebd","d95e9b84527c1091bbccde1a213d987e3eb3c1ba0de7bfb5ee710f45107b5734","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","ccccd5387e496a2899d50ed492c6120c83b7fdfe5d4fe79c4275a79b3ff7b936","751eff4da87d06979e2a4d1d99a9bc98abf04ed569c5871c97e568a83cfff25e","b121f7725eb885a38942124e6d87ef86d650b5221368f49d0b332695d53d0c34","1ff59fddc2d63283af44fd820daff74947a3282bf11bdfb014098aed1a3a51ac","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e",{"version":"e9aa2410e8871df21f0d81fa3afaa114c3092b3bacb25765e3e955697df8cfb8","signature":"7359fe30980ad122c41f840074f7881b195cb703d609a117c8dff7f673c4cf00"},{"version":"a8087c3721ee3a254e604071e4cb70469c142850ea3f3583868a8c05681ce026","signature":"75d8a7ffa7462afb0f4920188068b6b59a1ae85f59b7dad26e8942a7b77ccdda"},{"version":"52df1d99c73edcf21259dca5ba24f1d7643cf3a4014711b47df18e6a02c8cf19","signature":"5d15fe3a984cdba90e92274faeae015371a0ba64d7d443b178e77f701111afe9"},{"version":"9c4834280d7503d03fa8014183f30846170636baab6d264341d2b87d417e418c","signature":"63650a6d250c5f2f5e0e45e99113789dd65481a6605956643f765b0114cc2ce0"},{"version":"4270f9cfd62abd8ff10374a5acec0c2649e1d4f31300fd768c4933a58643dbb5","signature":"798d424b753359c997dcffdfc70e5636749621a687fc1125d444f86a5d22a93e"},{"version":"fd76e840ad3b3f7e9bd6d25b9485dc75a68b210c80f0ee034d4e27775114ba5f","signature":"d18efa93b91f45a1f26b97a23ae82670d8a80a91401f929826c4b5836973801e"},{"version":"b7ece87fafd76aebf93a0b670486851917d4d9ae981486286c231d6e6dc05428","signature":"67590c11008841bf8544396a16000a10b5687394d7caf5d2b5f60f8de9fac20b"},{"version":"15433a2e97dcee760d97f8e56afe908fe932611ef00711aed9b246ba058ce0be","signature":"c5fc52cba80e6ba6fde33b0f9370b8ba6866831070cbc84b20a7c6e9e1163160"},{"version":"8fab09703e473038c5b990ae1785dc9556b6dddb70694de55cc39f661c6ca7bd","signature":"c0e1493caaab115c737b9ef6f4d37b7d2962a1bf16a18b602f882c9a0a4b01ef"},"d021f18758b28bda32bdaf0a987e0804cec074a9a4cfab8232ed81d96e75dfae","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","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":[[119,238],[119],[90,119,126,127,128,143],[119,127,128,144,145],[119,126,127],[119,126,143,146,149],[119,126,146,149,150],[119,147,148,149,151,152],[119,126,149],[119,126,143,146,147,148,151],[119,126,134],[119,126],[90,119,126],[79,119,126],[119,130,131,132,133,134,135,136,137,138,139,140,141,142],[119,126,132,133],[119,126,132,134],[119,197],[119,197,198,199],[63,119],[66,119],[63,66,119],[64,65,66,67,68,69,70,71,72,73,74,119,154,157,158,159,160,161,162,163,164],[57,63,64,119],[66,72,74,119,153],[119,156],[66,67,119],[63,119,160],[119,192,193],[119,238,239,240,241,242],[119,238,240],[119,155],[119,245,246,247],[91,119,126],[119,250],[119,251],[119,262],[119,256,261],[119,265,267,268,269,270,271,272,273,274,275,276,277],[119,265,266,268,269,270,271,272,273,274,275,276,277],[119,266,267,268,269,270,271,272,273,274,275,276,277],[119,265,266,267,269,270,271,272,273,274,275,276,277],[119,265,266,267,268,270,271,272,273,274,275,276,277],[119,265,266,267,268,269,271,272,273,274,275,276,277],[119,265,266,267,268,269,270,272,273,274,275,276,277],[119,265,266,267,268,269,270,271,273,274,275,276,277],[119,265,266,267,268,269,270,271,272,274,275,276,277],[119,265,266,267,268,269,270,271,272,273,275,276,277],[119,265,266,267,268,269,270,271,272,273,274,276,277],[119,265,266,267,268,269,270,271,272,273,274,275,277],[119,265,266,267,268,269,270,271,272,273,274,275,276],[75,119],[78,119],[79,84,110,119],[80,90,91,98,107,118,119],[80,81,90,98,119],[82,119],[83,84,91,99,119],[84,107,115,119],[85,87,90,98,119],[86,119],[87,88,119],[89,90,119],[90,119],[90,91,92,107,118,119],[90,91,92,107,119],[93,98,107,118,119],[90,91,93,94,98,107,115,118,119],[93,95,107,115,118,119],[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,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125],[90,96,119],[97,118,119,123],[87,90,98,107,119],[99,119],[100,119],[78,101,119],[102,117,119,123],[103,119],[104,119],[90,105,119],[105,106,119,121],[79,90,107,108,109,119],[79,107,109,119],[107,108,119],[110,119],[111,119],[90,113,114,119],[113,114,119],[84,98,115,119],[116,119],[98,117,119],[79,93,104,118,119],[84,119],[107,119,120],[119,121],[119,122],[79,84,90,92,101,107,118,119,121,123],[107,119,124],[119,126,283],[119,286,325],[119,286,310,325],[119,325],[119,286],[119,286,311,325],[119,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,321,322,323,324],[119,311,325],[119,326],[119,329],[119,202],[119,202,213,214],[119,214,215,216],[119,177],[119,177,178,179,180,181],[119,166,167,168,169,170,171,172,173,174,175,176],[119,254,257],[119,254,257,258,259],[119,256],[119,253,260],[119,255],[56,58,59,60,61,62,119],[56,57,119],[58,119],[57,58,119],[56,58,119],[119,165,182,183,184],[119,183],[119,184],[55,119,183,184,185],[119,187],[119,187,188,191,195],[119,194],[119,165,189,190],[119,210,211,212],[119,209,210],[119,165,209,210],[119,165,202,209],[119,165,186,189,196,222,226,227,228,234,235],[119,236],[119,190,196],[50,119,189,196,229,231,233,236],[50,119,190,196,229,230,236],[119,189,196,232],[119,190],[119,189,190,196,236],[119,165,202],[119,165,203],[119,203,204,205,206,207,208],[119,165,186,189,196,200,201,218,219],[119,218],[119,201,218,220,221],[119,165,196,213,217],[119,165,222],[119,165,186,222,223],[119,126,165,186,222,223],[119,223,224,225],[165,186,222,226],[236],[190],[189,236],[229,236],[189,232]],"referencedMap":[[240,1],[238,2],[144,3],[127,2],[146,4],[128,5],[145,2],[150,6],[151,7],[147,7],[153,8],[148,7],[152,9],[149,10],[135,11],[132,12],[139,13],[133,11],[130,14],[138,2],[143,15],[140,2],[141,2],[142,2],[137,12],[134,16],[131,2],[136,17],[189,2],[202,13],[198,18],[199,18],[200,19],[197,2],[64,20],[65,20],[67,21],[68,20],[69,20],[70,22],[71,2],[72,2],[73,2],[66,20],[165,23],[74,24],[154,25],[157,26],[158,2],[159,2],[160,2],[161,2],[162,2],[163,27],[164,28],[192,2],[194,29],[193,2],[243,30],[239,1],[241,31],[242,1],[190,12],[156,32],[244,2],[245,2],[248,33],[246,2],[249,34],[250,2],[251,35],[252,36],[263,37],[262,38],[247,2],[264,2],[266,39],[267,40],[265,41],[268,42],[269,43],[270,44],[271,45],[272,46],[273,47],[274,48],[275,49],[276,50],[277,51],[278,2],[155,2],[75,52],[76,52],[78,53],[79,54],[80,55],[81,56],[82,57],[83,58],[84,59],[85,60],[86,61],[87,62],[88,62],[89,63],[90,64],[91,65],[92,66],[77,2],[125,2],[93,67],[94,68],[95,69],[126,70],[96,71],[97,72],[98,73],[99,74],[100,75],[101,76],[102,77],[103,78],[104,79],[105,80],[106,81],[107,82],[109,83],[108,84],[110,85],[111,86],[112,2],[113,87],[114,88],[115,89],[116,90],[117,91],[118,92],[119,93],[120,94],[121,95],[122,96],[123,97],[124,98],[279,2],[280,12],[281,2],[282,2],[284,99],[283,2],[285,12],[310,100],[311,101],[286,102],[289,102],[308,100],[309,100],[299,100],[298,103],[296,100],[291,100],[304,100],[302,100],[306,100],[290,100],[303,100],[307,100],[292,100],[293,100],[305,100],[287,100],[294,100],[295,100],[297,100],[301,100],[312,104],[300,100],[288,100],[325,105],[324,2],[319,104],[321,106],[320,104],[313,104],[314,104],[316,104],[318,104],[322,106],[323,106],[315,106],[317,106],[327,107],[326,2],[328,2],[227,2],[329,2],[330,108],[129,2],[253,2],[214,109],[215,110],[216,110],[217,111],[176,2],[173,112],[175,112],[174,112],[172,112],[182,113],[177,114],[181,2],[178,2],[180,2],[179,2],[168,112],[169,112],[170,112],[166,2],[167,2],[171,112],[254,2],[258,115],[260,116],[259,115],[257,117],[261,118],[256,119],[255,2],[56,2],[63,120],[58,121],[59,122],[60,122],[61,123],[62,123],[57,124],[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],[30,2],[31,2],[32,2],[33,2],[7,2],[34,2],[39,2],[40,2],[35,2],[36,2],[37,2],[38,2],[1,2],[41,2],[55,2],[185,125],[184,126],[183,127],[186,128],[188,129],[196,130],[195,131],[187,2],[191,132],[213,133],[211,134],[212,135],[210,136],[236,137],[228,138],[229,139],[234,140],[231,141],[233,142],[230,143],[232,143],[235,144],[237,138],[203,145],[204,146],[205,146],[206,2],[207,146],[209,147],[208,146],[220,148],[201,2],[219,149],[221,149],[222,150],[218,151],[223,152],[224,153],[225,154],[226,155],[46,2],[47,2],[48,2],[49,2],[50,2],[51,2],[42,2],[52,2],[53,2],[54,2],[43,2],[44,2],[45,2]],"exportedModulesMap":[[240,1],[238,2],[144,3],[127,2],[146,4],[128,5],[145,2],[150,6],[151,7],[147,7],[153,8],[148,7],[152,9],[149,10],[135,11],[132,12],[139,13],[133,11],[130,14],[138,2],[143,15],[140,2],[141,2],[142,2],[137,12],[134,16],[131,2],[136,17],[189,2],[202,13],[198,18],[199,18],[200,19],[197,2],[64,20],[65,20],[67,21],[68,20],[69,20],[70,22],[71,2],[72,2],[73,2],[66,20],[165,23],[74,24],[154,25],[157,26],[158,2],[159,2],[160,2],[161,2],[162,2],[163,27],[164,28],[192,2],[194,29],[193,2],[243,30],[239,1],[241,31],[242,1],[190,12],[156,32],[244,2],[245,2],[248,33],[246,2],[249,34],[250,2],[251,35],[252,36],[263,37],[262,38],[247,2],[264,2],[266,39],[267,40],[265,41],[268,42],[269,43],[270,44],[271,45],[272,46],[273,47],[274,48],[275,49],[276,50],[277,51],[278,2],[155,2],[75,52],[76,52],[78,53],[79,54],[80,55],[81,56],[82,57],[83,58],[84,59],[85,60],[86,61],[87,62],[88,62],[89,63],[90,64],[91,65],[92,66],[77,2],[125,2],[93,67],[94,68],[95,69],[126,70],[96,71],[97,72],[98,73],[99,74],[100,75],[101,76],[102,77],[103,78],[104,79],[105,80],[106,81],[107,82],[109,83],[108,84],[110,85],[111,86],[112,2],[113,87],[114,88],[115,89],[116,90],[117,91],[118,92],[119,93],[120,94],[121,95],[122,96],[123,97],[124,98],[279,2],[280,12],[281,2],[282,2],[284,99],[283,2],[285,12],[310,100],[311,101],[286,102],[289,102],[308,100],[309,100],[299,100],[298,103],[296,100],[291,100],[304,100],[302,100],[306,100],[290,100],[303,100],[307,100],[292,100],[293,100],[305,100],[287,100],[294,100],[295,100],[297,100],[301,100],[312,104],[300,100],[288,100],[325,105],[324,2],[319,104],[321,106],[320,104],[313,104],[314,104],[316,104],[318,104],[322,106],[323,106],[315,106],[317,106],[327,107],[326,2],[328,2],[227,2],[329,2],[330,108],[129,2],[253,2],[214,109],[215,110],[216,110],[217,111],[176,2],[173,112],[175,112],[174,112],[172,112],[182,113],[177,114],[181,2],[178,2],[180,2],[179,2],[168,112],[169,112],[170,112],[166,2],[167,2],[171,112],[254,2],[258,115],[260,116],[259,115],[257,117],[261,118],[256,119],[255,2],[56,2],[63,120],[58,121],[59,122],[60,122],[61,123],[62,123],[57,124],[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],[30,2],[31,2],[32,2],[33,2],[7,2],[34,2],[39,2],[40,2],[35,2],[36,2],[37,2],[38,2],[1,2],[41,2],[55,2],[185,125],[184,126],[183,127],[186,128],[188,129],[196,130],[195,131],[187,2],[191,132],[213,133],[211,134],[212,135],[210,136],[236,156],[228,157],[229,158],[234,159],[231,160],[233,161],[230,158],[232,158],[235,159],[237,138],[203,145],[204,146],[205,146],[206,2],[207,146],[209,147],[208,146],[220,148],[201,2],[219,149],[221,149],[222,150],[218,151],[223,152],[224,153],[225,154],[226,155],[46,2],[47,2],[48,2],[49,2],[50,2],[51,2],[42,2],[52,2],[53,2],[54,2],[43,2],[44,2],[45,2]],"semanticDiagnosticsPerFile":[240,238,144,127,146,128,145,150,151,147,153,148,152,149,135,132,139,133,130,138,143,140,141,142,137,134,131,136,189,202,198,199,200,197,64,65,67,68,69,70,71,72,73,66,165,74,154,157,158,159,160,161,162,163,164,192,194,193,243,239,241,242,190,156,244,245,248,246,249,250,251,252,263,262,247,264,266,267,265,268,269,270,271,272,273,274,275,276,277,278,155,75,76,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,77,125,93,94,95,126,96,97,98,99,100,101,102,103,104,105,106,107,109,108,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,279,280,281,282,284,283,285,310,311,286,289,308,309,299,298,296,291,304,302,306,290,303,307,292,293,305,287,294,295,297,301,312,300,288,325,324,319,321,320,313,314,316,318,322,323,315,317,327,326,328,227,329,330,129,253,214,215,216,217,176,173,175,174,172,182,177,181,178,180,179,168,169,170,166,167,171,254,258,260,259,257,261,256,255,56,63,58,59,60,61,62,57,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,30,31,32,33,7,34,39,40,35,36,37,38,1,41,55,185,184,183,186,188,196,195,187,191,213,211,212,210,236,228,229,234,231,233,230,232,235,237,203,204,205,206,207,209,208,220,201,219,221,222,218,223,224,225,226,46,47,48,49,50,51,42,52,53,54,43,44,45],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.8.4"} -\ 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.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/ts4.8/assert.d.ts","../../../node_modules/@types/node/ts4.8/assert/strict.d.ts","../../../node_modules/@types/node/ts4.8/globals.d.ts","../../../node_modules/@types/node/ts4.8/async_hooks.d.ts","../../../node_modules/@types/node/ts4.8/buffer.d.ts","../../../node_modules/@types/node/ts4.8/child_process.d.ts","../../../node_modules/@types/node/ts4.8/cluster.d.ts","../../../node_modules/@types/node/ts4.8/console.d.ts","../../../node_modules/@types/node/ts4.8/constants.d.ts","../../../node_modules/@types/node/ts4.8/crypto.d.ts","../../../node_modules/@types/node/ts4.8/dgram.d.ts","../../../node_modules/@types/node/ts4.8/diagnostics_channel.d.ts","../../../node_modules/@types/node/ts4.8/dns.d.ts","../../../node_modules/@types/node/ts4.8/dns/promises.d.ts","../../../node_modules/@types/node/ts4.8/domain.d.ts","../../../node_modules/@types/node/ts4.8/events.d.ts","../../../node_modules/@types/node/ts4.8/fs.d.ts","../../../node_modules/@types/node/ts4.8/fs/promises.d.ts","../../../node_modules/@types/node/ts4.8/http.d.ts","../../../node_modules/@types/node/ts4.8/http2.d.ts","../../../node_modules/@types/node/ts4.8/https.d.ts","../../../node_modules/@types/node/ts4.8/inspector.d.ts","../../../node_modules/@types/node/ts4.8/module.d.ts","../../../node_modules/@types/node/ts4.8/net.d.ts","../../../node_modules/@types/node/ts4.8/os.d.ts","../../../node_modules/@types/node/ts4.8/path.d.ts","../../../node_modules/@types/node/ts4.8/perf_hooks.d.ts","../../../node_modules/@types/node/ts4.8/process.d.ts","../../../node_modules/@types/node/ts4.8/punycode.d.ts","../../../node_modules/@types/node/ts4.8/querystring.d.ts","../../../node_modules/@types/node/ts4.8/readline.d.ts","../../../node_modules/@types/node/ts4.8/repl.d.ts","../../../node_modules/@types/node/ts4.8/stream.d.ts","../../../node_modules/@types/node/ts4.8/stream/promises.d.ts","../../../node_modules/@types/node/ts4.8/stream/consumers.d.ts","../../../node_modules/@types/node/ts4.8/stream/web.d.ts","../../../node_modules/@types/node/ts4.8/string_decoder.d.ts","../../../node_modules/@types/node/ts4.8/test.d.ts","../../../node_modules/@types/node/ts4.8/timers.d.ts","../../../node_modules/@types/node/ts4.8/timers/promises.d.ts","../../../node_modules/@types/node/ts4.8/tls.d.ts","../../../node_modules/@types/node/ts4.8/trace_events.d.ts","../../../node_modules/@types/node/ts4.8/tty.d.ts","../../../node_modules/@types/node/ts4.8/url.d.ts","../../../node_modules/@types/node/ts4.8/util.d.ts","../../../node_modules/@types/node/ts4.8/v8.d.ts","../../../node_modules/@types/node/ts4.8/vm.d.ts","../../../node_modules/@types/node/ts4.8/wasi.d.ts","../../../node_modules/@types/node/ts4.8/worker_threads.d.ts","../../../node_modules/@types/node/ts4.8/zlib.d.ts","../../../node_modules/@types/node/ts4.8/globals.global.d.ts","../../../node_modules/@types/node/ts4.8/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/regex.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","../../polling-controller/dist/types/abstractpollingcontroller.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/fetchblockfeehistory.ts","../src/fetchgasestimatesviaethfeehistory/medianof.ts","../src/fetchgasestimatesviaethfeehistory/calculategasfeeestimatesforprioritylevels.ts","../src/fetchgasestimatesviaethfeehistory/types.ts","../src/fetchgasestimatesviaethfeehistory/fetchlatestblock.ts","../src/fetchgasestimatesviaethfeehistory.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":"f20c05dbfe50a208301d2a1da37b9931bce0466eb5a1f4fe240971b4ecc82b67","affectsGlobalScope":true},"dc47c4fa66b9b9890cf076304de2a9c5201e94b740cffdf09f87296d877d71f6","7a387c58583dfca701b6c85e0adaf43fb17d590fb16d5b2dc0a2fbd89f35c467","8a12173c586e95f4433e0c6dc446bc88346be73ffe9ca6eec7aa63c8f3dca7f9","5f4e733ced4e129482ae2186aae29fde948ab7182844c3a5a51dd346182c7b06","e6b724280c694a9f588847f754198fb96c43d805f065c3a5b28bbc9594541c84","1fc5ab7a764205c68fa10d381b08417795fc73111d6dd16b5b1ed36badb743d9",{"version":"9b087de7268e4efc5f215347a62656663933d63c0b1d7b624913240367b999ea","affectsGlobalScope":true},{"version":"adb996790133eb33b33aadb9c09f15c2c575e71fb57a62de8bf74dbf59ec7dfb","affectsGlobalScope":true},{"version":"8cc8c5a3bac513368b0157f3d8b31cfdcfe78b56d3724f30f80ed9715e404af8","affectsGlobalScope":true},{"version":"cdccba9a388c2ee3fd6ad4018c640a471a6c060e96f1232062223063b0a5ac6a","affectsGlobalScope":true},{"version":"c5c05907c02476e4bde6b7e76a79ffcd948aedd14b6a8f56e4674221b0417398","affectsGlobalScope":true},{"version":"0d5f52b3174bee6edb81260ebcd792692c32c81fd55499d69531496f3f2b25e7","affectsGlobalScope":true},{"version":"55f400eec64d17e888e278f4def2f254b41b89515d3b88ad75d5e05f019daddd","affectsGlobalScope":true},{"version":"181f1784c6c10b751631b24ce60c7f78b20665db4550b335be179217bacc0d5f","affectsGlobalScope":true},{"version":"3013574108c36fd3aaca79764002b3717da09725a36a6fc02eac386593110f93","affectsGlobalScope":true},{"version":"75ec0bdd727d887f1b79ed6619412ea72ba3c81d92d0787ccb64bab18d261f14","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":"09aa50414b80c023553090e2f53827f007a301bc34b0495bfb2c3c08ab9ad1eb","affectsGlobalScope":true},{"version":"d7f680a43f8cd12a6b6122c07c54ba40952b0c8aa140dcfcf32eb9e6cb028596","affectsGlobalScope":true},{"version":"3787b83e297de7c315d55d4a7c546ae28e5f6c0a361b7a1dcec1f1f50a54ef11","affectsGlobalScope":true},{"version":"e7e8e1d368290e9295ef18ca23f405cf40d5456fa9f20db6373a61ca45f75f40","affectsGlobalScope":true},{"version":"faf0221ae0465363c842ce6aa8a0cbda5d9296940a8e26c86e04cc4081eea21e","affectsGlobalScope":true},{"version":"06393d13ea207a1bfe08ec8d7be562549c5e2da8983f2ee074e00002629d1871","affectsGlobalScope":true},{"version":"775d9c9fd150d5de79e0450f35bc8b8f94ae64e3eb5da12725ff2a649dccc777","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","869b0f507115c42896d917642f821752e8a84827bfe9ed74c23d76fb0c64c681","e475453e7140e95542332943d3052fe4c7430ad1efce42b3e9157f1fee8cbc5f","ebfdf904255ce746c9d30117c2edef355fb19bf7650478d2405f39f0e4f302e6","f3f63b48addb8e2ea9d20bb671c3c306413b3daa39996d0ae52f63d8e32158e1","a50599c08934a62f11657bdbe0dc929ab66da1b1f09974408fd9a33ec1bb8060","5a20e7d6c630b91be15e9b837853173829d00273197481dc8d3e94df61105a71","8d478048d71cc16f806d4b71b252ecb67c7444ccf4f4b09b29a312712184f859","b4000a0a525fa921e896cbdb32ae802c9684f0fd371b5fc69e7310f7918cc2c3","9df4662ca3dbc2522bc115833ee04faa1afbb4e249a85ef4a0a09c621346bd08","b25d9065cf1c1f537a140bbc508e953ed2262f77134574c432d206ff36f4bdbf","1b103313097041aa9cd705a682c652f08613cb5cf8663321061c0902f845e81c","68ccec8662818911d8a12b8ed028bc5729fb4f1d34793c4701265ba60bc73cf4","5f85b8b79dc4d36af672c035b2beb71545de63a5d60bccbeee64c260941672ab","affb9dc7079c3a3522e046c5dc1325950a843b1ebd7dc0f0386aeb2397b9f0db","40fe4b689225816b31fe5794c0fbf3534568819709e40295ead998a2bc1ab237","f65b5e33b9ad545a1eebbd6afe857314725ad42aaf069913e33f928ab3e4990a","fb6f2a87beb7fb1f4c2b762d0c76a9459fc91f557231569b0ee21399e22aa13d","31c858dc85996fac4b7fa944e1016d5c72f514930a72357ab5001097bf6511c7","3de30a871b3340be8b679c52aa12f90dd1c8c60874517be58968fdbcc4d79445","6fd985bd31eaf77542625306fb0404d32bff978990f0a06428e5f0b9a3b58109","34693fb4a5e771e11668219221344dd1bd7d8b77ed005a1c1d965fb559be8406","7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419",{"version":"8c61d5fc50490af59daf69c4e601cc76de260ee5b2ff057d608a78d6acb0b61a","affectsGlobalScope":true},"f51b4042a3ac86f1f707500a9768f88d0b0c1fc3f3e45a73333283dea720cdc6",{"version":"a7289d79eb84a59d2475b4d0136b4404be3cfdd17c3ea46b9194add1d645df01","affectsGlobalScope":true},"0bb26fa2a90ee890eed57ee812c71fa84d3d07850163ec4a204de86412cc57c1","132ca47da601c60141dd6f10bd08c70d0620177e5638439df2464ec3945b6d98",{"version":"55d2bbae076fed7269c3e16faeb32f988f558427b7a1c3bf04aa7551ab86ae90","affectsGlobalScope":true},"a40826e8476694e90da94aa008283a7de50d1dafd37beada623863f1901cb7fb","543c6e3a2353e9ad08b4090e1fb88a95cefb756d0d173b6ec045d7dc70a79964","3a41ebe7f089d50f447466b35b6cabb8b584c0994fc9809d0cd0a4ebc41e1239","f5350b93f10178838c23b3a81f3791d839bc44d1a2a89edb60069250fab90899","aa07f7230bcc5733919c941753d067cb8816dcad6651edb815cb302ae8ddd931","649ba4638a25c54a18dffe37367c6b7848a0bca53fa42fbbd7300b0c61aa861c","5f20d20b7607174caf1a6da9141aeb9f2142159ae2410ca30c7a0fccd1d19c99",{"version":"a34d65f61ec5aac5b53502c8b0bd4e00d217bccb95bf94d449e2571baa11fb8c","affectsGlobalScope":true},"8d42e5af5fb0a96a77e135ce84cc60636c9bad39d9dba043a4efe9d1bdeb3cc3","56fcc451e9065eb121c9cc4c1b9994a816306f3b0b3b1fce7ad59f0ac97a9999","d4a13a5a2e6df8ef02a84ac6fd5bad4a1ec54fdef47f33250da386ea6d5c1864","c3759b5bc5cc40f5988d86a497741a80fa91258629ae50a2b3735e774cd377cc","bf268a0aea37ad4ae3b7a9b58559190b6fc01ea16a31e35cd05817a0a60f895a","45dd82fb5aea9b12b2a90b427b28f3a014e8b2ee9b74087a5ab882841cb5fbc5",{"version":"d7dad6db394a3d9f7b49755e4b610fbf8ed6eb0c9810ae5f1a119f6b5d76de45","affectsGlobalScope":true},"48b2f9302651eb31acd5be69bb4e6b35797a7fcd6b77391d10a4ccadf7dc3609","605bed8af3052e790865a35e3d538a5447764a4ff01989c0f6b084a96f40e1cb","dd67d2b5e4e8a182a38de8e69fb736945eaa4588e0909c14e01a14bd3cc1fd1e",{"version":"2db274de1088f268805043df72e21258eae845e6418dada65331d2898998f330","affectsGlobalScope":true},{"version":"2923dee3c897f03e91b54a210cdbefea7290562f0ac4b948667d4c9ee844b79e","affectsGlobalScope":true},"79169698d09a2be54b14f3bcad2575b414bf3525063fde0a1e4fcd5d6efd380e","051d939bcf77caa3cef3282708ab3a6fdfb741a7366e1d74a9e7603b67417ec3","0be79b3ff0f16b6c2f9bc8c4cc7097ea417d8d67f8267f7e1eec8e32b548c2ff","1c61ffa3a71b77363b30d19832c269ef62fba787f5610cac7254728d3b69ab2e","f6877afc4b9d0b72e15378b187492a6547e48783980e9ff1301163b7b1c15b2e","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},"e3685a8957b4e2af64c3f04a58289ee0858a649dbcd963a2b897fe85858ae18a","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","3140d587067e55ce4028275cf71b40a3fd431863ad148efc3106af84a0794cf9","47383b45796d525a4039cd22d2840ac55a1ff03a43d027f7f867ba7314a9cf53","6548773b3abbc18de29176c2141f766d4e437e40596ee480447abf83575445ad","6ddd27af0436ce59dd4c1896e2bfdb2bdb2529847d078b83ce67a144dff05491","816264799aef3fd5a09a3b6c25217d5ec26a9dfc7465eac7d6073bcdc7d88f3f","4df0891b133884cd9ed752d31c7d0ec0a09234e9ed5394abffd3c660761598db","b603b62d3dcd31ef757dc7339b4fa8acdbca318b0fb9ac485f9a1351955615f9","e642bd47b75ad6b53cbf0dfd7ddfa0f120bd10193f0c58ec37d87b59bf604aca","be90b24d2ee6f875ce3aaa482e7c41a54278856b03d04212681c4032df62baf9","78f5ff400b3cb37e7b90eef1ff311253ed31c8cb66505e9828fad099bffde021","372c47090e1131305d163469a895ff2938f33fa73aad988df31cd31743f9efb6","71c67dc6987bdbd5599353f90009ff825dd7db0450ef9a0aee5bb0c574d18512","6f12403b5eca6ae7ca8e3efe3eeb9c683b06ce3e3844ccfd04098d83cd7e4957","282c535df88175d64d9df4550d2fd1176fd940c1c6822f1e7584003237f179d3","c3a4752cf103e4c6034d5bd449c8f9d5e7b352d22a5f8f9a41a8efb11646f9c2","11a9e38611ac3c77c74240c58b6bd64a0032128b29354e999650f1de1e034b1c","4ed103ca6fff9cb244f7c4b86d1eb28ce8069c32db720784329946731badb5bb","d738f282842970e058672663311c6875482ee36607c88b98ffb6604fba99cb2a","ec859cd8226aa623e41bbb47c249a55ee16dc1b8647359585244d57d3a5ed0c7","8891c6e959d253a66434ff5dc9ae46058fb3493e84b4ca39f710ef2d350656b1","c4463cf02535444dcbc3e67ecd29f1972490f74e49957d6fd4282a1013796ba6","0cb0a957ff02de0b25fd0f3f37130ca7f22d1e0dea256569c714c1f73c6791f8","2f5075dc512d51786b1ba3b1696565641dfaae3ac854f5f13d61fa12ef81a47e","55fcfa6909a227963d5ad3d59ea9f99480106ebd9c69d87fce8a66f8f66e23c3","7f03c7ae3f6cedb6d9261d31d0fb518d940fb2f1b8d2b02b306c6d4b7e1bc8aa","45351e0d51780b6f4088277a4457b9879506ee2720a887de232df0f1efcb33d8","b59e5a5f93b7bbf729cacd0bc86106498e58f07114868350265a3de3598312c7","7c1b05262a4623e5d2a5832433582e0d6b453a3db8aa799b98f38d105812296f","6ee58aa536dabb19b09bc036f1abe83feb51e13d63b23d30b2d0631a2de99b8f","8aceb205dcc6f814ad99635baf1e40b6e01d06d3fe27b72fd766c6d0b8c0c600","530fb1c53338ac6618f8b8c4aa30acf89a9fe13b2bf92184ad966c824350d827","c1e87ae9c6982a1628be91d9f3a52ec5a73ed1a6d42e4e8119bf96c453b14cc0","161a4d1342b99b095a9531e65ba773ab33df0d906d7ae9f4f39180320fe23237","722cf9592e0d42cb903e40aa725f3e8e7cde91271f6be8c318bfbd7e337ff886","6a6aaf58d8ccff1409f921c37d0eb6cec3113df483c485385af3d90414e8eb07","ac9b69620b356f5bcfbc17d6a2a0591354eade1c5febb05cb1079b30ea0094c6","8e7adb22c0adecf7464861fc58ae3fc617b41ffbd70c97aa8493dc0966a82273","755f3cd1d9c1b564cff090e3b0e29200ae55690a91b87cb9e7a64c2dbeb314d3","d6bb7e0a6877b7856c183bff13d09dd9ae599ea43c6f6b33d3d5f72a830ed460","f1b51ae93c762d7c43f559933cd4842dd870367e8d92e90704ffa685dd5b29a3","3f450762fd7c34ed545e738abccb0af6a703572a10521643cf8fc88e3724c99c","fcc8beef29f39f09b1d9c9f99c42f9fed605ab1c28d2a630185f732b9ba53763","8b497c8cdd875848164f60712378fb15fbc2d625b67d29285845a51fcca57aff","0be91c7eb27de7e2b84c2caa3f89ac2c314de7e00d142c01b3baa0c88163bba4","0a0658c71cfa72984205a2f33b1e28e5e5fdbce0e4fb88186aed4e5a658065dc","cb047832dc68f5a2c41c62c5e95ddcacbae3a8b034d40cd15319a8cb7f25104a","980336ccdfc3c08f3c3b201aa6662e6016e20f15847f8465b68f3e8e67b4665c","5a3493939995f46ff3d9073cd534fb8961c3bf4e08c71db27066ff03d906dea8","bb5a2ac327605ebebf831c469b05bd34a33a6a46ee8c1edd9f3310aad32cf6a1","bf5d041f2440b4a9391e2b5eb3b8d94cbf1e3b8ff4703b6539d4e65e758c8f37","8516469eb90e723b0eb03df1be098f7e6a4709f6f48fd4532868d20a0a934f6e","d60e9ab369a72d234aac49adbe2900d8ef1408a6ea4db552cf2a48c9d8d6a1bc","0ebb4698803f01e2e7df6acce572fff068f4a20c47221721dafd70a27e372831","a12eaa942232703a8a8477a2f240ad5a2c26c595012ea8f128224e77984099c4","4070c2f1c3434fcf84886e04d30d82cd650ee443e53b82b404b144175cf8741e","2cea9689efa8591732096235abe7f084fc29c92badd5b0897a5e876b77e71887","4ed4e504126014fee13aaef5e3fc140f2ff7031ff3a8b5386717905820ea2d09","b0a1e66ed5a0e5dbcd308bccc2e8ba2aac2e17306a3e6888f74bc450c9806a9e","e09f372cbf6582b5b271ef8de9a896d7785aa353dd5f9c574714dcd41636462a","8e13478e90645f18e32db1e7d82729f9b8adf2cfe9e8bb445e6d0c3f8249aebd","d95e9b84527c1091bbccde1a213d987e3eb3c1ba0de7bfb5ee710f45107b5734","12c89d0e32758c120a569045f21cf5b77244f86792611ced8de7f86b37e77781","ccccd5387e496a2899d50ed492c6120c83b7fdfe5d4fe79c4275a79b3ff7b936","751eff4da87d06979e2a4d1d99a9bc98abf04ed569c5871c97e568a83cfff25e","b121f7725eb885a38942124e6d87ef86d650b5221368f49d0b332695d53d0c34","1ff59fddc2d63283af44fd820daff74947a3282bf11bdfb014098aed1a3a51ac","fab58e600970e66547644a44bc9918e3223aa2cbd9e8763cec004b2cfb48827e",{"version":"e9aa2410e8871df21f0d81fa3afaa114c3092b3bacb25765e3e955697df8cfb8","signature":"7359fe30980ad122c41f840074f7881b195cb703d609a117c8dff7f673c4cf00"},{"version":"a8087c3721ee3a254e604071e4cb70469c142850ea3f3583868a8c05681ce026","signature":"75d8a7ffa7462afb0f4920188068b6b59a1ae85f59b7dad26e8942a7b77ccdda"},{"version":"52df1d99c73edcf21259dca5ba24f1d7643cf3a4014711b47df18e6a02c8cf19","signature":"5d15fe3a984cdba90e92274faeae015371a0ba64d7d443b178e77f701111afe9"},{"version":"9c4834280d7503d03fa8014183f30846170636baab6d264341d2b87d417e418c","signature":"63650a6d250c5f2f5e0e45e99113789dd65481a6605956643f765b0114cc2ce0"},{"version":"4270f9cfd62abd8ff10374a5acec0c2649e1d4f31300fd768c4933a58643dbb5","signature":"798d424b753359c997dcffdfc70e5636749621a687fc1125d444f86a5d22a93e"},{"version":"fd76e840ad3b3f7e9bd6d25b9485dc75a68b210c80f0ee034d4e27775114ba5f","signature":"d18efa93b91f45a1f26b97a23ae82670d8a80a91401f929826c4b5836973801e"},{"version":"b7ece87fafd76aebf93a0b670486851917d4d9ae981486286c231d6e6dc05428","signature":"67590c11008841bf8544396a16000a10b5687394d7caf5d2b5f60f8de9fac20b"},{"version":"15433a2e97dcee760d97f8e56afe908fe932611ef00711aed9b246ba058ce0be","signature":"c5fc52cba80e6ba6fde33b0f9370b8ba6866831070cbc84b20a7c6e9e1163160"},{"version":"6e43bf5325bc6ef67cdfb48f74a77bcf99768c1230067f22c88eb7c1a7cd1784","signature":"c0e1493caaab115c737b9ef6f4d37b7d2962a1bf16a18b602f882c9a0a4b01ef"},"d021f18758b28bda32bdaf0a987e0804cec074a9a4cfab8232ed81d96e75dfae","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","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":[[119,238],[119],[90,119,126,127,128,143],[119,127,128,144,145],[119,126,127],[119,126,143,146,149],[119,126,146,149,150],[119,147,148,149,151,152],[119,126,149],[119,126,143,146,147,148,151],[119,126,134],[119,126],[90,119,126],[79,119,126],[119,130,131,132,133,134,135,136,137,138,139,140,141,142],[119,126,132,133],[119,126,132,134],[119,197],[119,197,198,199],[63,119],[66,119],[63,66,119],[64,65,66,67,68,69,70,71,72,73,74,119,154,157,158,159,160,161,162,163,164],[57,63,64,119],[66,72,74,119,153],[119,156],[66,67,119],[63,119,160],[119,192,193],[119,238,239,240,241,242],[119,238,240],[119,155],[119,245,246,247],[91,119,126],[119,250],[119,251],[119,262],[119,256,261],[119,265,267,268,269,270,271,272,273,274,275,276,277],[119,265,266,268,269,270,271,272,273,274,275,276,277],[119,266,267,268,269,270,271,272,273,274,275,276,277],[119,265,266,267,269,270,271,272,273,274,275,276,277],[119,265,266,267,268,270,271,272,273,274,275,276,277],[119,265,266,267,268,269,271,272,273,274,275,276,277],[119,265,266,267,268,269,270,272,273,274,275,276,277],[119,265,266,267,268,269,270,271,273,274,275,276,277],[119,265,266,267,268,269,270,271,272,274,275,276,277],[119,265,266,267,268,269,270,271,272,273,275,276,277],[119,265,266,267,268,269,270,271,272,273,274,276,277],[119,265,266,267,268,269,270,271,272,273,274,275,277],[119,265,266,267,268,269,270,271,272,273,274,275,276],[75,119],[78,119],[79,84,110,119],[80,90,91,98,107,118,119],[80,81,90,98,119],[82,119],[83,84,91,99,119],[84,107,115,119],[85,87,90,98,119],[86,119],[87,88,119],[89,90,119],[90,119],[90,91,92,107,118,119],[90,91,92,107,119],[93,98,107,118,119],[90,91,93,94,98,107,115,118,119],[93,95,107,115,118,119],[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,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125],[90,96,119],[97,118,119,123],[87,90,98,107,119],[99,119],[100,119],[78,101,119],[102,117,119,123],[103,119],[104,119],[90,105,119],[105,106,119,121],[79,90,107,108,109,119],[79,107,109,119],[107,108,119],[110,119],[111,119],[90,113,114,119],[113,114,119],[84,98,115,119],[116,119],[98,117,119],[79,93,104,118,119],[84,119],[107,119,120],[119,121],[119,122],[79,84,90,92,101,107,118,119,121,123],[107,119,124],[119,126,283],[119,286,325],[119,286,310,325],[119,325],[119,286],[119,286,311,325],[119,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,321,322,323,324],[119,311,325],[119,326],[119,329],[119,202],[119,214,215,216],[119,202,213,214],[119,177],[119,177,178,179,180,181],[119,166,167,168,169,170,171,172,173,174,175,176],[119,254,257],[119,254,257,258,259],[119,256],[119,253,260],[119,255],[56,58,59,60,61,62,119],[56,57,119],[58,119],[57,58,119],[56,58,119],[119,165,182,183,184],[119,183],[55,119,183,184,185],[119,184],[119,187],[119,187,188,191,195],[119,194],[119,165,189,190],[119,210,211,212],[119,209,210],[119,165,209,210],[119,165,202,209],[119,236],[119,190,196],[50,119,189,196,229,231,233,236],[50,119,190,196,229,230,236],[119,189,196,232],[119,190],[119,189,190,196,236],[119,165,186,189,196,222,226,227,228,234,235],[119,165,203],[119,203,204,205,206,207,208],[119,165,202],[119,218],[119,201,218,220,221],[119,165,186,189,196,200,201,218,219],[119,165,196,213,217],[119,165,222],[119,165,186,222,223],[119,223,224,225],[119,126,165,186,222,223],[236],[190],[189,236],[229,236],[189,232],[165,186,222,226]],"referencedMap":[[240,1],[238,2],[144,3],[127,2],[146,4],[128,5],[145,2],[150,6],[151,7],[147,7],[153,8],[148,7],[152,9],[149,10],[135,11],[132,12],[139,13],[133,11],[130,14],[138,2],[143,15],[140,2],[141,2],[142,2],[137,12],[134,16],[131,2],[136,17],[189,2],[202,13],[198,18],[199,18],[200,19],[197,2],[64,20],[65,20],[67,21],[68,20],[69,20],[70,22],[71,2],[72,2],[73,2],[66,20],[165,23],[74,24],[154,25],[157,26],[158,2],[159,2],[160,2],[161,2],[162,2],[163,27],[164,28],[192,2],[194,29],[193,2],[243,30],[239,1],[241,31],[242,1],[190,12],[156,32],[244,2],[245,2],[248,33],[246,2],[249,34],[250,2],[251,35],[252,36],[263,37],[262,38],[247,2],[264,2],[266,39],[267,40],[265,41],[268,42],[269,43],[270,44],[271,45],[272,46],[273,47],[274,48],[275,49],[276,50],[277,51],[278,2],[155,2],[75,52],[76,52],[78,53],[79,54],[80,55],[81,56],[82,57],[83,58],[84,59],[85,60],[86,61],[87,62],[88,62],[89,63],[90,64],[91,65],[92,66],[77,2],[125,2],[93,67],[94,68],[95,69],[126,70],[96,71],[97,72],[98,73],[99,74],[100,75],[101,76],[102,77],[103,78],[104,79],[105,80],[106,81],[107,82],[109,83],[108,84],[110,85],[111,86],[112,2],[113,87],[114,88],[115,89],[116,90],[117,91],[118,92],[119,93],[120,94],[121,95],[122,96],[123,97],[124,98],[279,2],[280,12],[281,2],[282,2],[284,99],[283,2],[285,12],[310,100],[311,101],[286,102],[289,102],[308,100],[309,100],[299,100],[298,103],[296,100],[291,100],[304,100],[302,100],[306,100],[290,100],[303,100],[307,100],[292,100],[293,100],[305,100],[287,100],[294,100],[295,100],[297,100],[301,100],[312,104],[300,100],[288,100],[325,105],[324,2],[319,104],[321,106],[320,104],[313,104],[314,104],[316,104],[318,104],[322,106],[323,106],[315,106],[317,106],[327,107],[326,2],[328,2],[227,2],[329,2],[330,108],[129,2],[253,2],[214,109],[217,110],[215,111],[216,111],[176,2],[173,112],[175,112],[174,112],[172,112],[182,113],[177,114],[181,2],[178,2],[180,2],[179,2],[168,112],[169,112],[170,112],[166,2],[167,2],[171,112],[254,2],[258,115],[260,116],[259,115],[257,117],[261,118],[256,119],[255,2],[56,2],[63,120],[58,121],[59,122],[60,122],[61,123],[62,123],[57,124],[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],[30,2],[31,2],[32,2],[33,2],[7,2],[34,2],[39,2],[40,2],[35,2],[36,2],[37,2],[38,2],[1,2],[41,2],[55,2],[185,125],[184,126],[186,127],[183,128],[188,129],[196,130],[195,131],[187,2],[191,132],[213,133],[211,134],[212,135],[210,136],[228,137],[229,138],[234,139],[231,140],[233,141],[230,142],[232,142],[235,143],[236,144],[237,137],[204,145],[205,145],[206,2],[207,145],[209,146],[203,147],[208,145],[201,2],[219,148],[221,148],[222,149],[220,150],[218,151],[223,152],[224,153],[226,154],[225,155],[46,2],[47,2],[48,2],[49,2],[50,2],[51,2],[42,2],[52,2],[53,2],[54,2],[43,2],[44,2],[45,2]],"exportedModulesMap":[[240,1],[238,2],[144,3],[127,2],[146,4],[128,5],[145,2],[150,6],[151,7],[147,7],[153,8],[148,7],[152,9],[149,10],[135,11],[132,12],[139,13],[133,11],[130,14],[138,2],[143,15],[140,2],[141,2],[142,2],[137,12],[134,16],[131,2],[136,17],[189,2],[202,13],[198,18],[199,18],[200,19],[197,2],[64,20],[65,20],[67,21],[68,20],[69,20],[70,22],[71,2],[72,2],[73,2],[66,20],[165,23],[74,24],[154,25],[157,26],[158,2],[159,2],[160,2],[161,2],[162,2],[163,27],[164,28],[192,2],[194,29],[193,2],[243,30],[239,1],[241,31],[242,1],[190,12],[156,32],[244,2],[245,2],[248,33],[246,2],[249,34],[250,2],[251,35],[252,36],[263,37],[262,38],[247,2],[264,2],[266,39],[267,40],[265,41],[268,42],[269,43],[270,44],[271,45],[272,46],[273,47],[274,48],[275,49],[276,50],[277,51],[278,2],[155,2],[75,52],[76,52],[78,53],[79,54],[80,55],[81,56],[82,57],[83,58],[84,59],[85,60],[86,61],[87,62],[88,62],[89,63],[90,64],[91,65],[92,66],[77,2],[125,2],[93,67],[94,68],[95,69],[126,70],[96,71],[97,72],[98,73],[99,74],[100,75],[101,76],[102,77],[103,78],[104,79],[105,80],[106,81],[107,82],[109,83],[108,84],[110,85],[111,86],[112,2],[113,87],[114,88],[115,89],[116,90],[117,91],[118,92],[119,93],[120,94],[121,95],[122,96],[123,97],[124,98],[279,2],[280,12],[281,2],[282,2],[284,99],[283,2],[285,12],[310,100],[311,101],[286,102],[289,102],[308,100],[309,100],[299,100],[298,103],[296,100],[291,100],[304,100],[302,100],[306,100],[290,100],[303,100],[307,100],[292,100],[293,100],[305,100],[287,100],[294,100],[295,100],[297,100],[301,100],[312,104],[300,100],[288,100],[325,105],[324,2],[319,104],[321,106],[320,104],[313,104],[314,104],[316,104],[318,104],[322,106],[323,106],[315,106],[317,106],[327,107],[326,2],[328,2],[227,2],[329,2],[330,108],[129,2],[253,2],[214,109],[217,110],[215,111],[216,111],[176,2],[173,112],[175,112],[174,112],[172,112],[182,113],[177,114],[181,2],[178,2],[180,2],[179,2],[168,112],[169,112],[170,112],[166,2],[167,2],[171,112],[254,2],[258,115],[260,116],[259,115],[257,117],[261,118],[256,119],[255,2],[56,2],[63,120],[58,121],[59,122],[60,122],[61,123],[62,123],[57,124],[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],[30,2],[31,2],[32,2],[33,2],[7,2],[34,2],[39,2],[40,2],[35,2],[36,2],[37,2],[38,2],[1,2],[41,2],[55,2],[185,125],[184,126],[186,127],[183,128],[188,129],[196,130],[195,131],[187,2],[191,132],[213,133],[211,134],[212,135],[210,136],[228,156],[229,157],[234,158],[231,159],[233,160],[230,157],[232,157],[235,158],[236,161],[237,137],[204,145],[205,145],[206,2],[207,145],[209,146],[203,147],[208,145],[201,2],[219,148],[221,148],[222,149],[220,150],[218,151],[223,152],[224,153],[226,154],[225,155],[46,2],[47,2],[48,2],[49,2],[50,2],[51,2],[42,2],[52,2],[53,2],[54,2],[43,2],[44,2],[45,2]],"semanticDiagnosticsPerFile":[240,238,144,127,146,128,145,150,151,147,153,148,152,149,135,132,139,133,130,138,143,140,141,142,137,134,131,136,189,202,198,199,200,197,64,65,67,68,69,70,71,72,73,66,165,74,154,157,158,159,160,161,162,163,164,192,194,193,243,239,241,242,190,156,244,245,248,246,249,250,251,252,263,262,247,264,266,267,265,268,269,270,271,272,273,274,275,276,277,278,155,75,76,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,77,125,93,94,95,126,96,97,98,99,100,101,102,103,104,105,106,107,109,108,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,279,280,281,282,284,283,285,310,311,286,289,308,309,299,298,296,291,304,302,306,290,303,307,292,293,305,287,294,295,297,301,312,300,288,325,324,319,321,320,313,314,316,318,322,323,315,317,327,326,328,227,329,330,129,253,214,217,215,216,176,173,175,174,172,182,177,181,178,180,179,168,169,170,166,167,171,254,258,260,259,257,261,256,255,56,63,58,59,60,61,62,57,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,30,31,32,33,7,34,39,40,35,36,37,38,1,41,55,185,184,186,183,188,196,195,187,191,213,211,212,210,228,229,234,231,233,230,232,235,236,237,204,205,206,207,209,203,208,201,219,221,222,220,218,223,224,226,225,46,47,48,49,50,51,42,52,53,54,43,44,45],"latestChangedDtsFile":"./types/index.d.ts"},"version":"4.8.4"} -\ No newline at end of file -diff --git a/dist/types/GasFeeController.d.ts.map b/dist/types/GasFeeController.d.ts.map -index 84faaebaed6577287e57e16b6cb73a0809c3e90c..b8340ef0ce941a3108cfbe3cc34e5b9ff9cf609e 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;AAY3C,eAAO,MAAM,yBAAyB,kDAAkD,CAAC;AAEzF,oBAAY,aAAa,GAAG,SAAS,CAAC;AAItC,oBAAY,qBAAqB,GAAG,YAAY,CAAC;AAIjD,oBAAY,kBAAkB,GAAG,QAAQ,CAAC;AAK1C,oBAAY,uBAAuB,GAAG,cAAc,CAAC;AAGrD,oBAAY,cAAc,GAAG,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC;AAEF,oBAAY,eAAe,GACvB,qBAAqB,GACrB,uBAAuB,GACvB,kBAAkB,GAClB,cAAc,CAAC;AAEnB,oBAAY,yBAAyB,GAAG;IACtC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AAEH,oBAAY,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,oBAAY,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;GAQG;AACH,oBAAY,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,oBAAY,eAAe,GAAG,sBAAsB,GAAG,uBAAuB,CAAC;AAE/E,aAAK,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,aAAK,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;AAYF,oBAAY,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,oBAAY,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,oBAAY,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,oBAAY,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,oBAAY,0BAA0B,GAAG;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,oBAAY,sBAAsB,GAC9B,sBAAsB,GACtB,oBAAoB,GACpB,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B,oBAAY,wBAAwB,GAAG;IACrC,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CACnE,CAAC;AAEF,oBAAY,WAAW,GAAG,wBAAwB,GAAG,sBAAsB,CAAC;AAE5E,QAAA,MAAM,IAAI,qBAAqB,CAAC;AAEhC,oBAAY,iBAAiB,GAAG,0BAA0B,CACxD,OAAO,IAAI,EACX,WAAW,CACZ,CAAC;AAEF,oBAAY,cAAc,GAAG,wBAAwB,CAAC,OAAO,IAAI,EAAE,WAAW,CAAC,CAAC;AAEhF,oBAAY,uBAAuB,GAAG,cAAc,CAAC;AAErD,oBAAY,sBAAsB,GAAG,iBAAiB,CAAC;AAEvD,aAAK,cAAc,GACf,+BAA+B,GAC/B,2CAA2C,GAC3C,8CAA8C,CAAC;AAEnD,aAAK,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;AASF;;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,cAAc,CAAC;IAEvB,OAAO,CAAC,QAAQ,CAAC,CAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IAInC;;;;;;;;;;;;;;;;;;;;;;OAsBG;gBACS,EACV,QAAgB,EAChB,SAAS,EACT,KAAK,EACL,qCAAqC,EACrC,qCAAqC,EACrC,UAAU,EACV,0CAA0C,EAC1C,WAAW,EACX,kBAAkB,EAClB,iBAA6C,EAC7C,kBAAkB,EAClB,QAAQ,GACT,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,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB;IAyCK,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;IAkFvB;;;;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;CAwBrD;AAED,eAAe,gBAAgB,CAAC"} -\ No newline at end of file -+{"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;AAY3C,eAAO,MAAM,yBAAyB,kDAAkD,CAAC;AAEzF,oBAAY,aAAa,GAAG,SAAS,CAAC;AAItC,oBAAY,qBAAqB,GAAG,YAAY,CAAC;AAIjD,oBAAY,kBAAkB,GAAG,QAAQ,CAAC;AAK1C,oBAAY,uBAAuB,GAAG,cAAc,CAAC;AAGrD,oBAAY,cAAc,GAAG,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB;;;;;CAK9B,CAAC;AAEF,oBAAY,eAAe,GACvB,qBAAqB,GACrB,uBAAuB,GACvB,kBAAkB,GAClB,cAAc,CAAC;AAEnB,oBAAY,yBAAyB,GAAG;IACtC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,aAAa,CAAC;CACxC,CAAC;AAEF;;;;;;;GAOG;AAEH,oBAAY,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;;;GASG;AACH,oBAAY,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;GAQG;AACH,oBAAY,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,oBAAY,eAAe,GAAG,sBAAsB,GAAG,uBAAuB,CAAC;AAE/E,aAAK,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,aAAK,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;AAYF,oBAAY,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,oBAAY,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,oBAAY,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,oBAAY,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,oBAAY,0BAA0B,GAAG;IACvC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC,CAAC;AAEF;;;;;;GAMG;AACH,oBAAY,sBAAsB,GAC9B,sBAAsB,GACtB,oBAAoB,GACpB,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B,oBAAY,wBAAwB,GAAG;IACrC,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;CACnE,CAAC;AAEF,oBAAY,WAAW,GAAG,wBAAwB,GAAG,sBAAsB,CAAC;AAE5E,QAAA,MAAM,IAAI,qBAAqB,CAAC;AAEhC,oBAAY,iBAAiB,GAAG,0BAA0B,CACxD,OAAO,IAAI,EACX,WAAW,CACZ,CAAC;AAEF,oBAAY,cAAc,GAAG,wBAAwB,CAAC,OAAO,IAAI,EAAE,WAAW,CAAC,CAAC;AAEhF,oBAAY,uBAAuB,GAAG,cAAc,CAAC;AAErD,oBAAY,sBAAsB,GAAG,iBAAiB,CAAC;AAEvD,aAAK,cAAc,GACf,+BAA+B,GAC/B,2CAA2C,GAC3C,8CAA8C,CAAC;AAEnD,aAAK,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;AASF;;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,cAAc,CAAC;IAEvB,OAAO,CAAC,QAAQ,CAAC,CAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;IAInC;;;;;;;;;;;;;;;;;;;;;;OAsBG;gBACS,EACV,QAAgB,EAChB,SAAS,EACT,KAAK,EACL,qCAAqC,EACrC,qCAAqC,EACrC,UAAU,EACV,0CAA0C,EAC1C,WAAW,EACX,kBAAkB,EAClB,iBAA6C,EAC7C,kBAAkB,EAClB,QAAQ,GACT,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,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB;IAyCK,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;IAqFvB;;;;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;CAwBrD;AAED,eAAe,gBAAgB,CAAC"} -\ No newline at end of file diff --git a/.yarn/patches/@metamask-keyring-controller-npm-13.0.0-d94816a680.patch b/.yarn/patches/@metamask-keyring-controller-npm-13.0.0-d94816a680.patch deleted file mode 100644 index 254cf55f3f5f..000000000000 --- a/.yarn/patches/@metamask-keyring-controller-npm-13.0.0-d94816a680.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/dist/KeyringController.js b/dist/KeyringController.js -index fc649ea6fc97b905d811b236de638172fb10b548..beab676ab85e5e372eda7846e98b7d34af6317f5 100644 ---- a/dist/KeyringController.js -+++ b/dist/KeyringController.js -@@ -1092,9 +1092,13 @@ _KeyringController_keyringBuilders = new WeakMap(), _KeyringController_keyrings - }, _KeyringController_addQRKeyring = function _KeyringController_addQRKeyring() { - return __awaiter(this, void 0, void 0, function* () { - // QRKeyring is not yet compatible with Keyring type from @metamask/utils -- const qrKeyring = (yield __classPrivateFieldGet(this, _KeyringController_instances, "m", _KeyringController_newKeyring).call(this, KeyringTypes.qr, { -- accounts: [], -- })); -+ /** -+ * Patch for @metamask/keyring-controller v13.0.0 -+ * Below code change will fix the issue 23804, The intial code added a empty accounts as argument when creating a new QR keyring. -+ * cause the new Keystone MetamaskKeyring default properties all are undefined during deserialise() process. -+ * Please refer to PR 23903 for detail. -+ */ -+ const qrKeyring = (yield __classPrivateFieldGet(this, _KeyringController_instances, "m", _KeyringController_newKeyring).call(this, KeyringTypes.qr)); - const accounts = yield qrKeyring.getAccounts(); - yield __classPrivateFieldGet(this, _KeyringController_instances, "m", _KeyringController_checkForDuplicate).call(this, KeyringTypes.qr, accounts); - __classPrivateFieldGet(this, _KeyringController_keyrings, "f").push(qrKeyring); diff --git a/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch b/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch new file mode 100644 index 000000000000..27d866a74888 --- /dev/null +++ b/.yarn/patches/@metamask-keyring-controller-npm-15.0.0-fa070ce311.patch @@ -0,0 +1,120 @@ +diff --git a/dist/chunk-52QZQQKP.mjs b/dist/chunk-52QZQQKP.mjs +index 934f432c8013a6af5303726e1495bed2335fa078..8de2560fe81dfc3dbfef83dd0079482306c425d9 100644 +--- a/dist/chunk-52QZQQKP.mjs ++++ b/dist/chunk-52QZQQKP.mjs +@@ -2,7 +2,8 @@ import { + __privateAdd, + __privateGet, + __privateMethod, +- __privateSet ++ __privateSet, ++ KeyringControllerError + } from "./chunk-NAAWD7HX.mjs"; + + // src/KeyringController.ts +@@ -582,6 +583,18 @@ var KeyringController = class extends BaseController { + }) + ); + serializedKeyrings.push(...__privateGet(this, _unsupportedKeyrings)); ++ /** ++ * ============================== PATCH INFORMATION ============================== ++ * The HD keyring is the default keyring for all wallets if this keyring is missing ++ * for some reason we should avoid saving the keyrings ++ * ++ * The upstream fix is here: https://github.com/MetaMask/core/pull/4168 ++ * ++ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` ++ */ ++ if (!serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)) { ++ throw new Error(KeyringControllerError.NoHdKeyring); ++ } + let vault; + let newEncryptionKey; + if (__privateGet(this, _cacheEncryptionKey)) { +@@ -1087,9 +1100,16 @@ getKeyringBuilderForType_fn = function(type) { + }; + _addQRKeyring = new WeakSet(); + addQRKeyring_fn = async function() { +- const qrKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */, { +- accounts: [] +- }); ++ /** ++ * Patch for @metamask/keyring-controller v13.0.0 ++ * Below code change will fix the issue 23804, The intial code added a empty accounts as argument when creating a new QR keyring. ++ * cause the new Keystone MetamaskKeyring default properties all are undefined during deserialise() process. ++ * Please refer to PR 23903 for detail. ++ * ++ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` ++ */ ++ // @ts-expect-error See patch note ++ const qrKeyring = await __privateMethod(this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device"); + const accounts = await qrKeyring.getAccounts(); + await __privateMethod(this, _checkForDuplicate, checkForDuplicate_fn).call(this, "QR Hardware Wallet Device" /* qr */, accounts); + __privateGet(this, _keyrings).push(qrKeyring); +diff --git a/dist/chunk-CHLPTPMZ.js b/dist/chunk-CHLPTPMZ.js +index bef1a8e9dd5efe426f8aaaba1fe4501b124f7e87..7b48c000e54708da2a689e2d6cb1b61a279f1205 100644 +--- a/dist/chunk-CHLPTPMZ.js ++++ b/dist/chunk-CHLPTPMZ.js +@@ -50,6 +50,7 @@ var KeyringControllerError = /* @__PURE__ */ ((KeyringControllerError2) => { + KeyringControllerError2["ExpiredCredentials"] = "KeyringController - Encryption key and salt provided are expired"; + KeyringControllerError2["NoKeyringBuilder"] = "KeyringController - No keyringBuilder found for keyring"; + KeyringControllerError2["DataType"] = "KeyringController - Incorrect data type provided"; ++ KeyringControllerError2["NoHdKeyring"] = "KeyringController - No HD Keyring found"; + return KeyringControllerError2; + })(KeyringControllerError || {}); + +diff --git a/dist/chunk-GXM4O6HW.js b/dist/chunk-GXM4O6HW.js +index f7539e2e6354f418cbb095cc1a2cda01a5bdeae6..978f4426536c594568ecc56f1c27881db4bfa861 100644 +--- a/dist/chunk-GXM4O6HW.js ++++ b/dist/chunk-GXM4O6HW.js +@@ -582,6 +582,18 @@ var KeyringController = class extends _basecontroller.BaseController { + }) + ); + serializedKeyrings.push(..._chunkCHLPTPMZjs.__privateGet.call(void 0, this, _unsupportedKeyrings)); ++ /** ++ * ============================== PATCH INFORMATION ============================== ++ * The HD keyring is the default keyring for all wallets if this keyring is missing ++ * for some reason we should avoid saving the keyrings ++ * ++ * The upstream fix is here: https://github.com/MetaMask/core/pull/4168 ++ * ++ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` ++ */ ++ if (!serializedKeyrings.some((keyring) => keyring.type === KeyringTypes.hd)) { ++ throw new Error(_chunkCHLPTPMZjs.KeyringControllerError.NoHdKeyring); ++ } + let vault; + let newEncryptionKey; + if (_chunkCHLPTPMZjs.__privateGet.call(void 0, this, _cacheEncryptionKey)) { +@@ -1087,9 +1099,16 @@ getKeyringBuilderForType_fn = function(type) { + }; + _addQRKeyring = new WeakSet(); + addQRKeyring_fn = async function() { +- const qrKeyring = await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device" /* qr */, { +- accounts: [] +- }); ++ /** ++ * Patch for @metamask/keyring-controller v13.0.0 ++ * Below code change will fix the issue 23804, The intial code added a empty accounts as argument when creating a new QR keyring. ++ * cause the new Keystone MetamaskKeyring default properties all are undefined during deserialise() process. ++ * Please refer to PR 23903 for detail. ++ * ++ * This patch can be found on the core branch `extension-keyring-controller-v13-patch` ++ */ ++ // @ts-expect-error See patch note ++ const qrKeyring = await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _newKeyring, newKeyring_fn).call(this, "QR Hardware Wallet Device"); + const accounts = await qrKeyring.getAccounts(); + await _chunkCHLPTPMZjs.__privateMethod.call(void 0, this, _checkForDuplicate, checkForDuplicate_fn).call(this, "QR Hardware Wallet Device" /* qr */, accounts); + _chunkCHLPTPMZjs.__privateGet.call(void 0, this, _keyrings).push(qrKeyring); +diff --git a/dist/chunk-NAAWD7HX.mjs b/dist/chunk-NAAWD7HX.mjs +index b5de23aabec9d502e8e6423480ffaaff26257bfc..a0c027a7c13828883ec5c05cbb7eab92c41f0dc3 100644 +--- a/dist/chunk-NAAWD7HX.mjs ++++ b/dist/chunk-NAAWD7HX.mjs +@@ -50,6 +50,7 @@ var KeyringControllerError = /* @__PURE__ */ ((KeyringControllerError2) => { + KeyringControllerError2["ExpiredCredentials"] = "KeyringController - Encryption key and salt provided are expired"; + KeyringControllerError2["NoKeyringBuilder"] = "KeyringController - No keyringBuilder found for keyring"; + KeyringControllerError2["DataType"] = "KeyringController - Incorrect data type provided"; ++ KeyringControllerError2["NoHdKeyring"] = "KeyringController - No HD Keyring found"; + return KeyringControllerError2; + })(KeyringControllerError || {}); + diff --git a/.yarn/patches/@metamask-network-controller-npm-18.0.1-c4d0cfaecd.patch b/.yarn/patches/@metamask-network-controller-npm-18.1.0-680908c29a.patch similarity index 60% rename from .yarn/patches/@metamask-network-controller-npm-18.0.1-c4d0cfaecd.patch rename to .yarn/patches/@metamask-network-controller-npm-18.1.0-680908c29a.patch index 9469548a639d..581382429ebb 100644 --- a/.yarn/patches/@metamask-network-controller-npm-18.0.1-c4d0cfaecd.patch +++ b/.yarn/patches/@metamask-network-controller-npm-18.1.0-680908c29a.patch @@ -1,23 +1,23 @@ -diff --git a/dist/chunk-VNCJZRDU.js b/dist/chunk-VNCJZRDU.js -index 78251fc9517438d90261ed53172d2a4133d3a5fd..3ecc0c2838ac12c0ff5b5521a82b08955287956b 100644 ---- a/dist/chunk-VNCJZRDU.js -+++ b/dist/chunk-VNCJZRDU.js -@@ -323,7 +323,6 @@ var NetworkController = class extends _basecontroller.BaseController { +diff --git a/dist/chunk-AJED3H6M.mjs b/dist/chunk-AJED3H6M.mjs +index c7f9903611ccf8f0bb01bea786177efc1a915f7f..26d559fe80f193288badd737cfb8f9d91eb3fa67 100644 +--- a/dist/chunk-AJED3H6M.mjs ++++ b/dist/chunk-AJED3H6M.mjs +@@ -341,7 +341,6 @@ var NetworkController = class extends 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); + __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/dist/chunk-XWP6GXMK.mjs b/dist/chunk-XWP6GXMK.mjs -index fb7e30d27367a38e8a357ff9e744894a4505b5a5..884bb1d124934a613847d7fd398fbc6cb02cade8 100644 ---- a/dist/chunk-XWP6GXMK.mjs -+++ b/dist/chunk-XWP6GXMK.mjs -@@ -323,7 +323,6 @@ var NetworkController = class extends BaseController { +diff --git a/dist/chunk-UEWIYOS6.js b/dist/chunk-UEWIYOS6.js +index 05e09914e085ed2fb67106bc750b8bb46882c80e..16eb520b7658625e411c9c6e35f86bd49fcce52f 100644 +--- a/dist/chunk-UEWIYOS6.js ++++ b/dist/chunk-UEWIYOS6.js +@@ -341,7 +341,6 @@ var NetworkController = class extends _basecontroller.BaseController { async initializeProvider() { - __privateMethod(this, _ensureAutoManagedNetworkClientRegistryPopulated, ensureAutoManagedNetworkClientRegistryPopulated_fn).call(this); - __privateMethod(this, _applyNetworkSelection, applyNetworkSelection_fn).call(this); + _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(); } /** diff --git a/.yarn/patches/@metamask-snaps-controllers-npm-8.0.0-7e59688855.patch b/.yarn/patches/@metamask-snaps-controllers-npm-8.0.0-7e59688855.patch new file mode 100644 index 000000000000..30a3ce9aff85 --- /dev/null +++ b/.yarn/patches/@metamask-snaps-controllers-npm-8.0.0-7e59688855.patch @@ -0,0 +1,149 @@ +diff --git a/dist/chunk-HBNET6MY.mjs b/dist/chunk-HBNET6MY.mjs +index 5843d635c91e9125d130a6ef5688550e5d514eaa..ca94b6c69ac51c1415968339fe188b2141bce044 100644 +--- a/dist/chunk-HBNET6MY.mjs ++++ b/dist/chunk-HBNET6MY.mjs +@@ -117,7 +117,7 @@ function truncateSnap(snap) { + return truncatedSnap; + } + var name = "SnapController"; +-var _closeAllConnections, _dynamicPermissions, _environmentEndowmentPermissions, _excludedPermissions, _featureFlags, _fetchFunction, _idleTimeCheckInterval, _maxIdleTime, _encryptor, _getMnemonic, _detectSnapLocation, _snapsRuntimeData, _rollbackSnapshots, _timeoutForLastRequestStatus, _statusMachine, _initializeStateMachine, initializeStateMachine_fn, _registerMessageHandlers, registerMessageHandlers_fn, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn, _pollForLastRequestStatus, pollForLastRequestStatus_fn, _blockSnap, blockSnap_fn, _unblockSnap, unblockSnap_fn, _assertIsInstallAllowed, assertIsInstallAllowed_fn, _stopSnapsLastRequestPastMax, stopSnapsLastRequestPastMax_fn, _transition, transition_fn, _terminateSnap, terminateSnap_fn, _getSnapEncryptionKey, getSnapEncryptionKey_fn, _decryptSnapState, decryptSnapState_fn, _encryptSnapState, encryptSnapState_fn, _handleInitialConnections, handleInitialConnections_fn, _addSnapToSubject, addSnapToSubject_fn, _removeSnapFromSubjects, removeSnapFromSubjects_fn, _revokeAllSnapPermissions, revokeAllSnapPermissions_fn, _createApproval, createApproval_fn, _updateApproval, updateApproval_fn, _resolveAllowlistVersion, resolveAllowlistVersion_fn, _add, add_fn, _startSnap, startSnap_fn, _getEndowments, getEndowments_fn, _set, set_fn, _validateSnapPermissions, validateSnapPermissions_fn, _getExecutionTimeout, getExecutionTimeout_fn, _getRpcRequestHandler, getRpcRequestHandler_fn, _createInterface, createInterface_fn, _assertInterfaceExists, assertInterfaceExists_fn, _transformSnapRpcRequestResult, transformSnapRpcRequestResult_fn, _assertSnapRpcRequestResult, assertSnapRpcRequestResult_fn, _recordSnapRpcRequestStart, recordSnapRpcRequestStart_fn, _recordSnapRpcRequestFinish, recordSnapRpcRequestFinish_fn, _getRollbackSnapshot, getRollbackSnapshot_fn, _createRollbackSnapshot, createRollbackSnapshot_fn, _rollbackSnap, rollbackSnap_fn, _rollbackSnaps, rollbackSnaps_fn, _getRuntime, getRuntime_fn, _getRuntimeExpect, getRuntimeExpect_fn, _setupRuntime, setupRuntime_fn, _calculatePermissionsChange, calculatePermissionsChange_fn, _updatePermissions, updatePermissions_fn, _isValidUpdate, isValidUpdate_fn, _callLifecycleHook, callLifecycleHook_fn; ++var _closeAllConnections, _dynamicPermissions, _environmentEndowmentPermissions, _excludedPermissions, _featureFlags, _fetchFunction, _idleTimeCheckInterval, _maxIdleTime, _encryptor, _getMnemonic, _detectSnapLocation, _snapsRuntimeData, _rollbackSnapshots, _timeoutForLastRequestStatus, _statusMachine, _preinstalledSnaps, _initializeStateMachine, initializeStateMachine_fn, _registerMessageHandlers, registerMessageHandlers_fn, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn, _pollForLastRequestStatus, pollForLastRequestStatus_fn, _blockSnap, blockSnap_fn, _unblockSnap, unblockSnap_fn, _assertIsInstallAllowed, assertIsInstallAllowed_fn, _stopSnapsLastRequestPastMax, stopSnapsLastRequestPastMax_fn, _transition, transition_fn, _terminateSnap, terminateSnap_fn, _getSnapEncryptionKey, getSnapEncryptionKey_fn, _decryptSnapState, decryptSnapState_fn, _encryptSnapState, encryptSnapState_fn, _handleInitialConnections, handleInitialConnections_fn, _addSnapToSubject, addSnapToSubject_fn, _removeSnapFromSubjects, removeSnapFromSubjects_fn, _revokeAllSnapPermissions, revokeAllSnapPermissions_fn, _createApproval, createApproval_fn, _updateApproval, updateApproval_fn, _resolveAllowlistVersion, resolveAllowlistVersion_fn, _add, add_fn, _startSnap, startSnap_fn, _getEndowments, getEndowments_fn, _set, set_fn, _validateSnapPermissions, validateSnapPermissions_fn, _getExecutionTimeout, getExecutionTimeout_fn, _getRpcRequestHandler, getRpcRequestHandler_fn, _createInterface, createInterface_fn, _assertInterfaceExists, assertInterfaceExists_fn, _transformSnapRpcRequestResult, transformSnapRpcRequestResult_fn, _assertSnapRpcRequestResult, assertSnapRpcRequestResult_fn, _recordSnapRpcRequestStart, recordSnapRpcRequestStart_fn, _recordSnapRpcRequestFinish, recordSnapRpcRequestFinish_fn, _getRollbackSnapshot, getRollbackSnapshot_fn, _createRollbackSnapshot, createRollbackSnapshot_fn, _rollbackSnap, rollbackSnap_fn, _rollbackSnaps, rollbackSnaps_fn, _getRuntime, getRuntime_fn, _getRuntimeExpect, getRuntimeExpect_fn, _setupRuntime, setupRuntime_fn, _calculatePermissionsChange, calculatePermissionsChange_fn, _updatePermissions, updatePermissions_fn, _isValidUpdate, isValidUpdate_fn, _callLifecycleHook, callLifecycleHook_fn; + var SnapController = class extends BaseController { + constructor({ + closeAllConnections, +@@ -132,7 +132,7 @@ var SnapController = class extends BaseController { + fetchFunction = globalThis.fetch.bind(globalThis), + featureFlags = {}, + detectSnapLocation: detectSnapLocationFunction = detectSnapLocation, +- preinstalledSnaps, ++ preinstalledSnaps = null, + encryptor, + getMnemonic + }) { +@@ -448,6 +448,7 @@ var SnapController = class extends BaseController { + __privateAdd(this, _rollbackSnapshots, void 0); + __privateAdd(this, _timeoutForLastRequestStatus, void 0); + __privateAdd(this, _statusMachine, void 0); ++ __privateAdd(this, _preinstalledSnaps, void 0); + __privateSet(this, _closeAllConnections, closeAllConnections); + __privateSet(this, _dynamicPermissions, dynamicPermissions); + __privateSet(this, _environmentEndowmentPermissions, environmentEndowmentPermissions); +@@ -460,6 +461,7 @@ var SnapController = class extends BaseController { + __privateSet(this, _detectSnapLocation, detectSnapLocationFunction); + __privateSet(this, _encryptor, encryptor); + __privateSet(this, _getMnemonic, getMnemonic); ++ __privateSet(this, _preinstalledSnaps, preinstalledSnaps); + this._onUnhandledSnapError = this._onUnhandledSnapError.bind(this); + this._onOutboundRequest = this._onOutboundRequest.bind(this); + this._onOutboundResponse = this._onOutboundResponse.bind(this); +@@ -498,8 +500,8 @@ var SnapController = class extends BaseController { + }); + __privateMethod(this, _initializeStateMachine, initializeStateMachine_fn).call(this); + __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); +- if (preinstalledSnaps) { +- __privateMethod(this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, preinstalledSnaps); ++ if (__privateGet(this, _preinstalledSnaps)) { ++ __privateMethod(this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, __privateGet(this, _preinstalledSnaps)); + } + Object.values(this.state?.snaps ?? {}).forEach( + (snap) => __privateMethod(this, _setupRuntime, setupRuntime_fn).call(this, snap.id) +@@ -801,6 +803,13 @@ var SnapController = class extends BaseController { + state.snaps = {}; + state.snapStates = {}; + }); ++ __privateGet(this, _snapsRuntimeData).clear(); ++ if (__privateGet(this, _preinstalledSnaps)) { ++ __privateMethod(this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, __privateGet(this, _preinstalledSnaps)); ++ Object.values(this.state?.snaps).forEach( ++ (snap) => __privateMethod(this, _setupRuntime, setupRuntime_fn).call(this, snap.id) ++ ); ++ } + } + /** + * Removes the given snap from state, and clears all associated handlers +@@ -1420,6 +1429,7 @@ _snapsRuntimeData = new WeakMap(); + _rollbackSnapshots = new WeakMap(); + _timeoutForLastRequestStatus = new WeakMap(); + _statusMachine = new WeakMap(); ++_preinstalledSnaps = new WeakMap(); + _initializeStateMachine = new WeakSet(); + initializeStateMachine_fn = function() { + const disableGuard = ({ snapId }) => { +diff --git a/dist/chunk-JMP6XYRL.js b/dist/chunk-JMP6XYRL.js +index 20c3b12643e4f87d70aa0df96a36c4dda8b1ed47..5ac24c006b8db0b06d3a48cc312ba630845ef79c 100644 +--- a/dist/chunk-JMP6XYRL.js ++++ b/dist/chunk-JMP6XYRL.js +@@ -117,7 +117,7 @@ function truncateSnap(snap) { + return truncatedSnap; + } + var name = "SnapController"; +-var _closeAllConnections, _dynamicPermissions, _environmentEndowmentPermissions, _excludedPermissions, _featureFlags, _fetchFunction, _idleTimeCheckInterval, _maxIdleTime, _encryptor, _getMnemonic, _detectSnapLocation, _snapsRuntimeData, _rollbackSnapshots, _timeoutForLastRequestStatus, _statusMachine, _initializeStateMachine, initializeStateMachine_fn, _registerMessageHandlers, registerMessageHandlers_fn, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn, _pollForLastRequestStatus, pollForLastRequestStatus_fn, _blockSnap, blockSnap_fn, _unblockSnap, unblockSnap_fn, _assertIsInstallAllowed, assertIsInstallAllowed_fn, _stopSnapsLastRequestPastMax, stopSnapsLastRequestPastMax_fn, _transition, transition_fn, _terminateSnap, terminateSnap_fn, _getSnapEncryptionKey, getSnapEncryptionKey_fn, _decryptSnapState, decryptSnapState_fn, _encryptSnapState, encryptSnapState_fn, _handleInitialConnections, handleInitialConnections_fn, _addSnapToSubject, addSnapToSubject_fn, _removeSnapFromSubjects, removeSnapFromSubjects_fn, _revokeAllSnapPermissions, revokeAllSnapPermissions_fn, _createApproval, createApproval_fn, _updateApproval, updateApproval_fn, _resolveAllowlistVersion, resolveAllowlistVersion_fn, _add, add_fn, _startSnap, startSnap_fn, _getEndowments, getEndowments_fn, _set, set_fn, _validateSnapPermissions, validateSnapPermissions_fn, _getExecutionTimeout, getExecutionTimeout_fn, _getRpcRequestHandler, getRpcRequestHandler_fn, _createInterface, createInterface_fn, _assertInterfaceExists, assertInterfaceExists_fn, _transformSnapRpcRequestResult, transformSnapRpcRequestResult_fn, _assertSnapRpcRequestResult, assertSnapRpcRequestResult_fn, _recordSnapRpcRequestStart, recordSnapRpcRequestStart_fn, _recordSnapRpcRequestFinish, recordSnapRpcRequestFinish_fn, _getRollbackSnapshot, getRollbackSnapshot_fn, _createRollbackSnapshot, createRollbackSnapshot_fn, _rollbackSnap, rollbackSnap_fn, _rollbackSnaps, rollbackSnaps_fn, _getRuntime, getRuntime_fn, _getRuntimeExpect, getRuntimeExpect_fn, _setupRuntime, setupRuntime_fn, _calculatePermissionsChange, calculatePermissionsChange_fn, _updatePermissions, updatePermissions_fn, _isValidUpdate, isValidUpdate_fn, _callLifecycleHook, callLifecycleHook_fn; ++var _closeAllConnections, _dynamicPermissions, _environmentEndowmentPermissions, _excludedPermissions, _featureFlags, _fetchFunction, _idleTimeCheckInterval, _maxIdleTime, _encryptor, _getMnemonic, _detectSnapLocation, _snapsRuntimeData, _rollbackSnapshots, _timeoutForLastRequestStatus, _statusMachine, _preinstalledSnaps, _initializeStateMachine, initializeStateMachine_fn, _registerMessageHandlers, registerMessageHandlers_fn, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn, _pollForLastRequestStatus, pollForLastRequestStatus_fn, _blockSnap, blockSnap_fn, _unblockSnap, unblockSnap_fn, _assertIsInstallAllowed, assertIsInstallAllowed_fn, _stopSnapsLastRequestPastMax, stopSnapsLastRequestPastMax_fn, _transition, transition_fn, _terminateSnap, terminateSnap_fn, _getSnapEncryptionKey, getSnapEncryptionKey_fn, _decryptSnapState, decryptSnapState_fn, _encryptSnapState, encryptSnapState_fn, _handleInitialConnections, handleInitialConnections_fn, _addSnapToSubject, addSnapToSubject_fn, _removeSnapFromSubjects, removeSnapFromSubjects_fn, _revokeAllSnapPermissions, revokeAllSnapPermissions_fn, _createApproval, createApproval_fn, _updateApproval, updateApproval_fn, _resolveAllowlistVersion, resolveAllowlistVersion_fn, _add, add_fn, _startSnap, startSnap_fn, _getEndowments, getEndowments_fn, _set, set_fn, _validateSnapPermissions, validateSnapPermissions_fn, _getExecutionTimeout, getExecutionTimeout_fn, _getRpcRequestHandler, getRpcRequestHandler_fn, _createInterface, createInterface_fn, _assertInterfaceExists, assertInterfaceExists_fn, _transformSnapRpcRequestResult, transformSnapRpcRequestResult_fn, _assertSnapRpcRequestResult, assertSnapRpcRequestResult_fn, _recordSnapRpcRequestStart, recordSnapRpcRequestStart_fn, _recordSnapRpcRequestFinish, recordSnapRpcRequestFinish_fn, _getRollbackSnapshot, getRollbackSnapshot_fn, _createRollbackSnapshot, createRollbackSnapshot_fn, _rollbackSnap, rollbackSnap_fn, _rollbackSnaps, rollbackSnaps_fn, _getRuntime, getRuntime_fn, _getRuntimeExpect, getRuntimeExpect_fn, _setupRuntime, setupRuntime_fn, _calculatePermissionsChange, calculatePermissionsChange_fn, _updatePermissions, updatePermissions_fn, _isValidUpdate, isValidUpdate_fn, _callLifecycleHook, callLifecycleHook_fn; + var SnapController = class extends _basecontroller.BaseController { + constructor({ + closeAllConnections, +@@ -132,7 +132,7 @@ var SnapController = class extends _basecontroller.BaseController { + fetchFunction = globalThis.fetch.bind(globalThis), + featureFlags = {}, + detectSnapLocation: detectSnapLocationFunction = _chunkPT22IXNSjs.detectSnapLocation, +- preinstalledSnaps, ++ preinstalledSnaps = null, + encryptor, + getMnemonic + }) { +@@ -448,6 +448,7 @@ var SnapController = class extends _basecontroller.BaseController { + _chunkEXN2TFDJjs.__privateAdd.call(void 0, this, _rollbackSnapshots, void 0); + _chunkEXN2TFDJjs.__privateAdd.call(void 0, this, _timeoutForLastRequestStatus, void 0); + _chunkEXN2TFDJjs.__privateAdd.call(void 0, this, _statusMachine, void 0); ++ _chunkEXN2TFDJjs.__privateAdd.call(void 0, this, _preinstalledSnaps, void 0); + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _closeAllConnections, closeAllConnections); + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _dynamicPermissions, dynamicPermissions); + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _environmentEndowmentPermissions, environmentEndowmentPermissions); +@@ -460,6 +461,7 @@ var SnapController = class extends _basecontroller.BaseController { + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _detectSnapLocation, detectSnapLocationFunction); + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _encryptor, encryptor); + _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _getMnemonic, getMnemonic); ++ _chunkEXN2TFDJjs.__privateSet.call(void 0, this, _preinstalledSnaps, preinstalledSnaps); + this._onUnhandledSnapError = this._onUnhandledSnapError.bind(this); + this._onOutboundRequest = this._onOutboundRequest.bind(this); + this._onOutboundResponse = this._onOutboundResponse.bind(this); +@@ -498,8 +500,8 @@ var SnapController = class extends _basecontroller.BaseController { + }); + _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _initializeStateMachine, initializeStateMachine_fn).call(this); + _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _registerMessageHandlers, registerMessageHandlers_fn).call(this); +- if (preinstalledSnaps) { +- _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, preinstalledSnaps); ++ if (_chunkEXN2TFDJjs.__privateGet.call(void 0, this, _preinstalledSnaps)) { ++ _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, _chunkEXN2TFDJjs.__privateGet.call(void 0, this, _preinstalledSnaps)); + } + Object.values(this.state?.snaps ?? {}).forEach( + (snap) => _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _setupRuntime, setupRuntime_fn).call(this, snap.id) +@@ -801,6 +803,13 @@ var SnapController = class extends _basecontroller.BaseController { + state.snaps = {}; + state.snapStates = {}; + }); ++ _chunkEXN2TFDJjs.__privateGet.call(void 0, this, _snapsRuntimeData).clear(); ++ if (_chunkEXN2TFDJjs.__privateGet.call(void 0, this, _preinstalledSnaps)) { ++ _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _handlePreinstalledSnaps, handlePreinstalledSnaps_fn).call(this, _chunkEXN2TFDJjs.__privateGet.call(void 0, this, _preinstalledSnaps)); ++ Object.values(this.state?.snaps).forEach( ++ (snap) => _chunkEXN2TFDJjs.__privateMethod.call(void 0, this, _setupRuntime, setupRuntime_fn).call(this, snap.id) ++ ); ++ } + } + /** + * Removes the given snap from state, and clears all associated handlers +@@ -1420,6 +1429,7 @@ _snapsRuntimeData = new WeakMap(); + _rollbackSnapshots = new WeakMap(); + _timeoutForLastRequestStatus = new WeakMap(); + _statusMachine = new WeakMap(); ++_preinstalledSnaps = new WeakMap(); + _initializeStateMachine = new WeakSet(); + initializeStateMachine_fn = function() { + const disableGuard = ({ snapId }) => { +@@ -2434,4 +2444,5 @@ callLifecycleHook_fn = async function(snapId, handler) { + + + exports.controllerName = controllerName; exports.SNAP_APPROVAL_INSTALL = SNAP_APPROVAL_INSTALL; exports.SNAP_APPROVAL_UPDATE = SNAP_APPROVAL_UPDATE; exports.SNAP_APPROVAL_RESULT = SNAP_APPROVAL_RESULT; exports.SnapController = SnapController; ++ + //# sourceMappingURL=chunk-JMP6XYRL.js.map +\ No newline at end of file diff --git a/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch b/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch new file mode 100644 index 000000000000..220b6f5d6baf --- /dev/null +++ b/.yarn/patches/@spruceid-siwe-parser-npm-2.1.0-060b7ede7a.patch @@ -0,0 +1,51 @@ +diff --git a/dist/abnf.js b/dist/abnf.js +index 15caf986714ddc2276571d17c35bf392941fa346..0eeac1eeb94284e201fb0bbaea887c0f3d060aaa 100644 +--- a/dist/abnf.js ++++ b/dist/abnf.js +@@ -290,9 +290,6 @@ class ParsedMessage { + if (this.domain.length === 0) { + throw new Error("Domain cannot be empty."); + } +- if (!(0, utils_1.isEIP55Address)(this.address)) { +- throw new Error("Address not conformant to EIP-55."); +- } + } + } + exports.ParsedMessage = ParsedMessage; +diff --git a/dist/regex.js b/dist/regex.js +index 4740a7c271db7fb2b5f0885727053e1165e8e392..cbfa067030a975ae645ef68baa3b963059e89f2c 100644 +--- a/dist/regex.js ++++ b/dist/regex.js +@@ -55,9 +55,7 @@ class ParsedMessage { + throw new Error("Domain cannot be empty."); + } + this.address = (_b = match === null || match === void 0 ? void 0 : match.groups) === null || _b === void 0 ? void 0 : _b.address; +- if (!(0, utils_1.isEIP55Address)(this.address)) { +- throw new Error("Address not conformant to EIP-55."); +- } ++ + this.statement = (_c = match === null || match === void 0 ? void 0 : match.groups) === null || _c === void 0 ? void 0 : _c.statement; + this.uri = (_d = match === null || match === void 0 ? void 0 : match.groups) === null || _d === void 0 ? void 0 : _d.uri; + if (!uri.isUri(this.uri)) { +diff --git a/lib/abnf.ts b/lib/abnf.ts +index a7e5fcfaefdf39bac8bf0b5ee2d6e12cee4b07f0..6d022a2bd17ec5d7158b34e978a946465520aa74 100644 +--- a/lib/abnf.ts ++++ b/lib/abnf.ts +@@ -1,6 +1,6 @@ + import apgApi from "apg-js/src/apg-api/api"; + import apgLib from "apg-js/src/apg-lib/node-exports"; +-import { isEIP55Address, parseIntegerNumber } from "./utils"; ++import { parseIntegerNumber } from "./utils"; + + const GRAMMAR = ` + sign-in-with-ethereum = +@@ -358,9 +358,5 @@ export class ParsedMessage { + if (this.domain.length === 0) { + throw new Error("Domain cannot be empty."); + } +- +- if (!isEIP55Address(this.address)) { +- throw new Error("Address not conformant to EIP-55."); +- } + } + } diff --git a/.yarnrc.yml b/.yarnrc.yml index 374745715f05..adc73de8f717 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -43,7 +43,6 @@ npmAuditIgnoreAdvisories: # not appear to be used. - 1092461 - # Temp fix for https://github.com/MetaMask/metamask-extension/pull/16920 for the sake of 11.7.1 hotfix # This will be removed in this ticket https://github.com/MetaMask/metamask-extension/issues/22299 - 'ts-custom-error (deprecation)' @@ -93,7 +92,7 @@ npmAuditIgnoreAdvisories: # MetaMask owned repositories brought in by other MetaMask dependencies that # can be resolved by updating the versions throughout the dependency tree - 'eth-sig-util (deprecation)' # via @metamask/eth-ledger-bridge-keyring - - '@metamask/controller-utils (deprecation)' # via @metamask/phishin-controller + - '@metamask/controller-utils (deprecation)' # via @metamask/phishing-controller - 'safe-event-emitter (deprecation)' # via eth-block-tracker and others # @metamask-institutional relies upon crypto which is deprecated @@ -126,18 +125,18 @@ npmAuditIgnoreAdvisories: - '@metamask/snaps-ui (deprecation)' npmRegistries: - "https://npm.pkg.github.com": + 'https://npm.pkg.github.com': npmAlwaysAuth: true - npmAuthToken: "${GITHUB_PACKAGE_READ_TOKEN-}" + npmAuthToken: '${GITHUB_PACKAGE_READ_TOKEN-}' npmScopes: metamask: - npmRegistryServer: "${METAMASK_NPM_REGISTRY:-https://registry.yarnpkg.com}" + npmRegistryServer: '${METAMASK_NPM_REGISTRY:-https://registry.yarnpkg.com}' plugins: - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs - spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js" + spec: 'https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js' - path: .yarn/plugins/@yarnpkg/plugin-engines.cjs - spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js" + spec: 'https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js' yarnPath: .yarn/releases/yarn-4.0.2.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd8ff84720f..7ca4b44f8527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.17.0] + ## [11.14.4] ### Fixed - Fix bug that could cause safe-transfer-from transactions to be converted to transfer-from transactions, by removing the edit button on the safe-transfer-from confirmation screens ([#24287](https://github.com/MetaMask/metamask-extension/pull/24287)) @@ -4625,7 +4627,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/v11.14.4...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v11.17.0...HEAD +[11.17.0]: https://github.com/MetaMask/metamask-extension/compare/v11.14.4...v11.17.0 [11.14.4]: https://github.com/MetaMask/metamask-extension/compare/v11.14.3...v11.14.4 [11.14.3]: https://github.com/MetaMask/metamask-extension/compare/v11.14.2...v11.14.3 [11.14.2]: https://github.com/MetaMask/metamask-extension/compare/v11.14.1...v11.14.2 diff --git a/README.md b/README.md index d9a31a86a8b0..41f53ba99d53 100644 --- a/README.md +++ b/README.md @@ -117,20 +117,21 @@ Before running e2e tests, ensure you've run `yarn install` to download dependenc 1. Use `yarn download-builds:test` to quickly download and unzip test builds for Chrome and Firefox into the `./dist/` folder. This method is fast and convenient for standard testing. 2. Create a custom test build: for testing against different build types, use `yarn build:test`. This command allows you to generate test builds for various types, including: - - `yarn build:test` for main build - - `yarn build:test:flask` for flask build - - `yarn build:test:mmi` for mmi build - - `yarn build:test:mv3` for mv3 build -3. Start a test build with live changes: `yarn start:test` is particularly useful for development. It starts a test build that automatically recompiles application code upon changes.This option is ideal for iterative testing and development. -This command also allows you to generate test builds for various types, including: - - `yarn start:test` for main build - - `yarn start:test:flask` for flask build - - `yarn start:test:mv3` for mv3 build + - `yarn build:test` for main build + - `yarn build:test:flask` for flask build + - `yarn build:test:mmi` for mmi build + - `yarn build:test:mv3` for mv3 build +3. Start a test build with live changes: `yarn start:test` is particularly useful for development. It starts a test build that automatically recompiles application code upon changes. This option is ideal for iterative testing and development. This command also allows you to generate test builds for various types, including: + - `yarn start:test` for main build + - `yarn start:test:flask` for flask build + - `yarn start:test:mv3` for mv3 build Note: The `yarn start:test` command (which initiates the testDev build type) has LavaMoat disabled for both the build system and the application, offering a streamlined testing experience during development. On the other hand, `yarn build:test` enables LavaMoat for enhanced security in both the build system and application, mirroring production environments more closely. #### Running Tests + Once you have your test build ready, choose the browser for your e2e tests: + - For Firefox, run `yarn test:e2e:firefox`. - For Chrome, run `yarn test:e2e:chrome`. @@ -141,10 +142,11 @@ These scripts support additional options for debugging. Use `--help`to see all a Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below. ```console - --browser Set the browser used; either 'chrome' or 'firefox'. - [string] [choices: "chrome", "firefox"] + --browser Set the browser to be used; specify 'chrome', 'firefox', 'all' + or leave unset to run on 'all' by default. + [string] [default: 'all'] --debug Run tests in debug mode, logging each driver interaction - [boolean] [default: false] + [boolean] [default: true] --retries Set how many times the test should be retried upon failure. [number] [default: 0] --leave-running Leaves the browser running after a test fails, along with @@ -155,10 +157,10 @@ Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME. ``` For example, to run the `account-details` tests using Chrome, with debug logging and with the browser set to remain open upon failure, you would use: -`yarn test:e2e:single test/e2e/tests/account-menu/account-details.spec.js --browser=chrome --debug --leave-running` - +`yarn test:e2e:single test/e2e/tests/account-menu/account-details.spec.js --browser=chrome --leave-running` #### Running e2e tests against specific feature flag + While developing new features, we often use feature flags. As we prepare to make these features generally available (GA), we remove the feature flags. Existing feature flags are listed in the `.metamaskrc.dist` file. To execute e2e tests with a particular feature flag enabled, it's necessary to first generate a test build with that feature flag activated. There are two ways to achieve this: - To enable a feature flag in your local configuration, you should first ensure you have a `.metamaskrc` file copied from `.metamaskrc.dist`. Then, within your local `.metamaskrc` file, you can set the desired feature flag to true. Following this, a test build with the feature flag enabled can be created by executing `yarn build:test`. @@ -166,7 +168,7 @@ While developing new features, we often use feature flags. As we prepare to make - Alternatively, for enabling a feature flag directly during the test build creation, you can pass the parameter as true via the command line. For instance, activating the MULTICHAIN feature flag can be done by running `MULTICHAIN=1 yarn build:test` or `MULTICHAIN=1 yarn start:test` . This method allows for quick adjustments to feature flags without altering the `.metamaskrc` file. Once you've created a test build with the desired feature flag enabled, proceed to run your tests as usual. Your tests will now run against the version of the extension with the specific feature flag activated. For example: -`yarn test:e2e:single test/e2e/tests/account-menu/account-details.spec.js --browser=chrome --debug --leave-running` +`yarn test:e2e:single test/e2e/tests/account-menu/account-details.spec.js --browser=chrome` This approach ensures that your e2e tests accurately reflect the user experience for the upcoming GA features. diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index d7283cacf04b..824c64b10fe6 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Erlauben dieser externen Erweiterung auf:" - }, "allowSpendToken": { "message": "Genehmigung zum Zugriff auf Ihr $1 erteilen?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Erlauben Sie dieser Seite:" - }, - "allowThisSnapTo": { - "message": "Erlauben Sie diesem Snap Folgendes:" - }, "allowWithdrawAndSpend": { "message": "$1 erlauben, bis zu dem folgenden Betrag abzuheben und auszugeben:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Mit $1 verbinden", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Verbindung mit $1 wird hergestellt", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Mit all Ihren $1 verbinden", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "Konten", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Verbindung mit $1 wird hergestellt", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1-Konten", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Mit MetaMask verbinden" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Allgemein" }, - "globalTitle": { - "message": "Globales MenÃŧ" - }, - "globalTourDescription": { - "message": "Sehen Sie Ihr Portfolio, verbundene Seiten, Einstellungen und mehr." - }, "goBack": { "message": "ZurÃŧck" }, @@ -2707,7 +2672,8 @@ "message": "Ein betrÃŧgerischer Netzwerkanbieter kann bezÃŧglich des Status der Blockchain täuschen und Ihre Netzwerkaktivitäten aufzeichnen. FÃŧgen Sie nur vertrauenswÃŧrdige Netzwerke hinzu." }, "onlyConnectTrust": { - "message": "Verbinden Sie sich nur mit Seiten, denen Sie vertrauen." + "message": "Verbinden Sie sich nur mit Seiten, denen Sie vertrauen.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Öffnen Sie MetaMask im Vollbildmodus, um Ihren Ledger Ãŧber WebHID zu verbinden.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "Genehmigungsanfrage" }, - "permissionRequestCapitalized": { - "message": "Genehmigungsanfrage" - }, "permissionRequested": { "message": "Jetzt angefragt" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Genehmigungen" }, - "permissionsTitle": { - "message": "Genehmigungen" - }, - "permissionsTourDescription": { - "message": "Hier kÃļnnen Sie Ihre verbundenen Konten finden und Berechtigungen verwalten." - }, "personalAddressDetected": { "message": "Personalisierte Adresse identifiziert. Bitte fÃŧge die Token-Contract-Adresse ein." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "Smart Contracts" }, - "smartSwapsAreHere": { - "message": "Die Smart Swaps sind da!" - }, - "smartSwapsDescription": { - "message": "MetaMask Swaps ist jetzt wesentlich intelligenter! Die Aktivierung von Smart Swaps wird es MetaMask erlauben, Ihre Swaps programmatisch zu optimieren, um zu helfen:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Nicht genÃŧgend Gelder fÃŧr einen Smart Swap." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Webseite" }, - "snapInstallRequest": { - "message": "Durch die Installation von $1 werden folgende Genehmigungen erteilt. Nur fortfahren, wenn Sie $1 vertrauen.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Installation ist abgeschlossen" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "Stark" }, - "stxBenefit1": { - "message": "Transaktionskosten minimieren" - }, - "stxBenefit2": { - "message": "Transaktionsausfälle reduzieren" - }, - "stxBenefit3": { - "message": "Steckengebliebene Transaktionen eliminieren" - }, - "stxBenefit4": { - "message": "Front-Running verhindern" - }, "stxCancelled": { "message": "Swap wäre gescheitert" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "Sie haben gewechselt zu" }, - "switcherTitle": { - "message": "Netzwerkwechsler" - }, - "switcherTourDescription": { - "message": "Klicken Sie auf das Symbol, um das Netzwerk zu wechseln oder ein neues Netzwerk hinzuzufÃŧgen." - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Das Wechseln der Netzwerke wird alle ausstehenden Bestätigungen stornieren." }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Zeige $1 bei $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "$1 auf Etherscan anzeigen", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "Was ist das?" }, - "xOfY": { - "message": "$1 von $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 676378aa3946..430440d432f4 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "ΕĪ€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ĪƒÎĩ ÎąĪ…Ī„ÎŽ Ī„ΡÎŊ ÎĩΞĪ‰Ī„ÎĩĪÎšÎēÎŽ ÎĩĪ€Î­ÎēĪ„ÎąĪƒÎˇ ÎŊÎą:" - }, "allowSpendToken": { "message": "ΔίÎŊÎĩĪ„Îĩ ÎŦδÎĩΚι ÎŗΚι ÎŊÎą ÎąĪ€ÎŋÎēĪ„ÎŽĪƒÎĩĪ„Îĩ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇ ĪƒĪ„Îŋ $1;", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "ΕĪ€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ĪƒÎĩ ÎąĪ…Ī„ĪŒÎŊ Ī„ÎŋÎŊ ΚĪƒĪ„ĪŒĪ„ÎŋĪ€Îŋ ÎŊÎą:" - }, - "allowThisSnapTo": { - "message": "ΕĪ€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ snap ÎŊÎą:" - }, "allowWithdrawAndSpend": { "message": "ΕĪ€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ĪƒĪ„Îŋ $1 ÎŊÎą ÎēÎŦÎŊÎĩΚ ÎąÎŊÎŦÎģΡĪˆÎˇ ÎēιΚ ÎŊÎą ΞÎŋδέĪˆÎĩΚ ÎŧέĪ‡ĪÎš Ī„Îŋ ÎąÎēĪŒÎģÎŋĪ…θÎŋ Ī€ÎŋĪƒĪŒ:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "ÎŖĪÎŊδÎĩĪƒÎˇ $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "ÎŖĪ…ÎŊδÎĩθÎĩίĪ„Îĩ ĪƒÎĩ ĪŒÎģÎą Ī„Îą $1 ĪƒÎąĪ‚", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋί", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋί", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ Ī„Îŋ MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "ΓÎĩÎŊΚÎēÎŦ" }, - "globalTitle": { - "message": "ΓÎĩÎŊΚÎēĪŒ ÎŧÎĩÎŊÎŋĪ" - }, - "globalTourDescription": { - "message": "ΔÎĩίĪ„Îĩ Ī„Îŋ Ī‡ÎąĪĪ„ÎŋĪ†Ī…ÎģÎŦÎēΚĪŒ ĪƒÎąĪ‚, Ī„ΚĪ‚ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎĩĪ‚ ΚĪƒĪ„ÎŋĪƒÎĩÎģίδÎĩĪ‚, Ī„ΚĪ‚ ĪĪ…θÎŧίĪƒÎĩΚĪ‚ ÎēιΚ Ī€ÎŋÎģÎģÎŦ ÎŦÎģÎģÎą" - }, "goBack": { "message": "ΠΡÎŗÎąÎ¯ÎŊÎĩĪ„Îĩ Ī€Î¯ĪƒĪ‰" }, @@ -2707,7 +2672,8 @@ "message": "ΈÎŊÎąĪ‚ ÎēÎąÎēĪŒÎ˛ÎŋĪ…ÎģÎŋĪ‚ Ī€ÎŦĪÎŋĪ‡ÎŋĪ‚ δΚÎēĪ„ĪÎŋĪ… ÎŧĪ€ÎŋĪÎĩί ÎŊÎą Ī€ÎĩΚ ĪˆÎ­ÎŧÎąĪ„Îą ĪƒĪ‡ÎĩĪ„ΚÎēÎŦ ÎŧÎĩ Ī„ΡÎŊ ÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇ Ī„ÎŋĪ… blockchain ÎēιΚ ÎŊÎą ÎēÎąĪ„ÎąÎŗĪÎŦĪˆÎĩΚ Ī„Ρ δĪÎąĪƒĪ„ΡĪÎšĪŒĪ„ΡĪ„Îą Ī„ÎŋĪ… δΚÎēĪ„ĪÎŋĪ… ĪƒÎąĪ‚. ΠĪÎŋĪƒÎ¸Î­ĪƒĪ„Îĩ ÎŧĪŒÎŊÎŋ Ī€ĪÎŋĪƒÎąĪÎŧÎŋĪƒÎŧέÎŊÎą δίÎēĪ„Ī…Îą Ī€ÎŋĪ… ÎĩÎŧĪ€ÎšĪƒĪ„ÎĩĪÎĩĪƒĪ„Îĩ." }, "onlyConnectTrust": { - "message": "ÎŖĪ…ÎŊδÎĩθÎĩίĪ„Îĩ ÎŧĪŒÎŊÎŋ ÎŧÎĩ ΚĪƒĪ„ĪŒĪ„ÎŋĪ€ÎŋĪ…Ī‚ Ī€ÎŋĪ… ÎĩÎŧĪ€ÎšĪƒĪ„ÎĩĪÎĩĪƒĪ„Îĩ." + "message": "ÎŖĪ…ÎŊδÎĩθÎĩίĪ„Îĩ ÎŧĪŒÎŊÎŋ ÎŧÎĩ ΚĪƒĪ„ĪŒĪ„ÎŋĪ€ÎŋĪ…Ī‚ Ī€ÎŋĪ… ÎĩÎŧĪ€ÎšĪƒĪ„ÎĩĪÎĩĪƒĪ„Îĩ.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "ΜÎĩĪ„ιβÎĩίĪ„Îĩ ĪƒÎĩ Ī€ÎģÎŽĪÎˇ ÎŋθĪŒÎŊΡ ÎŗΚι ÎŊÎą ĪƒĪ…ÎŊδέĪƒÎĩĪ„Îĩ Ī„Îŋ Ledger ĪƒÎąĪ‚.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "ΑίĪ„ΡÎŧÎą ÎŦδÎĩΚιĪ‚" }, - "permissionRequestCapitalized": { - "message": "ΑίĪ„ΡÎŧÎą ÎŦδÎĩΚιĪ‚" - }, "permissionRequested": { "message": "ΖηĪ„ΎθΡÎēÎĩ Ī„ĪŽĪÎą" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "ΆδÎĩΚÎĩĪ‚" }, - "permissionsTitle": { - "message": "ΆδÎĩΚÎĩĪ‚" - }, - "permissionsTourDescription": { - "message": "ΒĪÎĩίĪ„Îĩ Ī„ÎŋĪ…Ī‚ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎŋĪ…Ī‚ ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋĪĪ‚ ĪƒÎąĪ‚ ÎēιΚ δΚιĪ‡ÎĩΚĪÎšĪƒĪ„ÎĩίĪ„Îĩ Ī„ΚĪ‚ ÎŦδÎĩΚÎĩĪ‚ ÎĩδĪŽ" - }, "personalAddressDetected": { "message": "Η Ī€ĪÎŋĪƒĪ‰Ī€ÎšÎēÎŽ δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ ÎĩÎŊĪ„ÎŋĪ€Î¯ĪƒĪ„ΡÎēÎĩ. ΚαĪ„ÎąĪ‡Ī‰ĪÎ¯ĪƒĪ„Îĩ Ī„Ρ δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ ĪƒĪ…ÎŧβÎŋÎģÎąÎ¯ÎŋĪ… Ī„ÎŋĪ… token." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "ΈΞĪ…Ī€ÎŊÎą ĪƒĪ…ÎŧβĪŒÎģιΚι" }, - "smartSwapsAreHere": { - "message": "Οι ΈΞĪ…Ī€ÎŊÎĩĪ‚ ΑÎŊĪ„ÎąÎģÎģÎąÎŗέĪ‚ ÎĩίÎŊιΚ ÎĩδĪŽ!" - }, - "smartSwapsDescription": { - "message": "Οι ΑÎŊĪ„ÎąÎģÎģÎąÎŗέĪ‚ ĪƒĪ„Îŋ MetaMask ÎŧĪŒÎģΚĪ‚ έÎŗΚÎŊÎąÎŊ Ī€ÎŋÎģĪ Ī€ÎšÎŋ έΞĪ…Ī€ÎŊÎĩĪ‚! Η ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Ī„Ī‰ÎŊ ΈΞĪ…Ī€ÎŊĪ‰ÎŊ ΑÎŊĪ„ÎąÎģÎģÎąÎŗĪŽÎŊ θι ÎĩĪ€ÎšĪ„ĪÎ­ĪˆÎĩΚ ĪƒĪ„Îŋ MetaMask ÎŊÎą βÎĩÎģĪ„ΚĪƒĪ„ÎŋĪ€ÎŋΚΎĪƒÎĩΚ Ī€ĪÎŋÎŗĪÎąÎŧÎŧÎąĪ„ΚĪƒĪ„ΚÎēÎŦ Ī„ΚĪ‚ ΑÎŊĪ„ÎąÎģÎģÎąÎŗέĪ‚ ĪƒÎąĪ‚, ĪŽĪƒĪ„Îĩ ÎŊÎą ĪƒÎąĪ‚ βÎŋΡθΎĪƒÎĩΚ:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "ΔÎĩÎŊ Ī…Ī€ÎŦĪĪ‡ÎŋĪ…ÎŊ ÎąĪÎēÎĩĪ„ÎŦ ÎēÎĩĪ†ÎŦÎģιΚι ÎŗΚι έΞĪ…Ī€ÎŊÎĩĪ‚ ÎąÎŊĪ„ÎąÎģÎģÎąÎŗέĪ‚." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "ΙĪƒĪ„ĪŒĪ„ÎŋĪ€ÎŋĪ‚" }, - "snapInstallRequest": { - "message": "ΜÎĩ Ī„ΡÎŊ ÎĩÎŗÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇ, ÎĩÎēĪ‡Ī‰ĪÎŋĪÎŊĪ„ιΚ ĪƒĪ„Îŋ $1 ÎŋΚ ÎąÎēĪŒÎģÎŋĪ…θÎĩĪ‚ ÎŦδÎĩΚÎĩĪ‚. ÎŖĪ…ÎŊÎĩĪ‡Î¯ĪƒĪ„Îĩ ÎŧĪŒÎŊÎŋ ÎąÎŊ ÎĩÎŧĪ€ÎšĪƒĪ„ÎĩĪÎĩĪƒĪ„Îĩ Ī„Îŋ $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Η ÎĩÎŗÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇ ÎŋÎģÎŋÎēÎģΡĪĪŽÎ¸ÎˇÎēÎĩ" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "ΙĪƒĪ‡Ī…ĪĪŒ" }, - "stxBenefit1": { - "message": "ΕÎģÎąĪ‡ÎšĪƒĪ„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Ī„ÎŋĪ… ÎēĪŒĪƒĪ„ÎŋĪ…Ī‚ ĪƒĪ…ÎŊÎąÎģÎģÎąÎŗĪŽÎŊ" - }, - "stxBenefit2": { - "message": "ΜÎĩίĪ‰ĪƒÎˇ Ī„Ī‰ÎŊ ÎąĪ€ÎŋĪ„Ī…Ī‡ÎˇÎŧέÎŊĪ‰ÎŊ ĪƒĪ…ÎŊÎąÎģÎģÎąÎŗĪŽÎŊ" - }, - "stxBenefit3": { - "message": "ΕξÎŦÎģÎĩΚĪˆÎˇ Ī„Ī‰ÎŊ ÎĩÎŧĪ€ÎģÎŋÎēĪŽÎŊ ĪƒĪ„ΚĪ‚ ĪƒĪ…ÎŊÎąÎģÎģÎąÎŗέĪ‚" - }, - "stxBenefit4": { - "message": "ΑĪ€ÎŋĪ„ĪÎŋĪ€ÎŽ Ī„Ī‰ÎŊ Ī€ĪÎŋĪ€ÎŋĪÎĩĪ…ĪŒÎŧÎĩÎŊĪ‰ÎŊ ĪƒĪ…ÎŊÎąÎģÎģÎąÎŗĪŽÎŊ (front-running)" - }, "stxCancelled": { "message": "Η ÎąÎŊĪ„ÎąÎģÎģÎąÎŗÎŽ θι ÎĩίĪ‡Îĩ ÎąĪ€ÎŋĪ„ĪĪ‡ÎĩΚ" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "ΈĪ‡ÎĩĪ„Îĩ ÎąÎģÎģÎŦΞÎĩΚ ĪƒÎĩ" }, - "switcherTitle": { - "message": "ΕĪ€ÎšÎģÎŋÎŗέιĪ‚ δΚÎēĪ„ĪÎŋĪ…" - }, - "switcherTourDescription": { - "message": "ΚÎŦÎŊĪ„Îĩ ÎēÎģΚÎē ĪƒĪ„Îŋ ÎĩΚÎēÎŋÎŊίδΚÎŋ ÎŗΚι ÎŊÎą ÎąÎģÎģÎŦΞÎĩĪ„Îĩ δίÎēĪ„Ī…Îŋ ÎŽ ÎŊÎą Ī€ĪÎŋĪƒÎ¸Î­ĪƒÎĩĪ„Îĩ έÎŊÎą ÎŊέÎŋ δίÎēĪ„Ī…Îŋ" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Η ÎąÎģÎģÎąÎŗÎŽ δΚÎēĪ„ĪĪ‰ÎŊ θι ÎąÎēĪ…ĪĪŽĪƒÎĩΚ ĪŒÎģÎĩĪ‚ Ī„ΚĪ‚ ÎĩÎēÎēĪÎĩÎŧÎĩίĪ‚ ÎĩĪ€ÎšÎ˛ÎĩβιΚĪŽĪƒÎĩΚĪ‚" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "ΠĪÎŋβÎŋÎģÎŽ $1 ĪƒĪ„Îŋ $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "ΠĪÎŋβÎŋÎģÎŽ $1 ĪƒĪ„Îŋ Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ΤΚ ÎĩίÎŊιΚ ÎąĪ…Ī„ĪŒ;" }, - "xOfY": { - "message": "$1 ÎąĪ€ĪŒ $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index db38fa50d0fc..51aaaf492ced 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -340,9 +340,18 @@ "airgapVault": { "message": "AirGap Vault" }, + "alert": { + "message": "Alert" + }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, + "alertModalAcknowledge": { + "message": "I have acknowledged the risk and still want to proceed" + }, + "alertModalDetails": { + "message": "Alert Details" + }, "alertSettingsUnconnectedAccount": { "message": "Browsing a website with an unconnected account selected" }, @@ -378,9 +387,6 @@ "allow": { "message": "Allow" }, - "allowExternalExtensionTo": { - "message": "Allow this external extension to:" - }, "allowMmiToConnectToCustodian": { "message": "This will allow MMI to connect to $1 to import your accounts." }, @@ -388,12 +394,6 @@ "message": "Give permission to access your $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Allow this site to:" - }, - "allowThisSnapTo": { - "message": "Allow this snap to:" - }, "allowWithdrawAndSpend": { "message": "Allow $1 to withdraw and spend up to the following amount:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -409,6 +409,9 @@ "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" @@ -562,6 +565,33 @@ "basic": { "message": "Basic" }, + "basicConfigurationBannerCTA": { + "message": "Turn on basic functionality" + }, + "basicConfigurationBannerTitle": { + "message": "Basic functionality is off" + }, + "basicConfigurationDescription": { + "message": "Includes token data and value, optimal gas settings, and more. Using these services shares your IP address with MetaMask, just like when you visit a website." + }, + "basicConfigurationLabel": { + "message": "Basic functionality" + }, + "basicConfigurationModalCheckbox": { + "message": "I understand and want to continue" + }, + "basicConfigurationModalDisclaimerOff": { + "message": "This means you won't fully optimize your time on MetaMask. Basic features (like token details, optimal gas settings, and others) won't be available to you." + }, + "basicConfigurationModalDisclaimerOn": { + "message": "To optimize your time on MetaMask, you’ll need to turn on this feature. Basic functions (like token details, optimal gas settings, and others) are important to the web3 experience." + }, + "basicConfigurationModalHeadingOff": { + "message": "Turn off basic functionality" + }, + "basicConfigurationModalHeadingOn": { + "message": "Turn on basic functionality" + }, "beCareful": { "message": "Be careful" }, @@ -619,7 +649,7 @@ "message": "If you approve this request, someone can steal your assets listed on Blur." }, "blockaidDescriptionErrored": { - "message": "Because of an error, this request was not verified by the security provider. Proceed with caution." + "message": "Because of an error, we couldn't check for security alerts. Only continue if you trust every address involved." }, "blockaidDescriptionMaliciousDomain": { "message": "You're interacting with a malicious domain. If you approve this request, you might lose your assets." @@ -640,7 +670,7 @@ "message": "This is a deceptive request" }, "blockaidTitleMayNotBeSafe": { - "message": "Request may not be safe" + "message": "Be careful" }, "blockaidTitleSuspicious": { "message": "This is a suspicious request" @@ -666,9 +696,6 @@ "busy": { "message": "Busy" }, - "buy": { - "message": "Buy" - }, "buyAndSell": { "message": "Buy & Sell" }, @@ -683,6 +710,10 @@ "buyNow": { "message": "Buy Now" }, + "buyToken": { + "message": "Buy $1", + "description": "$1 is the token symbol" + }, "bytes": { "message": "Bytes" }, @@ -759,6 +790,9 @@ "close": { "message": "Close" }, + "closeExtension": { + "message": "Close extension" + }, "coingecko": { "message": "CoinGecko" }, @@ -799,15 +833,18 @@ "confirmRecoveryPhrase": { "message": "Confirm Secret Recovery Phrase" }, - "confirmTitleDescPersonalSignature": { - "message": "Only sign this message if you fully understand the content and trust the requesting site" + "confirmTitleDescContractInteractionTransaction": { + "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." }, - "confirmTitleDescTypedDataSignature": { - "message": "Review everything below before continuing. Once done, this transaction can’t be undone" + "confirmTitleDescSignature": { + "message": "Only confirm this message if you approve the content and trust the requesting site." }, "confirmTitleSignature": { "message": "Signature request" }, + "confirmTitleTransaction": { + "message": "Transaction request" + }, "confirmed": { "message": "Confirmed" }, @@ -854,26 +891,6 @@ "message": "Connect $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Connect to $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Connect to all your $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "accounts", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Connect to $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 accounts", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Connect with MetaMask" }, @@ -894,6 +911,9 @@ "message": "$1 can see the account balance, address, activity, and suggest transactions to approve for connected accounts.", "description": "$1 is the origin name" }, + "connectedAccountsToast": { + "message": "Connected accounts updated" + }, "connectedSites": { "message": "Connected sites" }, @@ -911,11 +931,8 @@ "connectedWith": { "message": "Connected with" }, - "connectedaccountsTabKey": { - "message": "Connected accounts" - }, "connecting": { - "message": "Connecting..." + "message": "Connecting" }, "connectingTo": { "message": "Connecting to $1" @@ -1106,6 +1123,12 @@ "custodianAccountAddedTitle": { "message": "Selected $1 accounts have been added." }, + "custodianQRCodeScan": { + "message": "Scan QR code with your $1 mobile app" + }, + "custodianQRCodeScanDescription": { + "message": "Or log into your $1 account and click on the 'Connect to MMI' button" + }, "custodianReplaceRefreshTokenChangedFailed": { "message": "Please go to $1 and click the 'Connect to MMI' button within their user interface to connect your accounts to MMI again." }, @@ -1386,6 +1409,18 @@ "details": { "message": "Details" }, + "developerOptions": { + "message": "Developer Options" + }, + "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" @@ -1419,6 +1454,14 @@ "disconnectThisAccount": { "message": "Disconnect this account" }, + "disconnectedAllAccountsToast": { + "message": "All accounts disconnected from $1", + "description": "$1 is name of the dapp`" + }, + "disconnectedSingleAccountToast": { + "message": "$1 disconnected from $2", + "description": "$1 is name of the name and $2 represents the dapp name`" + }, "discoverSnaps": { "message": "Discover Snaps", "description": "Text that links to the Snaps website. Displayed in a banner on Snaps list page in settings." @@ -1566,15 +1609,15 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Edit speed up gas fee" }, + "enable": { + "message": "Enable" + }, "enableAutoDetect": { "message": " Enable autodetect" }, "enableFromSettings": { "message": " Enable it from Settings." }, - "enableSmartSwaps": { - "message": "Enable Smart Swaps" - }, "enableSnap": { "message": "Enable" }, @@ -1745,9 +1788,6 @@ "failureMessage": { "message": "Something went wrong, and we were unable to complete the action" }, - "faqAndRiskDisclosures": { - "message": "FAQ and Risk Disclosures" - }, "fast": { "message": "Fast" }, @@ -1826,6 +1866,13 @@ "functionType": { "message": "Function type" }, + "fundYourWallet": { + "message": "Fund your wallet" + }, + "fundYourWalletDescription": { + "message": "Get started by adding some $1 to your wallet.", + "description": "$1 is the token symbol" + }, "gas": { "message": "Gas" }, @@ -1909,11 +1956,13 @@ "genericExplorerView": { "message": "View account on $1" }, - "globalTitle": { - "message": "Global menu" + "getStartedWithNFTs": { + "message": "Get $1 to buy NFTs", + "description": "$1 is the token symbol" }, - "globalTourDescription": { - "message": "See your portfolio, connected sites, settings, and more" + "getStartedWithNFTsDescription": { + "message": "Get started with NFTs by adding some $1 to your wallet.", + "description": "$1 is the token symbol" }, "goBack": { "message": "Go back" @@ -2194,7 +2243,7 @@ "message": "Install origin" }, "installRequest": { - "message": "Installation request" + "message": "Add to MetaMask" }, "installedOn": { "message": "Installed on $1", @@ -2227,6 +2276,9 @@ "interactingWith": { "message": "Interacting with" }, + "interactingWithTransactionDescription": { + "message": "This is the contract you're interacting with. Protect yourself from scammers by verifying the details." + }, "invalidAddress": { "message": "Invalid address" }, @@ -2357,6 +2409,9 @@ "layer1Fees": { "message": "Layer 1 fees" }, + "layer2Fees": { + "message": "Layer 2 fees" + }, "learnCancelSpeeedup": { "message": "Learn how to $1", "description": "$1 is link to cancel or speed up transactions" @@ -2374,6 +2429,9 @@ "learnMoreUpperCase": { "message": "Learn more" }, + "learnMoreUpperCaseWithDot": { + "message": "Learn more." + }, "learnScamRisk": { "message": "scams and security risks." }, @@ -2516,9 +2574,6 @@ "message": "Make sure nobody is looking", "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, - "manageInSettings": { - "message": "Manage in settings" - }, "max": { "message": "Max" }, @@ -2569,6 +2624,12 @@ "metamaskVersion": { "message": "MetaMask Version" }, + "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." + }, "methodNotSupported": { "message": "Not supported with this account." }, @@ -2617,7 +2678,7 @@ "message": "more" }, "multipleSnapConnectionWarning": { - "message": "$1 wants to connect with $2 snaps. Only proceed if you trust this website.", + "message": "$1 wants to use $2 Snaps", "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." }, "mustSelectOne": { @@ -2682,6 +2743,10 @@ "message": "Choose a nickname...", "description": "Placeholder text for name input field in name component modal." }, + "nativePermissionRequestDescription": { + "message": "Do you want this site to do the following?", + "description": "Description below header used on Permission Connect screen for native permissions." + }, "nativeToken": { "message": "The native token on this network is $1. It is the token used for gas fees. ", "description": "$1 represents the name of the native token on the current network" @@ -2877,9 +2942,6 @@ "nftDisclaimer": { "message": "Disclaimer: MetaMask pulls the media file from the source url. This url sometimes gets changed by the marketplace on which the NFT was minted." }, - "nftLearnMore": { - "message": "Learn more about NFTs" - }, "nftOptions": { "message": "NFT Options" }, @@ -2936,6 +2998,9 @@ "noSnaps": { "message": "You don't have any snaps installed." }, + "noThanks": { + "message": "No thanks" + }, "noTransactions": { "message": "You have no transactions" }, @@ -2945,6 +3010,9 @@ "noWebcamFoundTitle": { "message": "Webcam not found" }, + "nonCustodialAccounts": { + "message": "MetaMask Institutional allows you to use non-custodial accounts, if you plan to use these accounts backup the Secret Recovery Phrase." + }, "nonce": { "message": "Nonce" }, @@ -3062,6 +3130,14 @@ "notificationsEmptyText": { "message": "This is where you can find notifications from your installed snaps." }, + "notificationsFeatureToggle": { + "message": "Enable Wallet Notifications", + "description": "Experimental feature title" + }, + "notificationsFeatureToggleDescription": { + "message": "This enables wallet notifications like send/receive funds or nfts and feature announcements.", + "description": "Description of the experimental notifications feature" + }, "notificationsHeader": { "message": "Notifications" }, @@ -3147,6 +3223,9 @@ "on": { "message": "On" }, + "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." }, @@ -3273,7 +3352,8 @@ "message": "A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust." }, "onlyConnectTrust": { - "message": "Only connect with sites you trust." + "message": "Only connect with sites you trust. $1", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openCustodianApp": { "message": "Open $1 app", @@ -3301,9 +3381,6 @@ "operationFailed": { "message": "Operation Failed" }, - "optimismFees": { - "message": "Optimism fees" - }, "optional": { "message": "Optional" }, @@ -3396,9 +3473,6 @@ "permissionRequest": { "message": "Permission request" }, - "permissionRequestCapitalized": { - "message": "Permission request" - }, "permissionRequested": { "message": "Requested now" }, @@ -3628,12 +3702,6 @@ "permissionsPageTourTitle": { "message": "Connected sites are now permissions" }, - "permissionsTitle": { - "message": "Permissions" - }, - "permissionsTourDescription": { - "message": "Find your connected accounts and manage permissions here" - }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." }, @@ -3737,6 +3805,78 @@ "publicAddress": { "message": "Public address" }, + "pushPlatformNotificationsFundsReceivedDescription": { + "message": "You received $1 $2" + }, + "pushPlatformNotificationsFundsReceivedDescriptionDefault": { + "message": "You received some tokens" + }, + "pushPlatformNotificationsFundsReceivedTitle": { + "message": "Funds received" + }, + "pushPlatformNotificationsFundsSentDescription": { + "message": "You successfully sent $1 $2" + }, + "pushPlatformNotificationsFundsSentDescriptionDefault": { + "message": "You successfully sent some tokens" + }, + "pushPlatformNotificationsFundsSentTitle": { + "message": "Funds sent" + }, + "pushPlatformNotificationsNftReceivedDescription": { + "message": "You received new NFTs" + }, + "pushPlatformNotificationsNftReceivedTitle": { + "message": "NFT received" + }, + "pushPlatformNotificationsNftSentDescription": { + "message": "You have successfully sent an NFT" + }, + "pushPlatformNotificationsNftSentTitle": { + "message": "NFT sent" + }, + "pushPlatformNotificationsStakingLidoStakeCompletedDescription": { + "message": "Your Lido stake was successful" + }, + "pushPlatformNotificationsStakingLidoStakeCompletedTitle": { + "message": "Stake complete" + }, + "pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnDescription": { + "message": "Your Lido stake is now ready to be withdrawn" + }, + "pushPlatformNotificationsStakingLidoStakeReadyToBeWithdrawnTitle": { + "message": "Stake ready for withdrawal" + }, + "pushPlatformNotificationsStakingLidoWithdrawalCompletedDescription": { + "message": "Your Lido withdrawal was successful" + }, + "pushPlatformNotificationsStakingLidoWithdrawalCompletedTitle": { + "message": "Withdrawal completed" + }, + "pushPlatformNotificationsStakingLidoWithdrawalRequestedDescription": { + "message": "Your Lido withdrawal request was submitted" + }, + "pushPlatformNotificationsStakingLidoWithdrawalRequestedTitle": { + "message": "Withdrawal requested" + }, + "pushPlatformNotificationsStakingRocketpoolStakeCompletedDescription": { + "message": "Your RocketPool stake was successful" + }, + "pushPlatformNotificationsStakingRocketpoolStakeCompletedTitle": { + "message": "Stake complete" + }, + "pushPlatformNotificationsStakingRocketpoolUnstakeCompletedDescription": { + "message": "Your RocketPool unstake was successful" + }, + "pushPlatformNotificationsStakingRocketpoolUnstakeCompletedTitle": { + "message": "Unstake complete" + }, + "pushPlatformNotificationsSwapCompletedDescription": { + "message": "Your MetaMask Swap was successful" + }, + "pushPlatformNotificationsSwapCompletedTitle": { + "message": "Swap completed" + }, "queued": { "message": "Queued" }, @@ -3755,6 +3895,9 @@ "receive": { "message": "Receive" }, + "receiveTokensCamelCase": { + "message": "Receive tokens" + }, "recipientAddressPlaceholder": { "message": "Enter public address (0x) or ENS name" }, @@ -3788,6 +3931,12 @@ "recoveryPhraseReminderTitle": { "message": "Protect your funds" }, + "redesignedConfirmationsEnabledToggle": { + "message": "Improved signature requests" + }, + "redesignedConfirmationsToggleDescription": { + "message": "Turn this on to see signature requests in an enhanced format." + }, "refreshList": { "message": "Refresh list" }, @@ -3839,6 +3988,9 @@ "removeNFT": { "message": "Remove NFT" }, + "removeNftErrorMessage": { + "message": "We could not remove this NFT." + }, "removeNftMessage": { "message": "NFT was successfully removed!" }, @@ -3876,6 +4028,9 @@ "requestFromInfo": { "message": "This is the site asking for your signature." }, + "requestFromTransactionDescription": { + "message": "This is the site asking for your confirmation." + }, "requestMayNotBeSafe": { "message": "Request may not be safe" }, @@ -3897,6 +4052,9 @@ "reset": { "message": "Reset" }, + "resetStates": { + "message": "Reset States" + }, "resetWallet": { "message": "Reset wallet" }, @@ -4218,6 +4376,9 @@ "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." }, @@ -4353,12 +4514,6 @@ "simulationsSettingSubHeader": { "message": "Estimate balance changes" }, - "siteConnections": { - "message": "Site Connections" - }, - "sites": { - "message": "Sites" - }, "skip": { "message": "Skip" }, @@ -4371,28 +4526,58 @@ "smartContracts": { "message": "Smart contracts" }, - "smartSwaps": { - "message": "Smart Swaps" - }, - "smartSwapsAreHere": { - "message": "Smart Swaps are here!" - }, - "smartSwapsDescription": { - "message": "MetaMask Swaps just got a whole lot smarter! Enabling Smart Swaps will allow MetaMask to programmatically optimize your Swap to help:" - }, - "smartSwapsDescription2": { - "message": "*Smart Swaps will submit your transaction privately. You can opt-out in advanced settings at any time. To learn more about Smart Swaps, read our $1.", - "description": "$1 is an external link to FAQ and Risk Disclosures" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Not enough funds for a smart swap." }, "smartSwapsErrorUnavailable": { "message": "Smart Swaps are temporarily unavailable." }, - "smartSwapsTooltip": { - "message": "Simulate transactions before submitting to decrease transaction costs and reduce failures. To learn more, read our $1", - "description": "$1 is an external link to FAQ and Risk Disclosures" + "smartTransactionCancelled": { + "message": "Your transaction was canceled" + }, + "smartTransactionCancelledDescription": { + "message": "Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees." + }, + "smartTransactionError": { + "message": "Your transaction failed" + }, + "smartTransactionErrorDescription": { + "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." + }, + "smartTransactionPending": { + "message": "Submitting your transaction" + }, + "smartTransactionSuccess": { + "message": "Your transaction is complete" + }, + "smartTransactionTakingTooLong": { + "message": "Sorry for the wait" + }, + "smartTransactionTakingTooLongDescription": { + "message": "If your transaction is not finalized within $1, it will be canceled and you will not be charged for gas.", + "description": "$1 is remaining time in seconds" + }, + "smartTransactions": { + "message": "Smart Transactions" + }, + "smartTransactionsBenefit1": { + "message": "99.5% success rate" + }, + "smartTransactionsBenefit2": { + "message": "Transaction protection" + }, + "smartTransactionsBenefit3": { + "message": "Real-time updates" + }, + "smartTransactionsDescription": { + "message": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions." + }, + "smartTransactionsDescription2": { + "message": "Only available on Ethereum. Enable or disable any time in settings. $1", + "description": "$1 is an external link to learn more about Smart Transactions" + }, + "smartTransactionsOptItModalTitle": { + "message": "Transactions just got smarter" }, "snapAccountCreated": { "message": "Account created" @@ -4433,12 +4618,9 @@ "message": "Accounts controlled by third-party Snaps." }, "snapConnectionWarning": { - "message": "$1 wants to connect to $2. Only continue if you trust this website.", + "message": "$1 wants to use $2", "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." }, - "snapConnections": { - "message": "Snap Connections" - }, "snapContent": { "message": "This content is coming from $1", "description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap." @@ -4447,7 +4629,7 @@ "message": "Website" }, "snapInstallRequest": { - "message": "Installing $1 gives it the following permissions. Only continue if you trust $1.", + "message": "Installing $1 gives it the following permissions.", "description": "$1 is the snap name." }, "snapInstallSuccess": { @@ -4509,8 +4691,8 @@ "description": "Error title used when snap update fails." }, "snapUpdateRequest": { - "message": "Updating $1 to $2 gives it the following permissions. Only continue if you trust $1.", - "description": "$1 is the snap name and $2 is the snap version." + "message": "Updating $1 gives it the following permissions.", + "description": "$1 is the Snap name." }, "snapUpdateSuccess": { "message": "Update complete" @@ -4705,6 +4887,14 @@ "stake": { "message": "Stake" }, + "startYourJourney": { + "message": "Start your journey with $1", + "description": "$1 is the token symbol" + }, + "startYourJourneyDescription": { + "message": "Get started with web3 by adding some $1 to your wallet.", + "description": "$1 is the token symbol" + }, "stateLogError": { "message": "Error in retrieving state logs." }, @@ -4717,6 +4907,9 @@ "stateLogsDescription": { "message": "State logs contain your public account addresses and sent transactions." }, + "states": { + "message": "States" + }, "status": { "message": "Status" }, @@ -4760,18 +4953,6 @@ "strong": { "message": "Strong" }, - "stxBenefit1": { - "message": "Minimize transaction costs" - }, - "stxBenefit2": { - "message": "Reduce transaction failures" - }, - "stxBenefit3": { - "message": "Eliminate stuck transactions" - }, - "stxBenefit4": { - "message": "Prevent front-running" - }, "stxCancelled": { "message": "Swap would have failed" }, @@ -4781,6 +4962,10 @@ "stxCancelledSubDescription": { "message": "Try your swap again. We’ll be here to protect you against similar risks next time." }, + "stxEstimatedCompletion": { + "message": "Estimated completion in < $1", + "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" + }, "stxFailure": { "message": "Swap failed" }, @@ -4788,6 +4973,9 @@ "message": "Sudden market changes can cause failures. If the problem persists, please reach out to $1.", "description": "This message is shown to a user if their swap fails. The $1 will be replaced by support.metamask.io" }, + "stxOptInDescription": { + "message": "Turn on Smart Transactions for more reliable and secure transactions on ETH Mainnet. $1" + }, "stxPendingPrivatelySubmittingSwap": { "message": "Privately submitting your Swap..." }, @@ -5276,14 +5464,15 @@ "switchToThisAccount": { "message": "Switch to this account" }, - "switchedTo": { - "message": "You're now using" + "switchedNetworkToastDecline": { + "message": "Don't show again" }, - "switcherTitle": { - "message": "Network switcher" + "switchedNetworkToastMessage": { + "message": "$1 is now active on $2", + "description": "$1 represents the account name, $2 represents the network name" }, - "switcherTourDescription": { - "message": "Click the icon to switch networks or add a new network" + "switchedTo": { + "message": "You're now using" }, "switchingNetworksCancelsPendingConfirmations": { "message": "Switching networks will cancel all pending confirmations" @@ -5593,6 +5782,12 @@ "tryAgain": { "message": "Try again" }, + "turnOff": { + "message": "Turn off" + }, + "turnOn": { + "message": "Turn on" + }, "turnOnTokenDetection": { "message": "Turn on enhanced token detection" }, @@ -5738,6 +5933,9 @@ "view": { "message": "View" }, + "viewActivity": { + "message": "View activity" + }, "viewAllDetails": { "message": "View all details" }, @@ -5761,7 +5959,7 @@ }, "viewOnCustomBlockExplorer": { "message": "View $1 at $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "View $1 on Etherscan", @@ -5773,6 +5971,9 @@ "viewOnOpensea": { "message": "View on Opensea" }, + "viewTransaction": { + "message": "View transaction" + }, "viewinCustodianApp": { "message": "View in custodian app" }, @@ -5872,10 +6073,6 @@ "whatsThis": { "message": "What's this?" }, - "xOfY": { - "message": "$1 of $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "xOfYPending": { "message": "$1 of $2 pending", "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" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index fbff1412a9e7..98f0b3a45eb5 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Permitir que esta extensiÃŗn externa haga lo siguiente:" - }, "allowSpendToken": { "message": "ÂŋDar permiso para acceder a su $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Permitir que este sitio haga lo siguiente:" - }, - "allowThisSnapTo": { - "message": "Permitir que este snap haga:" - }, "allowWithdrawAndSpend": { "message": "Permitir que se retire $1 y gastar hasta el siguiente importe:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Conectar $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Conectarse a $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Conectarse a todas sus $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "cuentas", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Conectarse a $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 cuentas", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Conectarse con MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "General" }, - "globalTitle": { - "message": "MenÃē global" - }, - "globalTourDescription": { - "message": "Vea su portafolio, sitios conectados, configuraciones y mÃĄs" - }, "goBack": { "message": "Volver" }, @@ -2707,7 +2672,8 @@ "message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza." }, "onlyConnectTrust": { - "message": "ConÊctese solo con sitios de confianza." + "message": "ConÊctese solo con sitios de confianza.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Pase al modo de pantalla completa para conectar su Ledger.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "Solicitud de permiso" }, - "permissionRequestCapitalized": { - "message": "Solicitud de permiso" - }, "permissionRequested": { "message": "Solicitado ahora" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Permisos" }, - "permissionsTitle": { - "message": "Permisos" - }, - "permissionsTourDescription": { - "message": "Encuentre sus cuentas conectadas y administre los permisos aquí" - }, "personalAddressDetected": { "message": "Se detectÃŗ una direcciÃŗn personal. Ingrese la direcciÃŗn de contrato del token." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "Contratos inteligentes" }, - "smartSwapsAreHere": { - "message": "ÂĄLos intercambios inteligentes ya estÃĄn aquí!" - }, - "smartSwapsDescription": { - "message": "ÂĄLa funciÃŗn Intercambios de MetaMask ahora es mucho mÃĄs inteligente! Habilitar Intercambios inteligentes permitirÃĄ que MetaMask optimice mediante programaciÃŗn su intercambio para ayudar a:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "No hay suficientes fondos para un intercambio inteligente." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Sitio web" }, - "snapInstallRequest": { - "message": "La instalaciÃŗn de $1 otorga los siguientes permisos. Solo continÃēe si confía en $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "InstalaciÃŗn completa" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "Fuerte" }, - "stxBenefit1": { - "message": "Minimizar los costos de transacciÃŗn" - }, - "stxBenefit2": { - "message": "Reducir las fallas en las transacciones" - }, - "stxBenefit3": { - "message": "Eliminar las transacciones atascadas" - }, - "stxBenefit4": { - "message": "Prevenir la inversiÃŗn ventajista" - }, "stxCancelled": { "message": "El intercambio habría fallado" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "Ha cambiado a" }, - "switcherTitle": { - "message": "Selector de red" - }, - "switcherTourDescription": { - "message": "Haga clic en el icono para cambiar de red o agregar una nueva red" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Cambiar de red cancelarÃĄ todas las confirmaciones pendientes" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Ver $1 en $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Ver $1 en Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ÂŋQuÊ es esto?" }, - "xOfY": { - "message": "$1 de $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 370c94563e9d..6b01eb14cf43 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -172,16 +172,10 @@ "alerts": { "message": "Alertas" }, - "allowExternalExtensionTo": { - "message": "Permitir que esta extensiÃŗn externa haga lo siguiente:" - }, "allowSpendToken": { "message": "ÂŋDar permiso para acceder a su $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Permitir que este sitio haga lo siguiente:" - }, "allowWithdrawAndSpend": { "message": "Permitir que se retire $1 y gastar hasta el siguiente importe:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -377,26 +371,6 @@ "connectManually": { "message": "Conectarse manualmente al sitio actual" }, - "connectTo": { - "message": "Conectarse a $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Conectarse a todas sus $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "cuentas", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Conectarse a $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 cuentas", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Conectarse con MetaMask" }, @@ -1487,7 +1461,8 @@ "message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza." }, "onlyConnectTrust": { - "message": "ConÊctese solo con sitios de confianza." + "message": "ConÊctese solo con sitios de confianza.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Abra MetaMask en pantalla completa para conectar su Ledger a travÊs de WebHID.", @@ -2543,7 +2518,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Ver $1 en $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Ver $1 en Etherscan", @@ -2618,10 +2593,6 @@ "whatsThis": { "message": "ÂŋQuÊ es esto?" }, - "xOfY": { - "message": "$1 de $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 6a29f4110dca..075d4d823a26 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Autoriser cette extension externe à :" - }, "allowSpendToken": { "message": "Donner l’autorisation d’accÊder à votre $1 ?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Autoriser ce site à :" - }, - "allowThisSnapTo": { - "message": "Autoriser ce snap à :" - }, "allowWithdrawAndSpend": { "message": "Permettre à $1 de retirer et de dÊpenser jusqu’au montant suivant :", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Connecter $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Connectez-vous à $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Connectez-vous à vos $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "comptes", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Connectez-vous à $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 comptes", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Connectez-vous avec MetaMask" }, @@ -1653,12 +1624,6 @@ "genericExplorerView": { "message": "Afficher le compte sur $1" }, - "globalTitle": { - "message": "Menu global" - }, - "globalTourDescription": { - "message": "Voyez votre portfolio, vos sites connectÊs, vos paramètres, et bien plus encore" - }, "goBack": { "message": "Retour" }, @@ -2710,7 +2675,8 @@ "message": "Un fournisseur de rÊseau malveillant peut mentir quant à l’Êtat de la blockchain et enregistrer votre activitÊ rÊseau. N’ajoutez que des rÊseaux personnalisÊs auxquels vous faites confiance." }, "onlyConnectTrust": { - "message": "Ne vous connectez qu’aux sites auxquels vous faites confiance." + "message": "Ne vous connectez qu’aux sites auxquels vous faites confiance.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Passez en plein Êcran pour connecter votre Ledger.", @@ -2807,9 +2773,6 @@ "permissionRequest": { "message": "Demande d’autorisation" }, - "permissionRequestCapitalized": { - "message": "Demande d’autorisation" - }, "permissionRequested": { "message": "DemandÊ maintenant" }, @@ -2891,12 +2854,6 @@ "permissions": { "message": "Autorisations" }, - "permissionsTitle": { - "message": "Autorisations" - }, - "permissionsTourDescription": { - "message": "Trouvez vos comptes connectÊs et gÊrez les autorisations ici" - }, "personalAddressDetected": { "message": "Votre adresse personnelle a ÊtÊ dÊtectÊe. Veuillez saisir à la place l’adresse du contrat du jeton." }, @@ -3483,12 +3440,6 @@ "smartContracts": { "message": "Contrats intelligents" }, - "smartSwapsAreHere": { - "message": "Les contrats de swap intelligents sont enfin arrivÊs !" - }, - "smartSwapsDescription": { - "message": "Les swaps sont devenus beaucoup plus intelligents sur MetaMask ! L’activation des contrats de swap intelligents permettra à MetaMask d’optimiser programmatiquement le processus contractuel pour vous aider à :" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Fonds insuffisants pour souscrire un contrat de swap intelligent." }, @@ -3506,10 +3457,6 @@ "snapDetailWebsite": { "message": "Site web" }, - "snapInstallRequest": { - "message": "L’installation de $1 lui donne les autorisations suivantes. Ne continuez que si vous faites confiance à $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Installation terminÊe" }, @@ -3768,18 +3715,6 @@ "strong": { "message": "Robuste" }, - "stxBenefit1": { - "message": "Minimise les frais de transaction" - }, - "stxBenefit2": { - "message": "RÊduit les Êchecs de transaction" - }, - "stxBenefit3": { - "message": "Élimine les blocages de transaction" - }, - "stxBenefit4": { - "message": "EmpÃĒcher le favoritisme" - }, "stxCancelled": { "message": "Le swap aurait ÊchouÊ" }, @@ -4269,12 +4204,6 @@ "switchedTo": { "message": "Vous ÃĒtes passÊ à" }, - "switcherTitle": { - "message": "Commutateur rÊseau" - }, - "switcherTourDescription": { - "message": "Cliquez sur l’icône pour changer de rÊseau ou ajouter un nouveau rÊseau" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Le changement de rÊseau annulera toutes les confirmations en attente" }, @@ -4710,7 +4639,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Afficher $1 à $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Afficher $1 sur Etherscan", @@ -4814,10 +4743,6 @@ "whatsThis": { "message": "Qu’est-ce que c’est ?" }, - "xOfY": { - "message": "$1 sur $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 7d1c50d48515..e263a2cafec9 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "ā¤‡ā¤¸ ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤ā¤•āĨā¤¸ā¤ŸāĨ‡ā¤‚ā¤ļā¤¨ ā¤•āĨ‹ ā¤‡ā¤¸ā¤•āĨ€ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚:" - }, "allowSpendToken": { "message": "ā¤†ā¤Ēā¤•āĨ‡ $1 ā¤•āĨ‹ ā¤ā¤•āĨā¤¸āĨ‡ā¤¸ ā¤•ā¤°ā¤¨āĨ‡ ā¤•āĨ€ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "ā¤‡ā¤¸ ā¤¸ā¤žā¤‡ā¤Ÿ ā¤•āĨ‹ ā¤‡ā¤¸ā¤•āĨ€ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚:" - }, - "allowThisSnapTo": { - "message": "ā¤‡ā¤¸ Snap ā¤•āĨ‹ ā¤‡ā¤¸ā¤•āĨ€ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚:" - }, "allowWithdrawAndSpend": { "message": "$1 ā¤•āĨ‹ ā¤¨ā¤ŋā¤ŽāĨā¤¨ā¤˛ā¤ŋā¤–ā¤ŋā¤¤ ā¤¤ā¤• ā¤…ā¤Žā¤žā¤‰ā¤‚ā¤Ÿ ā¤¨ā¤ŋā¤•ā¤žā¤˛ā¤¨āĨ‡ ā¤”ā¤° ā¤–ā¤°āĨā¤š ā¤•ā¤°ā¤¨āĨ‡ ā¤•āĨ€ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "$1 ā¤•āĨ‹ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "$1 ā¤¸āĨ‡ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "ā¤…ā¤Ēā¤¨āĨ‡ ā¤¸ā¤­āĨ€ $1 ā¤¸āĨ‡ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "ā¤ā¤•ā¤žā¤‰ā¤‚ā¤ŸāĨā¤¸", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "$1 ā¤¸āĨ‡ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 ā¤ā¤•ā¤žā¤‰ā¤‚ā¤ŸāĨā¤¸", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "MetaMask ā¤•āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚" }, @@ -1650,12 +1621,6 @@ "general": { "message": "ā¤¸ā¤žā¤Žā¤žā¤¨āĨā¤¯" }, - "globalTitle": { - "message": "ā¤ĩāĨˆā¤ļāĨā¤ĩā¤ŋā¤• ā¤ŽāĨ‡ā¤¨āĨā¤¯āĨ‚" - }, - "globalTourDescription": { - "message": "ā¤…ā¤Ēā¤¨ā¤ž ā¤ĒāĨ‹ā¤°āĨā¤Ÿā¤Ģā¤ŧāĨ‹ā¤˛ā¤ŋā¤¯āĨ‹, ā¤œāĨā¤Ąā¤ŧāĨ€ ā¤šāĨā¤ˆ ā¤¸ā¤žā¤‡ā¤ŸāĨ‡ā¤‚, ā¤¸āĨ‡ā¤Ÿā¤ŋā¤‚ā¤—āĨā¤¸ ā¤†ā¤Ļā¤ŋ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚" - }, "goBack": { "message": "ā¤ĩā¤žā¤Ēā¤¸ ā¤œā¤žā¤ā¤‚" }, @@ -2707,7 +2672,8 @@ "message": "ā¤ā¤• ā¤ŦāĨā¤°āĨ€ ā¤¨āĨ€ā¤¯ā¤¤ ā¤ĩā¤žā¤˛ā¤ž ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤ĒāĨā¤°āĨ‹ā¤ĩā¤žā¤‡ā¤Ąā¤° ā¤ŦāĨā¤˛āĨ‰ā¤•ā¤šāĨ‡ā¤¨ ā¤•āĨ€ ā¤¸āĨā¤Ĩā¤ŋā¤¤ā¤ŋ ā¤•āĨ‡ ā¤Ŧā¤žā¤°āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤āĨ‚ā¤  ā¤ŦāĨ‹ā¤˛ ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ ā¤”ā¤° ā¤†ā¤Ēā¤•āĨ€ ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤ā¤•āĨā¤Ÿā¤ŋā¤ĩā¤ŋā¤ŸāĨ€ ā¤•āĨ‹ ā¤°ā¤ŋā¤•āĨ‰ā¤°āĨā¤Ą ā¤•ā¤° ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆāĨ¤ ā¤•āĨ‡ā¤ĩā¤˛ ā¤‰ā¤¨ ā¤•ā¤¸āĨā¤Ÿā¤Ž ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤•āĨ‹ ā¤œāĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚, ā¤œā¤ŋā¤¨ ā¤Ēā¤° ā¤†ā¤Ē ā¤­ā¤°āĨ‹ā¤¸ā¤ž ā¤•ā¤°ā¤¤āĨ‡ ā¤šāĨˆā¤‚āĨ¤" }, "onlyConnectTrust": { - "message": "ā¤•āĨ‡ā¤ĩā¤˛ ā¤‰ā¤¨ ā¤¸ā¤žā¤‡ā¤ŸāĨ‹ā¤‚ ā¤¸āĨ‡ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚, ā¤œā¤ŋā¤¨ ā¤Ēā¤° ā¤†ā¤Ē ā¤­ā¤°āĨ‹ā¤¸ā¤ž ā¤•ā¤°ā¤¤āĨ‡ ā¤šāĨˆā¤‚āĨ¤" + "message": "ā¤•āĨ‡ā¤ĩā¤˛ ā¤‰ā¤¨ ā¤¸ā¤žā¤‡ā¤ŸāĨ‹ā¤‚ ā¤¸āĨ‡ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°āĨ‡ā¤‚, ā¤œā¤ŋā¤¨ ā¤Ēā¤° ā¤†ā¤Ē ā¤­ā¤°āĨ‹ā¤¸ā¤ž ā¤•ā¤°ā¤¤āĨ‡ ā¤šāĨˆā¤‚āĨ¤", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "ā¤…ā¤Ēā¤¨āĨ‡ Ledger ā¤•āĨ‹ ā¤•ā¤¨āĨ‡ā¤•āĨā¤Ÿ ā¤•ā¤°ā¤¨āĨ‡ ā¤•āĨ‡ ā¤˛ā¤ŋā¤ ā¤ĢāĨā¤˛ ā¤¸āĨā¤•āĨā¤°āĨ€ā¤¨ ā¤Ēā¤° ā¤œā¤žā¤ā¤‚āĨ¤", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤…ā¤¨āĨā¤°āĨ‹ā¤§" }, - "permissionRequestCapitalized": { - "message": "ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋ ā¤•āĨ‡ ā¤˛ā¤ŋā¤ ā¤…ā¤¨āĨā¤°āĨ‹ā¤§" - }, "permissionRequested": { "message": "ā¤…ā¤Ŧ ā¤°ā¤ŋā¤•āĨā¤ĩāĨ‡ā¤¸āĨā¤Ÿ ā¤•ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋā¤¯ā¤žā¤" }, - "permissionsTitle": { - "message": "ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋā¤¯ā¤žā¤‚" - }, - "permissionsTourDescription": { - "message": "ā¤…ā¤Ēā¤¨āĨ‡ ā¤œāĨā¤Ąā¤ŧāĨ‡ ā¤šāĨā¤ ā¤…ā¤•ā¤žā¤‰ā¤‚ā¤Ÿ ā¤ĸāĨ‚ā¤‚ā¤ĸāĨ‡ā¤‚ ā¤”ā¤° ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋā¤¯ā¤žā¤‚ ā¤¯ā¤šā¤žā¤‚ ā¤ĒāĨā¤°ā¤Ŧā¤‚ā¤§ā¤ŋā¤¤ ā¤•ā¤°āĨ‡ā¤‚" - }, "personalAddressDetected": { "message": "ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋā¤—ā¤¤ ā¤ā¤ĄāĨā¤°āĨ‡ā¤¸ ā¤•ā¤ž ā¤ā¤ĄāĨā¤°āĨ‡ā¤¸ ā¤šā¤˛ā¤žāĨ¤ ā¤ŸāĨ‹ā¤•ā¤¨ ā¤•āĨ‰ā¤¨āĨā¤ŸāĨā¤°āĨˆā¤•āĨā¤Ÿ ā¤ā¤ĄāĨā¤°āĨ‡ā¤¸ ā¤Ąā¤žā¤˛āĨ‡ā¤‚āĨ¤" }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "ā¤¸āĨā¤Žā¤žā¤°āĨā¤Ÿ ā¤•āĨ‰ā¤¨āĨā¤ŸāĨā¤°āĨˆā¤•āĨā¤ŸāĨā¤¸" }, - "smartSwapsAreHere": { - "message": "ā¤¸āĨā¤Žā¤žā¤°āĨā¤Ÿ ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤¯ā¤šā¤žā¤‚ ā¤šāĨˆā¤‚!" - }, - "smartSwapsDescription": { - "message": "MetaMask ā¤•āĨ‡ ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤…ā¤Ŧ ā¤”ā¤° ā¤…ā¤§ā¤ŋā¤• ā¤¸āĨā¤Žā¤žā¤°āĨā¤Ÿ ā¤šāĨ‹ ā¤—ā¤ ā¤šāĨˆā¤‚! ā¤‡ā¤¨ ā¤šāĨ‡ā¤¤āĨ ā¤¸ā¤šā¤žā¤¯ā¤¤ā¤ž ā¤•āĨ‡ ā¤˛ā¤ŋā¤ ā¤¸āĨā¤Žā¤žā¤°āĨā¤Ÿ ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤•āĨ‹ ā¤‡ā¤¨āĨ‡ā¤Ŧā¤˛ ā¤•ā¤°ā¤¨āĨ‡ ā¤¸āĨ‡ MetaMask ā¤†ā¤Ēā¤•āĨ‡ ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤•āĨ‹ ā¤ĒāĨā¤°āĨ‹ā¤—āĨā¤°ā¤žā¤ŽāĨ‡ā¤Ÿā¤ŋā¤• ā¤°āĨ‚ā¤Ē ā¤¸āĨ‡ ā¤‘ā¤ĒāĨā¤Ÿā¤ŋā¤Žā¤žā¤‡ā¤œ ā¤•ā¤° ā¤Ēā¤žā¤ā¤—ā¤ž:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "ā¤¸āĨā¤Žā¤žā¤°āĨā¤Ÿ ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤•āĨ‡ ā¤˛ā¤ŋā¤ ā¤Ēā¤°āĨā¤¯ā¤žā¤ĒāĨā¤¤ ā¤Ģā¤‚ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤" }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "ā¤ĩāĨ‡ā¤Ŧā¤¸ā¤žā¤‡ā¤Ÿ" }, - "snapInstallRequest": { - "message": "$1 ā¤‡ā¤‚ā¤¸āĨā¤ŸāĨ‰ā¤˛ ā¤•ā¤°ā¤¨āĨ‡ ā¤¸āĨ‡ ā¤‡ā¤¸āĨ‡ ā¤¨ā¤ŋā¤ŽāĨā¤¨ā¤˛ā¤ŋā¤–ā¤ŋā¤¤ ā¤…ā¤¨āĨā¤Žā¤¤ā¤ŋā¤¯ā¤žā¤‚ ā¤Žā¤ŋā¤˛ā¤¤āĨ€ ā¤šāĨˆā¤‚āĨ¤ ā¤…ā¤—ā¤° ā¤†ā¤Ē $1 ā¤Ēā¤° ā¤­ā¤°āĨ‹ā¤¸ā¤ž ā¤•ā¤°ā¤¤āĨ‡ ā¤šāĨˆā¤‚ ā¤¤āĨ‹ ā¤šāĨ€ ā¤œā¤žā¤°āĨ€ ā¤°ā¤–āĨ‡ā¤‚āĨ¤", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "ā¤‡ā¤‚ā¤¸āĨā¤ŸāĨ‰ā¤˛āĨ‡ā¤ļā¤¨ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "ā¤Žā¤œā¤ŦāĨ‚ā¤¤" }, - "stxBenefit1": { - "message": "ā¤ŸāĨā¤°ā¤žā¤‚ā¤¸āĨ‡ā¤•āĨā¤ļā¤¨ ā¤˛ā¤žā¤—ā¤¤āĨ‡ā¤‚ ā¤Žā¤ŋā¤¨ā¤ŋā¤Žā¤žā¤‡ā¤œā¤ŧ ā¤•ā¤°āĨ‡ā¤‚" - }, - "stxBenefit2": { - "message": "ā¤ŸāĨā¤°ā¤žā¤‚ā¤¸āĨ‡ā¤•āĨā¤ļā¤¨ ā¤ĩā¤ŋā¤Ģā¤˛ā¤¤ā¤žā¤ā¤‚ ā¤•ā¤Ž ā¤•ā¤°āĨ‡ā¤‚" - }, - "stxBenefit3": { - "message": "ā¤…ā¤Ÿā¤•āĨ‡ ā¤šāĨā¤ ā¤ŸāĨā¤°ā¤žā¤‚ā¤¸āĨ‡ā¤•āĨā¤ļā¤¨ ā¤•āĨ‹ ā¤šā¤Ÿā¤ž ā¤ĻāĨ‡ā¤‚" - }, - "stxBenefit4": { - "message": "ā¤Ģā¤ŧāĨā¤°ā¤‚ā¤Ÿ-ā¤°ā¤¨ā¤ŋā¤‚ā¤— ā¤•āĨ‹ ā¤°āĨ‹ā¤•āĨ‡ā¤‚" - }, "stxCancelled": { "message": "ā¤¸āĨā¤ĩāĨˆā¤Ē ā¤ĩā¤ŋā¤Ģā¤˛ ā¤šāĨ‹ ā¤¸ā¤•ā¤¤ā¤ž ā¤Ĩā¤ž" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "ā¤†ā¤Ēā¤¨āĨ‡ ā¤¸āĨā¤ĩā¤ŋā¤š ā¤•ā¤° ā¤˛ā¤ŋā¤¯ā¤ž ā¤šāĨˆ" }, - "switcherTitle": { - "message": "ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤¸āĨā¤ĩā¤ŋā¤šā¤°" - }, - "switcherTourDescription": { - "message": "ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤¸āĨā¤ĩā¤ŋā¤š ā¤•ā¤°ā¤¨āĨ‡ ā¤¯ā¤ž ā¤¨ā¤¯ā¤ž ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤œāĨ‹ā¤Ąā¤ŧā¤¨āĨ‡ ā¤•āĨ‡ ā¤˛ā¤ŋā¤ ā¤†ā¤‡ā¤•ā¤¨ ā¤Ēā¤° ā¤•āĨā¤˛ā¤ŋā¤• ā¤•ā¤°āĨ‡ā¤‚" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "ā¤¨āĨ‡ā¤Ÿā¤ĩā¤°āĨā¤• ā¤¸āĨā¤ĩā¤ŋā¤š ā¤•ā¤°ā¤¨āĨ‡ ā¤¸āĨ‡ ā¤¸ā¤­āĨ€ ā¤˛ā¤‚ā¤Ŧā¤ŋā¤¤ ā¤•ā¤¨āĨā¤Ģā¤°āĨā¤ŽāĨ‡ā¤ļā¤¨ ā¤•āĨˆā¤‚ā¤¸ā¤ŋā¤˛ ā¤šāĨ‹ ā¤œā¤žā¤ā¤‚ā¤—āĨ‡" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "$1 ā¤•āĨ‹ $2 ā¤Ēā¤° ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Etherscan ā¤Ēā¤° $1 ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ā¤¯ā¤š ā¤•āĨā¤¯ā¤ž ā¤šāĨˆ?" }, - "xOfY": { - "message": "$2 ā¤ŽāĨ‡ā¤‚ ā¤¸āĨ‡ $1", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 52017a426059..fbaa937695e0 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Izinkan ekstensi eksternal ini untuk:" - }, "allowSpendToken": { "message": "Berikan izin untuk mengakses $1 Anda?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Izinkan situs ini untuk:" - }, - "allowThisSnapTo": { - "message": "Izinkan snap ini untuk:" - }, "allowWithdrawAndSpend": { "message": "Izinkan $1 untuk menarik dan menggunakan hingga jumlah berikut:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Hubungkan $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Hubungkan ke $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Hubungkan ke semua $1 Anda", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "akun", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Hubungkan ke $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 akun", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Hubungkan dengan MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Umum" }, - "globalTitle": { - "message": "Menu global" - }, - "globalTourDescription": { - "message": "Lihat portofolio, situs terhubung, pengaturan, dan lainnya" - }, "goBack": { "message": "Kembali" }, @@ -2707,7 +2672,8 @@ "message": "Penyedia jaringan jahat dapat berbohong tentang status blockchain dan merekam aktivitas jaringan Anda. Hanya tambahkan jaringan kustom yang Anda percayai." }, "onlyConnectTrust": { - "message": "Hanya hubungkan ke situs yang Anda percayai." + "message": "Hanya hubungkan ke situs yang Anda percayai.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Buka layar penuh untuk menghubungkan Ledger Anda.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "Permintaan izin" }, - "permissionRequestCapitalized": { - "message": "Permintaan izin" - }, "permissionRequested": { "message": "Diminta sekarang" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Izin" }, - "permissionsTitle": { - "message": "Izin" - }, - "permissionsTourDescription": { - "message": "Temukan akun yang terhubung dan kelola izin di sini" - }, "personalAddressDetected": { "message": "Alamat pribadi terdeteksi. Masukkan alamat kontrak token." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "Kontrak cerdas" }, - "smartSwapsAreHere": { - "message": "Smart Swap telah hadir!" - }, - "smartSwapsDescription": { - "message": "MetaMask Swaps kini semakin pintar! Mengaktifkan Smart Swap akan mengizinkan MetaMask mengoptimalkan Swap secara terprogram untuk membantu:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Dana tidak cukup untuk pertukaran cerdas." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Situs web" }, - "snapInstallRequest": { - "message": "Menginstal $1 memberinya izin berikut. Lanjutkan hanya jika Anda memercayai $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Instalasi selesai" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "Kuat" }, - "stxBenefit1": { - "message": "Meminimalkan biaya transaksi" - }, - "stxBenefit2": { - "message": "Kurangi potensi kegagalan transaksi \t" - }, - "stxBenefit3": { - "message": "Hapus transaksi yang macet" - }, - "stxBenefit4": { - "message": "Cegah perilaku front running \t" - }, "stxCancelled": { "message": "Pertukaran akan gagal" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "Anda telah beralih ke" }, - "switcherTitle": { - "message": "Pengalih jaringan" - }, - "switcherTourDescription": { - "message": "Klik ikon untuk beralih jaringan atau menambahkan jaringan baru" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Mengalihkan jaringan akan membatalkan semua konfirmasi yang berstatus menunggu" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Lihat $1 di $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Lihat $1 di Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "Apa ini?" }, - "xOfY": { - "message": "$1 dari $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index e56fbdab8df1..668d3d34fdc4 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -241,16 +241,10 @@ "message": "Tutti i tuoi $1", "description": "$1 is the symbol or name of the token that the user is approving spending" }, - "allowExternalExtensionTo": { - "message": "Permetti a questa estensione di:" - }, "allowSpendToken": { "message": "Dai il permesso di spendere tuoi $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Permetti a questo sito di:" - }, "allowWithdrawAndSpend": { "message": "Consenti a $1 di ritirare e spendere fino a questo importo:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -456,26 +450,6 @@ "connectManually": { "message": "Connettiti al sito manualmente" }, - "connectTo": { - "message": "Connettiti a $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Connettiti a tutti i tuoi $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "account", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Connettiti a $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 account", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Connetti con MetaMask" }, @@ -1195,7 +1169,8 @@ "message": "Una rete malevola puÃ˛ mentire sullo stato della blockchain e registrare le tue azioni. Aggiungi solo reti fidate." }, "onlyConnectTrust": { - "message": "Connettiti solo con siti di cui ti fidi." + "message": "Connettiti solo con siti di cui ti fidi.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "origin": { "message": "Origine" @@ -1825,10 +1800,6 @@ "whatsThis": { "message": "Cos'è?" }, - "xOfY": { - "message": "$1 di $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "youNeedToAllowCameraAccess": { "message": "Devi consentire l'accesso alla fotocamera per usare questa funzionalità." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 2c096d1b1b52..a6159449e7fc 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "ã“ãŽå¤–éƒ¨æ‹Ąåŧĩ抟čƒŊãĢæŦĄãŽæ“äŊœã‚’č¨ąå¯ã—ãžã™" - }, "allowSpendToken": { "message": "$1へぎã‚ĸクã‚ģã‚šč¨ąå¯ã‚’ä¸Žãˆãžã™ã‹īŧŸ", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "こぎã‚ĩイトãĢæŦĄãŽæ“äŊœã‚’č¨ąå¯ã—ãžã™" - }, - "allowThisSnapTo": { - "message": "こぎsnapãĢæŦĄãŽæ“äŊœã‚’č¨ąå¯ã—ãžã™:" - }, "allowWithdrawAndSpend": { "message": "$1ãĢäģĨä¸‹ãŽéĄãžã§ãŽåŧ•ãå‡ēしとäŊŋį”¨ã‚’č¨ąå¯ã—ãžã™ã€‚", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "$1をæŽĨįļš", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "$1ãĢæŽĨįļš", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "すずãĻぎ$1ãĢæŽĨįļš", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "ã‚ĸã‚Ģã‚Ļãƒŗト", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "$1ãĢæŽĨįļš", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1ã‚ĸã‚Ģã‚Ļãƒŗト", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "MetaMaskをäŊŋį”¨ã—ãĻæŽĨįļš" }, @@ -1650,12 +1621,6 @@ "general": { "message": "一čˆŦ" }, - "globalTitle": { - "message": "グロãƒŧバãƒĢãƒĄãƒ‹ãƒĨãƒŧ" - }, - "globalTourDescription": { - "message": "ポãƒŧトフりãƒĒã‚Ē、æŽĨįļšã•ã‚ŒãŸã‚ĩã‚¤ãƒˆã€č¨­åŽšãĒおをįĸēčĒã§ããžã™" - }, "goBack": { "message": "æˆģる" }, @@ -2707,7 +2672,8 @@ "message": "æ‚Ē意ぎあるネットワãƒŧク プロバイダãƒŧは、ブロックチェãƒŧãƒŗぎ゚テãƒŧトをåŊり、ãƒĻãƒŧã‚ļãƒŧぎネットワãƒŧクã‚ĸクテã‚Ŗビテã‚Ŗã‚’č¨˜éŒ˛ã™ã‚‹ã“ã¨ãŒã‚ã‚Šãžã™ã€‚äŋĄé ŧするã‚Ģã‚šã‚ŋムネットワãƒŧクぎãŋをčŋŊ加しãĻください。" }, "onlyConnectTrust": { - "message": "äŋĄé ŧするã‚ĩイトãĢぎãŋæŽĨįļšã—ãĻください。" + "message": "äŋĄé ŧするã‚ĩイトãĢぎãŋæŽĨįļšã—ãĻください。", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "全į”ģéĸãƒĸãƒŧドãĢしãĻLedgerをæŽĨįļšã—ぞす。", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "č¨ąå¯ãŽãƒĒクエ゚ト" }, - "permissionRequestCapitalized": { - "message": "č¨ąå¯ãŽãƒĒクエ゚ト" - }, "permissionRequested": { "message": "įžåœ¨ãƒĒクエ゚ト中" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "č¨ąå¯" }, - "permissionsTitle": { - "message": "č¨ąå¯" - }, - "permissionsTourDescription": { - "message": "ここでæŽĨįļšã•ã‚ŒãŸã‚ĸã‚Ģã‚ĻãƒŗトをčĻ‹ã¤ã‘ãĻč¨ąå¯ãŽįŽĄį†ã‚’čĄŒã„ãžã™" - }, "personalAddressDetected": { "message": "個äēēã‚ĸドãƒŦ゚が検å‡ēされぞした。トãƒŧクãƒŗã‚ŗãƒŗトナクトã‚ĸドãƒŦã‚šã‚’å…Ĩ力しãĻください。" }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "゚マãƒŧトã‚ŗãƒŗトナクト" }, - "smartSwapsAreHere": { - "message": "゚マãƒŧト゚ワップぎį™ģ場ですīŧ" - }, - "smartSwapsDescription": { - "message": "MetaMask SwapsがはるかãĢčŗĸくãĒりぞしたīŧã‚šãƒžãƒŧト゚ワップを有劚ãĢすると、MetaMaskがプログナムãĢåž“ãŖãĻ゚ワップを最遊化できるようãĢãĒるため、äģĨ下ぎようãĒãƒĄãƒĒットがありぞす。" - }, "smartSwapsErrorNotEnoughFunds": { "message": "゚マãƒŧト゚ワップãĢåŋ…čĻãĒčŗ‡é‡‘が不čļŗしãĻいぞす。" }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Webã‚ĩイト" }, - "snapInstallRequest": { - "message": "$1をイãƒŗ゚トãƒŧãƒĢすることで、æŦĄãŽã‚ĸクã‚ģã‚šč¨ąå¯ãŒäģ˜ä¸Žã•ã‚Œãžã™ã€‚$1をäŋĄé ŧできる場合ãĢぎãŋįļščĄŒã—ãĻください。", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "イãƒŗ゚トãƒŧãƒĢ厌äē†" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "åŧˇ" }, - "stxBenefit1": { - "message": "トナãƒŗã‚ļã‚¯ã‚ˇãƒ§ãƒŗã‚ŗ゚トを最小化" - }, - "stxBenefit2": { - "message": "トナãƒŗã‚ļã‚¯ã‚ˇãƒ§ãƒŗãŽå¤ąæ•—æ•°ã‚’äŊŽæ¸›" - }, - "stxBenefit3": { - "message": "トナãƒŗã‚ļã‚¯ã‚ˇãƒ§ãƒŗぎ停æģžã‚’č§Ŗæļˆ" - }, - "stxBenefit4": { - "message": "フロãƒŗトナãƒŗニãƒŗã‚°ã‚’é˜˛æ­ĸ" - }, "stxCancelled": { "message": "ã‚šãƒ¯ãƒƒãƒ—ãŒå¤ąæ•—ã™ã‚‹ã¨ã“ã‚ã§ã—ãŸ" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "æŦĄãĢ切りæ›ŋえぞした:" }, - "switcherTitle": { - "message": "ネットワãƒŧク゚イッチãƒŖãƒŧ" - }, - "switcherTourDescription": { - "message": "ã‚ĸイã‚ŗãƒŗをクãƒĒックしãĻネットワãƒŧクを切りæ›ŋえるか、新しいネットワãƒŧクをčŋŊ加しぞす" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "ネットワãƒŧクを切りæ›ŋえると、äŋį•™ä¸­ãŽæ‰ŋčĒãŒã™ãšãĻキãƒŖãƒŗã‚ģãƒĢされぞす" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "$1を$2ã§čĄ¨į¤ē", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "$1をEtherscanã§čĄ¨į¤ē", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "これはäŊ•ã§ã™ã‹īŧŸ" }, - "xOfY": { - "message": "$2中ぎ$1", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index ad6702b20e57..c547cb7354a3 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "ė´ ė™¸ëļ€ 확ėžĨė„ 다ėŒė— 허ėšŠ:" - }, "allowSpendToken": { "message": "$1ė— ė•Ąė„¸ėŠ¤í•  ėˆ˜ ėžˆëŠ” ęļŒí•œė„ ëļ€ė—Ŧ하ė‹œę˛ ėŠĩ니까?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "다ėŒė— ė´ ė‚Ŧė´íŠ¸ëĨŧ 허ėšŠ:" - }, - "allowThisSnapTo": { - "message": "다ėŒė— ė´ ėŠ¤ëƒ…ė„ 허ėšŠ:" - }, "allowWithdrawAndSpend": { "message": "$1ė—ė„œ 다ėŒ 금ė•ĄęšŒė§€ ė¸ėļœ 및 ė§€ėļœí•˜ë„록 허ėšŠ:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "$1 ė—°ę˛°", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "$1ė— ė—°ę˛°", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "ëĒ¨ë“  $1ė— ė—°ę˛°", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "ęŗ„ė •", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "$1ė— ė—°ę˛°", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1개 ęŗ„ė •", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "MetaMask로 ė—°ę˛°" }, @@ -1650,12 +1621,6 @@ "general": { "message": "ėŧ반" }, - "globalTitle": { - "message": "글로벌 메뉴" - }, - "globalTourDescription": { - "message": "íŦ트폴ëĻŦė˜¤, ė—°ę˛°ëœ ė‚Ŧė´íŠ¸, ė„¤ė • 등ė„ 확ė¸í•˜ė„¸ėš”" - }, "goBack": { "message": "뒤로 가기" }, @@ -2707,7 +2672,8 @@ "message": "ė•…ė„ą 네트ė›ŒíŦ ęŗĩ급ė—…ė˛´ëŠ” 블록ė˛´ė¸ ėƒíƒœëĨŧ ęą°ė§“ėœŧ로 ëŗ´ęŗ í•˜ęŗ  네트ė›ŒíŦ 활동ė„ 기록할 ėˆ˜ ėžˆėŠĩ니다. ė‹ ëĸ°í•˜ëŠ” 맞ėļ¤ 네트ė›ŒíŦ만 ėļ”ę°€í•˜ė„¸ėš”." }, "onlyConnectTrust": { - "message": "ė‹ ëĸ°í•˜ëŠ” ė‚Ŧė´íŠ¸ë§Œ ė—°ę˛°í•˜ė„¸ėš”." + "message": "ė‹ ëĸ°í•˜ëŠ” ė‚Ŧė´íŠ¸ë§Œ ė—°ę˛°í•˜ė„¸ėš”.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "ė „ė˛´ 화면ėœŧ로 ė´ë™í•˜ė—Ŧ LedgerëĨŧ ė—°ę˛°í•˜ė„¸ėš”.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "ęļŒí•œ ėš”ė˛­" }, - "permissionRequestCapitalized": { - "message": "ęļŒí•œ ėš”ė˛­" - }, "permissionRequested": { "message": "ė§€ę¸ˆ ėš”ė˛­ë¨" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "ęļŒí•œ" }, - "permissionsTitle": { - "message": "ęļŒí•œ" - }, - "permissionsTourDescription": { - "message": "ė—°ę˛°ëœ ęŗ„ė •ė„ ė°žė•„ ė—Ŧ기ė„œ ęļŒí•œė„ 관ëĻŦ하ė„¸ėš”" - }, "personalAddressDetected": { "message": "개ė¸ ėŖŧė†Œę°€ 발ę˛Ŧ되ė—ˆėŠĩ니다. 토큰 ęŗ„ė•Ŋ ėŖŧė†ŒëĨŧ ėž…ë Ĩ하ė„¸ėš”." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "ėŠ¤ë§ˆíŠ¸ ęŗ„ė•Ŋ" }, - "smartSwapsAreHere": { - "message": "ėŠ¤ë§ˆíŠ¸ ėŠ¤ė™‘ė´ ė‹œėž‘되ė—ˆėŠĩ니다!" - }, - "smartSwapsDescription": { - "message": "MetaMask ėŠ¤ė™‘ė´ 더ėšą ėŠ¤ë§ˆíŠ¸í•´ėĄŒėŠĩ니다! ėŠ¤ë§ˆíŠ¸ ėŠ¤ė™‘ė„ 활ė„ąí™”하늴 MetaMask가 프로그램ė„ í†ĩ해 ėŠ¤ė™‘ė„ ėĩœė í™”하ė—Ŧ 다ėŒęŗŧ 같ė€ 활동ė— 도ė›€ė„ 드ëĻŊ니다." - }, "smartSwapsErrorNotEnoughFunds": { "message": "ėŠ¤ë§ˆíŠ¸ ėŠ¤ė™‘ ėžę¸ˆ ëļ€ėĄą" }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "ė›šė‚Ŧė´íŠ¸" }, - "snapInstallRequest": { - "message": "$1 ė„¤ėš˜ëŠ” 다ėŒęŗŧ 같ė€ ęļŒí•œė„ 허ėšŠí•Šë‹ˆë‹¤. $1 ėŠ¤ëƒ…ė„ ė‹ ëĸ°í•˜ëŠ” ę˛Ŋėš°ė—ë§Œ ęŗ„ė† ė§„행하ė„¸ėš”.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "ė„¤ėš˜ ė™„ëŖŒ" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "강함" }, - "stxBenefit1": { - "message": "트랜ėž­ė…˜ 비ėšŠ ėĩœė†Œí™”í•˜ę¸°" - }, - "stxBenefit2": { - "message": "트랜ėž­ė…˜ ė‹¤íŒ¨ ė¤„ė´ę¸°" - }, - "stxBenefit3": { - "message": "ė¤‘단된 트랜ėž­ė…˜ ė œęą°í•˜ę¸°" - }, - "stxBenefit4": { - "message": "프런트 ëŸŦ닝 ë°Šė§€" - }, "stxCancelled": { "message": "ėŠ¤ė™‘ė´ ė‹¤íŒ¨í–ˆė„ 것ėž…니다" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "다ėŒėœŧ로 ëŗ€ę˛Ŋ했ėŠĩ니다:" }, - "switcherTitle": { - "message": "네트ė›ŒíŦ ė „í™˜ę¸°" - }, - "switcherTourDescription": { - "message": "ė•„ė´ėŊ˜ė„ 클ëĻ­í•˜ëŠ´ 네트ė›ŒíŦ가 ëŗ€ę˛Ŋ되거나 ėƒˆëĄœėš´ 네트ė›ŒíŦ가 ėļ”ę°€ëŠë‹ˆë‹¤" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "네트ė›ŒíŦëĨŧ ė „환하늴 대기 ė¤‘ė¸ ëĒ¨ë“  ėģ¨íŽŒ ėž‘ė—…ė´ ėˇ¨ė†ŒëŠë‹ˆë‹¤." }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "$2ė—ė„œ $1 ëŗ´ę¸°", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Etherscanė—ė„œ $1 ëŗ´ę¸°", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ė´ę˛ƒė€ ëŦ´ė—‡ė¸ę°€ėš”?" }, - "xOfY": { - "message": "$1/$2개", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 9f33919237fa..ba8979bc4a95 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -96,12 +96,6 @@ "alerts": { "message": "Mga Alerto" }, - "allowExternalExtensionTo": { - "message": "Payagan ang external extension na ito na:" - }, - "allowThisSiteTo": { - "message": "Payagan ang site na ito na:" - }, "allowWithdrawAndSpend": { "message": "Payagan ang $1 na mag-withdraw at gastusin ang sumusunod na halaga:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -249,26 +243,6 @@ "connectManually": { "message": "Manu-manong kumonekta sa kasalukuyang site" }, - "connectTo": { - "message": "Kumonekta sa $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Ikonekta sa lahat ng iyong $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "mga account", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Kumonekta sa $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "Mga $1 account", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Kumonekta sa MetaMask" }, @@ -960,7 +934,8 @@ "message": "Magagawa ng nakakapinsalang network provider na magsinungaling tungkol sa status ng blockchain at itala ang aktibidad ng iyong network. Magdagdag lang ng mga custom na network na pinagkakatiwalaan mo." }, "onlyConnectTrust": { - "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo." + "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "origin": { "message": "Pinagmulan" @@ -1774,10 +1749,6 @@ "whatsThis": { "message": "Ano ito?" }, - "xOfY": { - "message": "$1 ng $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "xOfYPending": { "message": "$1 sa $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" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index adfdccf4280b..7f8f53cf83ac 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Permitir que essa extensÃŖo externa:" - }, "allowSpendToken": { "message": "VocÃĒ dÃĄ permissÃŖo para acessar seus $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Permitir que esse site:" - }, - "allowThisSnapTo": { - "message": "Permitir que esse snap:" - }, "allowWithdrawAndSpend": { "message": "Permitir que $1 saque e gaste atÊ o seguinte valor:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Conectar $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Conectar a $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Conecte-se a todas as suas $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "contas", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Conecte-se a $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 contas", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Conectar-se com a MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Geral" }, - "globalTitle": { - "message": "Menu global" - }, - "globalTourDescription": { - "message": "Veja seu portfÃŗlio, sites conectados, configuraçÃĩes e mais" - }, "goBack": { "message": "Voltar" }, @@ -2707,7 +2672,8 @@ "message": "Um provedor de rede mal-intencionado pode mentir sobre o estado da blockchain e registrar as atividades da sua rede. Adicione somente as redes personalizadas em que vocÃĒ confia." }, "onlyConnectTrust": { - "message": "Conecte-se somente com sites em que vocÃĒ confia." + "message": "Conecte-se somente com sites em que vocÃĒ confia.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Abra o app em tela cheia para conectar seu Ledger.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "SolicitaçÃŖo de permissÃŖo" }, - "permissionRequestCapitalized": { - "message": "SolicitaçÃŖo de permissÃŖo" - }, "permissionRequested": { "message": "Solicitada agora" }, @@ -2892,12 +2855,6 @@ "permissions": { "message": "PermissÃĩes" }, - "permissionsTitle": { - "message": "PermissÃĩes" - }, - "permissionsTourDescription": { - "message": "Encontre suas contas conectadas e gerencie permissÃĩes aqui" - }, "personalAddressDetected": { "message": "Endereço pessoal detectado. Insira o endereço de contrato do token." }, @@ -3484,12 +3441,6 @@ "smartContracts": { "message": "Contratos inteligentes" }, - "smartSwapsAreHere": { - "message": "As trocas inteligentes chegaram!" - }, - "smartSwapsDescription": { - "message": "As trocas na MetaMask ficaram muito mais inteligentes! Ativar as trocas inteligentes permitirÃĄ que a MetaMask otimize programaticamente sua troca para ajudar:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Fundos insuficientes para uma troca inteligente." }, @@ -3507,10 +3458,6 @@ "snapDetailWebsite": { "message": "Site" }, - "snapInstallRequest": { - "message": "Instalar $1 concede as permissÃĩes a seguir. Continue apenas se vocÃĒ confia em $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "InstalaçÃŖo concluída" }, @@ -3769,18 +3716,6 @@ "strong": { "message": "Forte" }, - "stxBenefit1": { - "message": "Minimize os custos das transaçÃĩes" - }, - "stxBenefit2": { - "message": "Reduza as falhas nas transaçÃĩes" - }, - "stxBenefit3": { - "message": "Elimine transaçÃĩes travadas" - }, - "stxBenefit4": { - "message": "Previna o front-running (uso de informaçÃĩes privilegiadas para negociaçÃĩes)" - }, "stxCancelled": { "message": "A troca teria falhado" }, @@ -4270,12 +4205,6 @@ "switchedTo": { "message": "VocÃĒ alternou para" }, - "switcherTitle": { - "message": "Alternador de redes" - }, - "switcherTourDescription": { - "message": "Clique no ícone para alternar as redes ou adicionar uma nova" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "A alternÃĸncia de redes cancelarÃĄ todas as confirmaçÃĩes pendentes" }, @@ -4711,7 +4640,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Ver $1 em $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Ver $1 no Etherscan", @@ -4815,10 +4744,6 @@ "whatsThis": { "message": "O que Ê isso?" }, - "xOfY": { - "message": "$1 de $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index c2fe68724a82..b837d881daf3 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -172,16 +172,10 @@ "alerts": { "message": "Alertas" }, - "allowExternalExtensionTo": { - "message": "Permitir que essa extensÃŖo externa:" - }, "allowSpendToken": { "message": "VocÃĒ concede acesso aos seus $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Permitir que esse site:" - }, "allowWithdrawAndSpend": { "message": "Permitir que $1 saque e gaste atÊ o seguinte valor:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -377,26 +371,6 @@ "connectManually": { "message": "Conectar manualmente ao site atual" }, - "connectTo": { - "message": "Conectar a $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Conecte-se a todas as suas $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "contas", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Conecte-se a $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 contas", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Conectar-se com a MetaMask" }, @@ -1491,7 +1465,8 @@ "message": "Um provedor de rede mal-intencionado pode mentir sobre o estado do blockchain e registrar as atividades da sua rede. Adicione somente as redes personalizadas em que vocÃĒ confia." }, "onlyConnectTrust": { - "message": "Conecte-se somente com sites em que vocÃĒ confia." + "message": "Conecte-se somente com sites em que vocÃĒ confia.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Abra a MetaMask em tela cheia para conectar sua ledger por meio do WebHID.", @@ -2547,7 +2522,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Ver $1 em $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Ver $1 no Etherscan", @@ -2622,10 +2597,6 @@ "whatsThis": { "message": "O que Ê isso?" }, - "xOfY": { - "message": "$1 de $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "xOfYPending": { "message": "$1 de $2 pendente", "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" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index bd3b27f8beae..e200ec9a99f1 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "РаСŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ŅŅ‚ĐžĐŧŅƒ вĐŊĐĩŅˆĐŊĐĩĐŧŅƒ Ņ€Đ°ŅŅˆĐ¸Ņ€ĐĩĐŊиŅŽ:" - }, "allowSpendToken": { "message": "РаСŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ Đ´ĐžŅŅ‚ŅƒĐŋ Đē ваŅˆĐĩĐŧŅƒ $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "РаСŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ŅŅ‚ĐžĐŧŅƒ ŅĐ°ĐšŅ‚Ņƒ:" - }, - "allowThisSnapTo": { - "message": "РаСŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚ snap Đē:" - }, "allowWithdrawAndSpend": { "message": "РаСŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ $1 ŅĐŊŅŅ‚ŅŒ и ĐŋĐžŅ‚Ņ€Đ°Ņ‚иŅ‚ŅŒ Đ´Đž ŅĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐš ŅŅƒĐŧĐŧŅ‹:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "ПодĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Đē $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "ПодĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Đē $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "ПодĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ ĐēĐž вŅĐĩĐŧ ваŅˆĐ¸Đŧ $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "ŅŅ‡ĐĩŅ‚Đ°Đŧ", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "ПодĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Đē $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 ŅŅ‡ĐĩŅ‚Đ°Đŧ", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "ПодĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ Ņ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "ОбŅ‰Đ¸Đĩ" }, - "globalTitle": { - "message": "ГĐģОйаĐģŅŒĐŊĐžĐĩ ĐŧĐĩĐŊŅŽ" - }, - "globalTourDescription": { - "message": "ПŅ€ĐžŅĐŧĐ°Ņ‚Ņ€Đ¸Đ˛Đ°ĐšŅ‚Đĩ ŅĐ˛ĐžĐš Portfolio, ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đĩ ŅĐ°ĐšŅ‚Ņ‹, ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи и ĐŧĐŊĐžĐŗĐžĐĩ Đ´Ņ€ŅƒĐŗĐžĐĩ" - }, "goBack": { "message": "Назад" }, @@ -2707,7 +2672,8 @@ "message": "ВŅ€ĐĩĐ´ĐžĐŊĐžŅĐŊŅ‹Đš ŅĐĩŅ‚ĐĩвОК ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ ĐŧĐžĐļĐĩŅ‚ Đ´ĐĩСиĐŊŅ„ĐžŅ€ĐŧиŅ€ĐžĐ˛Đ°Ņ‚ŅŒ Đž ŅĐžŅŅ‚ĐžŅĐŊии ĐąĐģĐžĐēŅ‡ĐĩĐšĐŊĐ° и СаĐŋиŅŅ‹Đ˛Đ°Ņ‚ŅŒ ваŅˆĐ¸ Đ´ĐĩĐšŅŅ‚виŅ в ŅĐĩŅ‚и. ДобавĐģŅĐšŅ‚Đĩ Ņ‚ĐžĐģŅŒĐēĐž Ņ‚Đĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅŒŅĐēиĐĩ ŅĐĩŅ‚и, ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧ дОвĐĩŅ€ŅĐĩŅ‚Đĩ." }, "onlyConnectTrust": { - "message": "ПодĐēĐģŅŽŅ‡Đ°ĐšŅ‚ĐĩŅŅŒ Ņ‚ĐžĐģŅŒĐēĐž Đē ŅĐ°ĐšŅ‚Đ°Đŧ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧ дОвĐĩŅ€ŅĐĩŅ‚Đĩ." + "message": "ПодĐēĐģŅŽŅ‡Đ°ĐšŅ‚ĐĩŅŅŒ Ņ‚ĐžĐģŅŒĐēĐž Đē ŅĐ°ĐšŅ‚Đ°Đŧ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧ дОвĐĩŅ€ŅĐĩŅ‚Đĩ.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "ПĐĩŅ€ĐĩКдиŅ‚Đĩ в ĐŋĐžĐģĐŊĐžŅĐēŅ€Đ°ĐŊĐŊŅ‹Đš Ņ€ĐĩĐļиĐŧ, Ņ‡Ņ‚ОйŅ‹ ĐŋОдĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅĐ˛ĐžĐš Ledger.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "ЗаĐŋŅ€ĐžŅ Ņ€Đ°ĐˇŅ€ĐĩŅˆĐĩĐŊиŅ" }, - "permissionRequestCapitalized": { - "message": "ЗаĐŋŅ€ĐžŅ Ņ€Đ°ĐˇŅ€ĐĩŅˆĐĩĐŊиŅ" - }, "permissionRequested": { "message": "ЗаĐŋŅ€ĐžŅˆĐĩĐŊĐž ŅĐĩĐšŅ‡Đ°Ņ" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "РаСŅ€ĐĩŅˆĐĩĐŊиŅ" }, - "permissionsTitle": { - "message": "РаСŅ€ĐĩŅˆĐĩĐŊиŅ" - }, - "permissionsTourDescription": { - "message": "НайдиŅ‚Đĩ ŅĐ˛ĐžĐ¸ ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ‹Đĩ ŅŅ‡ĐĩŅ‚Đ° и ŅƒĐŋŅ€Đ°Đ˛ĐģŅĐšŅ‚Đĩ Ņ€Đ°ĐˇŅ€ĐĩŅˆĐĩĐŊиŅĐŧи СдĐĩŅŅŒ" - }, "personalAddressDetected": { "message": "ОбĐŊĐ°Ņ€ŅƒĐļĐĩĐŊ ĐģиŅ‡ĐŊŅ‹Đš Đ°Đ´Ņ€ĐĩŅ. ВвĐĩдиŅ‚Đĩ Đ°Đ´Ņ€ĐĩŅ ĐēĐžĐŊŅ‚Ņ€Đ°ĐēŅ‚Đ° Ņ‚ĐžĐēĐĩĐŊĐ°." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "ĐĄĐŧĐ°Ņ€Ņ‚-ĐēĐžĐŊŅ‚Ņ€Đ°ĐēŅ‚Ņ‹" }, - "smartSwapsAreHere": { - "message": "ПоŅĐ˛Đ¸ĐģиŅŅŒ ŅĐŧĐ°Ņ€Ņ‚-ŅĐ˛ĐžĐŋŅ‹!" - }, - "smartSwapsDescription": { - "message": "ХвОĐŋŅ‹ MetaMask ŅŅ‚Đ°Đģи ĐŊĐ°ĐŧĐŊĐžĐŗĐž ŅƒĐŧĐŊĐĩĐĩ! ВĐēĐģŅŽŅ‡ĐĩĐŊиĐĩ ŅĐŧĐ°Ņ€Ņ‚-ŅĐ˛ĐžĐŋОв ĐŋОСвОĐģиŅ‚ MetaMask ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŧĐŊĐž ĐžĐŋŅ‚иĐŧиСиŅ€ĐžĐ˛Đ°Ņ‚ŅŒ ваŅˆ ŅĐ˛ĐžĐŋ, Ņ‡Ņ‚ОйŅ‹ ĐŋĐžĐŧĐžŅ‡ŅŒ:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "НĐĩĐ´ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž ŅŅ€ĐĩĐ´ŅŅ‚в Đ´ĐģŅ ŅĐŧĐ°Ņ€Ņ‚-ŅĐ˛ĐžĐŋĐ°." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "ВĐĩĐą-ŅĐ°ĐšŅ‚" }, - "snapInstallRequest": { - "message": "ĐŖŅŅ‚Đ°ĐŊОвĐēĐ° $1 Đ´Đ°ĐĩŅ‚ ĐĩĐŧŅƒ ŅĐģĐĩĐ´ŅƒŅŽŅ‰Đ¸Đĩ Ņ€Đ°ĐˇŅ€ĐĩŅˆĐĩĐŊиŅ. ПŅ€ĐžĐ´ĐžĐģĐļĐ°ĐšŅ‚Đĩ, Ņ‚ĐžĐģŅŒĐēĐž ĐĩŅĐģи дОвĐĩŅ€ŅĐĩŅ‚Đĩ $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "ĐŖŅŅ‚Đ°ĐŊОвĐēĐ° СавĐĩŅ€ŅˆĐĩĐŊĐ°" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "ХиĐģŅŒĐŊŅ‹Đš" }, - "stxBenefit1": { - "message": "МиĐŊиĐŧиСиŅ€ŅƒĐšŅ‚Đĩ Ņ‚Ņ€Đ°ĐŊСаĐēŅ†Đ¸ĐžĐŊĐŊŅ‹Đĩ иСдĐĩŅ€ĐļĐēи" - }, - "stxBenefit2": { - "message": "ĐŖĐŧĐĩĐŊŅŒŅˆĐ¸Ņ‚Đĩ ĐēĐžĐģиŅ‡ĐĩŅŅ‚вО ŅĐąĐžĐĩв Ņ‚Ņ€Đ°ĐŊСаĐēŅ†Đ¸Đš" - }, - "stxBenefit3": { - "message": "ĐŖŅŅ‚Ņ€Đ°ĐŊиŅ‚Đĩ СавиŅĐ°ĐŊиĐĩ Ņ‚Ņ€Đ°ĐŊСаĐēŅ†Đ¸Đš" - }, - "stxBenefit4": { - "message": "ПŅ€ĐĩĐ´ĐžŅ‚вŅ€Đ°Ņ‚иŅ‚Đĩ ĐžĐŋĐĩŅ€ĐĩĐļĐĩĐŊиĐĩ" - }, "stxCancelled": { "message": "ХвОĐŋ ĐąŅ‹ ĐŊĐĩ ŅƒĐ´Đ°ĐģŅŅ" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "ВŅ‹ ĐŋĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ¸ĐģиŅŅŒ ĐŊĐ°" }, - "switcherTitle": { - "message": "ПĐĩŅ€ĐĩĐēĐģŅŽŅ‡Đ°Ņ‚ĐĩĐģŅŒ ŅĐĩŅ‚и" - }, - "switcherTourDescription": { - "message": "НаĐļĐŧиŅ‚Đĩ ĐŊĐ° СĐŊĐ°Ņ‡ĐžĐē Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐēĐģŅŽŅ‡ĐĩĐŊиŅ ŅĐĩŅ‚ĐĩĐš иĐģи дОйавĐģĐĩĐŊиŅ ĐŊОвОК ŅĐĩŅ‚и" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "В ŅĐģŅƒŅ‡Đ°Đĩ ŅĐŧĐĩĐŊŅ‹ ŅĐĩŅ‚ĐĩĐš вŅĐĩ ĐžĐļидаŅŽŅ‰Đ¸Đĩ ĐŋОдŅ‚вĐĩŅ€ĐļĐ´ĐĩĐŊиŅ ĐąŅƒĐ´ŅƒŅ‚ ĐžŅ‚ĐŧĐĩĐŊĐĩĐŊŅ‹" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "ĐĄĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ $1 в $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "ĐĄĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ 1$ ĐŊĐ° Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ЧŅ‚Đž ŅŅ‚Đž?" }, - "xOfY": { - "message": "$1 иС $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 115104b327dd..8e1fe644a228 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Payagan ang external extension na ito na:" - }, "allowSpendToken": { "message": "Magbigay ng pahintulot na ma-access ang iyong $1?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Payagan ang site na ito na:" - }, - "allowThisSnapTo": { - "message": "Payagan ang snap na ito sa:" - }, "allowWithdrawAndSpend": { "message": "Payagan ang $1 na mag-withdraw at gastusin ang sumusunod na halaga:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Ikonekta ang $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Kumonekta sa $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Ikonekta sa lahat ng iyong $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "mga account", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Kumonekta sa $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "Mga $1 account", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Kumonekta sa MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Pangkalahatan" }, - "globalTitle": { - "message": "Global menu" - }, - "globalTourDescription": { - "message": "Tingnan ang iyong portfolio, mga nakakonektang site, setting at marami pang iba" - }, "goBack": { "message": "Bumalik" }, @@ -2707,7 +2672,8 @@ "message": "Ang isang mapaminsalang network provider ay maaaring magsinungaling tungkol sa estado ng blockchain at itala ang iyong aktibidad sa network. Magdagdag lamang ng mga custom na network na pinagkakatiwalaan mo." }, "onlyConnectTrust": { - "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo." + "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Pumunta sa full screen para ikonekta ang iyong Ledger.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "Kahilingan sa pahintulot" }, - "permissionRequestCapitalized": { - "message": "Kahilingan sa pahintulot" - }, "permissionRequested": { "message": "Hiniling ngayon" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Mga Pahintulot" }, - "permissionsTitle": { - "message": "Mga Pahintulot" - }, - "permissionsTourDescription": { - "message": "Hanapin ang mga nakakonekta mong account at pamahalaan ang mga pahintulot dito" - }, "personalAddressDetected": { "message": "Natukoy ang personal na address. Ilagay ang address ng kontrata ng token." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "Mga smart na kontrata" }, - "smartSwapsAreHere": { - "message": "Nandito na ang mga Smart Swap!" - }, - "smartSwapsDescription": { - "message": "Mas humusay pa ang mga MetaMask Swap! Ang pag-enable sa mga Smart Swap ay magbibigay-daan sa MetaMask na i-optimize ang iyong Swap gamit ang program para makatulong na:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Hindi sapat ang pondo para sa smart swap." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Website" }, - "snapInstallRequest": { - "message": "Ang pag-install ng $1 ay nagbibigay dito ng mga sumusunod na pahintulot. Magpatuloy lang kung pinagkakatiwalaan mo ang $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Tapos na ang pag-install" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "Mahirap" }, - "stxBenefit1": { - "message": "Bawasan ang mga gastos sa transaksyon" - }, - "stxBenefit2": { - "message": "Bawasan ang mga nabigong transaksyon" - }, - "stxBenefit3": { - "message": "Alisin ang mga hindi umuusad na transaksyon" - }, - "stxBenefit4": { - "message": "Pigilan ang front-running" - }, "stxCancelled": { "message": "Nabigo sana ang pag-swap kung" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "Lumipat ka sa" }, - "switcherTitle": { - "message": "Network switcher" - }, - "switcherTourDescription": { - "message": "I-click ang icon para lumipat ng network o magdagdag ng bagong network" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Ang paglipat ng network ay magkakansela ng lahat ng nakabinbing kumpirmasyon" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Tingnan ang $1 sa $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Tingnan ang $1 sa Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "Ano ito?" }, - "xOfY": { - "message": "$1 ng $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 0b268dcd5a9e..21fa9e79b8e1 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Bu harici uzantÄąnÄąn şunu yapmasÄąna izin ver:" - }, "allowSpendToken": { "message": "$1 erişimine izin ver?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Bu sitenin şunu yapmasÄąna izin ver:" - }, - "allowThisSnapTo": { - "message": "Bu snap için şuna izin verin:" - }, "allowWithdrawAndSpend": { "message": "$1 için şu tutara kadar para çekme ve harcama izni ver:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Şuna bağlanÄąn: $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "$1 uygulamasÄąna bağlan", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "TÃŧmÃŧne bağlan: $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "hesaplar", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Bağlan: $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 hesap", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "MetaMask ile Bağlan" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Genel" }, - "globalTitle": { - "message": "Genel menÃŧ" - }, - "globalTourDescription": { - "message": "PortfÃļyÃŧnÃŧze, bağlÄą sitelere, ayarlara ve daha fazlasÄąna bakÄąn" - }, "goBack": { "message": "Geri git" }, @@ -2707,7 +2672,8 @@ "message": "KÃļtÃŧ amaçlÄą bir ağ sağlayÄącÄą blokzinciri durumu hakkÄąnda yalan sÃļyleyebilir ve ağ aktivitenizi kaydedebilir. Sadece gÃŧvendiğiniz Ãļzel ağlarÄą ekleyin." }, "onlyConnectTrust": { - "message": "Sadece gÃŧvendiğiniz sitelere bağlayÄąn." + "message": "Sadece gÃŧvendiğiniz sitelere bağlayÄąn.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Ledger'ÄąnÄązÄą bağlamak için tam ekrana gidin.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "Ä°zin talebi" }, - "permissionRequestCapitalized": { - "message": "Ä°zin talebi" - }, "permissionRequested": { "message": "Şimdi talep edildi" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Ä°zinler" }, - "permissionsTitle": { - "message": "Ä°zinler" - }, - "permissionsTourDescription": { - "message": "Burada bağlÄą hesaplarÄąnÄązÄą bulun ve izinleri yÃļnetin" - }, "personalAddressDetected": { "message": "Kişisel adres algÄąlandÄą. Token sÃļzleşme adresini girin." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "AkÄąllÄą sÃļzleşmeler" }, - "smartSwapsAreHere": { - "message": "AkÄąllÄą Swap'lar burada!" - }, - "smartSwapsDescription": { - "message": "MetaMask Swap işlemleri artÄąk çok daha akÄąllÄą! AkÄąllÄą Swap'larÄą etkinleştirmek, MetaMask'in aşağıdakilere yardÄąmcÄą olmak için Swap'ini programlÄą olarak optimize etmesine olanak tanÄąr:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "AkÄąllÄą swap için yeterli para yok." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Web Sitesi" }, - "snapInstallRequest": { - "message": "$1 yÃŧklendiğinde aşağıdaki izinler verilir. Sadece $1 adlÄą snape gÃŧveniyorsanÄąz devam edin.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "YÃŧkleme tamamlandÄą" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "GÃŧçlÃŧ" }, - "stxBenefit1": { - "message": "İşlem maliyetlerini en aza indir" - }, - "stxBenefit2": { - "message": "İşlem hatalarÄąnÄą azalt" - }, - "stxBenefit3": { - "message": "SÄąkÄąÅŸmÄąÅŸ işlemleri ortadan kaldÄąr" - }, - "stxBenefit4": { - "message": "Önden çalÄąÅŸtÄąrmayÄą engelle" - }, "stxCancelled": { "message": "Swap işlemi başarÄąsÄąz olurdu" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "Şuna geçiş yaptÄąnÄąz:" }, - "switcherTitle": { - "message": "Ağ değiştirici" - }, - "switcherTourDescription": { - "message": "Ağ değiştirmek veya yeni bir ağ eklemek için simgeye tÄąklayÄąn" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Ağ değiştirmek bekleyen tÃŧm onaylarÄą iptal eder" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "$1 Ãļgesini $2 Ãŧzerinde gÃļrÃŧntÃŧle", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Etherscan'de $1 gÃļrÃŧntÃŧle", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "Bu nedir?" }, - "xOfY": { - "message": "$1 / $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 62a63f8e1ec7..b77ef6777e9a 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "Cho phÊp tiáģ‡n ích máģŸ ráģ™ng bÃĒn ngoài này:" - }, "allowSpendToken": { "message": "CáēĨp quyáģn truy cáē­p vào $1 cáģ§a báēĄn?", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "Cho phÊp trang web này:" - }, - "allowThisSnapTo": { - "message": "Cho phÊp Snap này:" - }, "allowWithdrawAndSpend": { "message": "Cho phÊp $1 rÃēt và chi tiÃĒu táģ‘i đa sáģ‘ tiáģn sau đÃĸy:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "Káēŋt náģ‘i $1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "Káēŋt náģ‘i váģ›i $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "Káēŋt náģ‘i váģ›i táēĨt cáēŖ cÃĄc $1 cáģ§a báēĄn", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "tài khoáēŖn", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "Káēŋt náģ‘i váģ›i $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 tài khoáēŖn", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "Káēŋt náģ‘i váģ›i MetaMask" }, @@ -1650,12 +1621,6 @@ "general": { "message": "Chung" }, - "globalTitle": { - "message": "TrÃŦnh Ä‘ÆĄn chung" - }, - "globalTourDescription": { - "message": "Xem danh máģĨc đáē§u tÆ°, trang web đÃŖ káēŋt náģ‘i, cài đáēˇt, v.v... cáģ§a báēĄn" - }, "goBack": { "message": "Quay LáēĄi" }, @@ -2707,7 +2672,8 @@ "message": "Máģ™t nhà cung cáēĨp máēĄng đáģ™c háēĄi cÃŗ tháģƒ nÃŗi dáģ‘i váģ tráēĄng thÃĄi cáģ§a chuáģ—i kháģ‘i và ghi láēĄi hoáēĄt đáģ™ng cáģ§a báēĄn trÃĒn máēĄng. Cháģ‰ thÃĒm cÃĄc máēĄng tÚy cháģ‰nh mà báēĄn tin tÆ°áģŸng." }, "onlyConnectTrust": { - "message": "Cháģ‰ káēŋt náģ‘i váģ›i cÃĄc trang web mà báēĄn tin tÆ°áģŸng." + "message": "Cháģ‰ káēŋt náģ‘i váģ›i cÃĄc trang web mà báēĄn tin tÆ°áģŸng.", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "Báē­t toàn màn hÃŦnh đáģƒ káēŋt náģ‘i váģ›i thiáēŋt báģ‹ Ledger cáģ§a báēĄn.", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "YÃĒu cáē§u cáēĨp quyáģn" }, - "permissionRequestCapitalized": { - "message": "YÃĒu cáē§u cáēĨp quyáģn" - }, "permissionRequested": { "message": "ĐÃŖ yÃĒu cáē§u ngay" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "Quyáģn" }, - "permissionsTitle": { - "message": "Quyáģn" - }, - "permissionsTourDescription": { - "message": "TÃŦm tài khoáēŖn đÃŖ káēŋt náģ‘i cáģ§a báēĄn và quáēŖn lÃŊ quyáģn táēĄi đÃĸy" - }, "personalAddressDetected": { "message": "ĐÃŖ tÃŦm tháēĨy đáģ‹a cháģ‰ cÃĄ nhÃĸn. Nháē­p đáģ‹a cháģ‰ háģŖp đáģ“ng token." }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "HáģŖp đáģ“ng thông minh" }, - "smartSwapsAreHere": { - "message": "HoÃĄn đáģ•i thông minh đÃŖ ra máē¯t!" - }, - "smartSwapsDescription": { - "message": "Tính năng HoÃĄn đáģ•i cáģ§a MetaMask nay đÃŖ thông minh hÆĄn ráēĨt nhiáģu! Kích hoáēĄt HoÃĄn đáģ•i thông minh sáēŊ cho phÊp MetaMask táģ‘i Æ°u quy trÃŦnh HoÃĄn đáģ•i đáģƒ giÃēp báēĄn:" - }, "smartSwapsErrorNotEnoughFunds": { "message": "Không cÃŗ đáģ§ tiáģn đáģƒ tháģąc hiáģ‡n hoÃĄn đáģ•i thông minh." }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "Trang web" }, - "snapInstallRequest": { - "message": "Cài đáēˇt $1 đáģƒ đưáģŖc cáēĨp cÃĄc quyáģn sau. Cháģ‰ tiáēŋp táģĨc náēŋu báēĄn tin tÆ°áģŸng $1.", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "Cài đáēˇt hoàn táēĨt" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "MáēĄnh" }, - "stxBenefit1": { - "message": "GiáēŖm thiáģƒu chi phí giao dáģ‹ch" - }, - "stxBenefit2": { - "message": "GiáēŖm táģˇ láģ‡ tháēĨt báēĄi khi giao dáģ‹ch" - }, - "stxBenefit3": { - "message": "LoáēĄi báģ cÃĄc giao dáģ‹ch báģ‹ máē¯c káēšt" - }, - "stxBenefit4": { - "message": "Ngăn cháēˇn giao dáģ‹ch cháēĄy trÆ°áģ›c" - }, "stxCancelled": { "message": "HoÃĄn đáģ•i sáēŊ tháēĨt báēĄi" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "BáēĄn đÃŖ chuyáģƒn sang" }, - "switcherTitle": { - "message": "TrÃŦnh chuyáģƒn máēĄng" - }, - "switcherTourDescription": { - "message": "NháēĨp vào biáģƒu tÆ°áģŖng đáģƒ chuyáģƒn máēĄng hoáēˇc thÃĒm máēĄng máģ›i" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "Khi báēĄn chuyáģƒn máēĄng, máģi xÃĄc nháē­n đang cháģ xáģ­ lÃŊ sáēŊ báģ‹ háģ§y" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "Xem $1 táēĄi $2", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "Xem $1 trÃĒn Etherscan", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "ĐÃĸy là gÃŦ?" }, - "xOfY": { - "message": "$1/$2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 8e2a95ae4ecb..20d08a9cab47 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -348,19 +348,10 @@ "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" }, - "allowExternalExtensionTo": { - "message": "å…čŽ¸æ­¤å¤–éƒ¨æ‰Šåą•į¨‹åēīŧš" - }, "allowSpendToken": { "message": "授äēˆčŽŋ闎您įš„ $1 įš„权限īŧŸ", "description": "$1 is the symbol of the token that are requesting to spend" }, - "allowThisSiteTo": { - "message": "å…čŽ¸æ­¤įŊ‘įĢ™īŧš" - }, - "allowThisSnapTo": { - "message": "å…čŽ¸æ­¤ Snapīŧš" - }, "allowWithdrawAndSpend": { "message": "å…čŽ¸ $1 提取和æļˆč´šæœ€éĢ˜äģĨ下金éĸīŧš", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -743,26 +734,6 @@ "message": "čŋžæŽĨ$1", "description": "$1 is the snap for which a connection is being requested." }, - "connectTo": { - "message": "čŋžæŽĨ到 $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "čŋžæŽĨ到您įš„全部 $1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "č´Ļæˆˇ", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "čŋžæŽĨ到 $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 ä¸Ēč´Ļæˆˇ", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "与 MetaMask čŋžæŽĨ" }, @@ -1650,12 +1621,6 @@ "general": { "message": "通į”¨" }, - "globalTitle": { - "message": "å…¨åą€čœå•" - }, - "globalTourDescription": { - "message": "æŸĨįœ‹æ‚¨įš„投čĩ„įģ„åˆã€åˇ˛čŋžæŽĨįš„įŊ‘įĢ™ã€čŽžįŊŽį­‰į­‰" - }, "goBack": { "message": "čŋ”回" }, @@ -2707,7 +2672,8 @@ "message": "æļ意įŊ‘įģœæäž›å•†å¯čƒŊäŧšč°ŽæŠĨåŒē块铞įš„įŠļ态åšļ莰åŊ•æ‚¨įš„įŊ‘įģœæ´ģ动。åĒæˇģ加您äŋĄäģģįš„č‡Ē厚䚉įŊ‘įģœã€‚" }, "onlyConnectTrust": { - "message": "åĒčŋžæŽĨ您äŋĄäģģįš„įŊ‘įĢ™ã€‚" + "message": "åĒčŋžæŽĨ您äŋĄäģģįš„įŊ‘įĢ™ã€‚", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "openFullScreenForLedgerWebHid": { "message": "å…¨åąæ‰“åŧ€äģĨčŋžæŽĨ您įš„ Ledger。", @@ -2804,9 +2770,6 @@ "permissionRequest": { "message": "æƒé™č¯ˇæą‚" }, - "permissionRequestCapitalized": { - "message": "æƒé™č¯ˇæą‚" - }, "permissionRequested": { "message": "įĢ‹åŗč¯ˇæą‚" }, @@ -2888,12 +2851,6 @@ "permissions": { "message": "权限" }, - "permissionsTitle": { - "message": "权限" - }, - "permissionsTourDescription": { - "message": "在此处æŸĨįœ‹æ‚¨įš„åˇ˛čŋžæŽĨč´ĻæˆˇåšļįŽĄį†æƒé™" - }, "personalAddressDetected": { "message": "æŖ€æĩ‹åˆ°ä¸Ēäēēåœ°å€ã€‚č¯ˇčž“å…ĨäģŖ币合įēĻ地址。" }, @@ -3480,12 +3437,6 @@ "smartContracts": { "message": "æ™ēčƒŊ合įēĻ" }, - "smartSwapsAreHere": { - "message": "æ™ēčƒŊ兑æĸåˇ˛æŽ¨å‡ēīŧ" - }, - "smartSwapsDescription": { - "message": "MetaMask Swaps 变垗更加æ™ēčƒŊīŧå¯į”¨æ™ēčƒŊ兑æĸäŊŋåž— MetaMask 在įŧ–į¨‹æ–šéĸčŽŠæ‚¨įš„å…‘æĸäŊ“éĒŒæ›´åŠ äŧ˜åŒ–īŧŒæœ‰åŠŠäēŽīŧš" - }, "smartSwapsErrorNotEnoughFunds": { "message": "æ˛Ąæœ‰čļŗ够įš„čĩ„金čŋ›čĄŒæ™ēčƒŊ兑æĸ。" }, @@ -3503,10 +3454,6 @@ "snapDetailWebsite": { "message": "įŊ‘įĢ™" }, - "snapInstallRequest": { - "message": "厉čŖ…$1īŧŒå°†å‘å…ļ授äēˆäģĨ下权限。åĒ有在您äŋĄäģģ$1įš„情å†ĩ下才čƒŊįģ§įģ­ã€‚", - "description": "$1 is the snap name." - }, "snapInstallSuccess": { "message": "厉čŖ…厌成" }, @@ -3765,18 +3712,6 @@ "strong": { "message": "åŧē" }, - "stxBenefit1": { - "message": "将äē¤æ˜“成æœŦ减č‡ŗ最äŊŽ" - }, - "stxBenefit2": { - "message": "减少äē¤æ˜“å¤ąč´Ĩ" - }, - "stxBenefit3": { - "message": "æļˆé™¤åĄäŊįš„äē¤æ˜“" - }, - "stxBenefit4": { - "message": "防æ­ĸæŠĸ先äē¤æ˜“" - }, "stxCancelled": { "message": "äē¤æĸå°ąäŧšå¤ąč´Ĩ" }, @@ -4266,12 +4201,6 @@ "switchedTo": { "message": "æ‚¨åˇ˛åˆ‡æĸ到" }, - "switcherTitle": { - "message": "įŊ‘įģœåˆ‡æĸåˇĨå…ˇ" - }, - "switcherTourDescription": { - "message": "į‚šå‡ģ此回标äģĨ切æĸįŊ‘įģœæˆ–æˇģ加新įŊ‘įģœ" - }, "switchingNetworksCancelsPendingConfirmations": { "message": "切æĸįŊ‘įģœå°†å–æļˆæ‰€æœ‰åž…处į†įš„įĄŽčŽ¤" }, @@ -4707,7 +4636,7 @@ }, "viewOnCustomBlockExplorer": { "message": "在 $2 上æŸĨįœ‹ $1", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "在 Etherscan 上æŸĨįœ‹ $1", @@ -4811,10 +4740,6 @@ "whatsThis": { "message": "čŋ™æ˜¯äģ€äšˆīŧŸ" }, - "xOfY": { - "message": "$1 / $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index ce0fc3d38a96..d3d6207ae492 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -92,12 +92,6 @@ "alerts": { "message": "提醒" }, - "allowExternalExtensionTo": { - "message": "å…č¨ąé€™å€‹å¤–éƒ¨æ“´å……åŠŸčƒŊīŧš" - }, - "allowThisSiteTo": { - "message": "å…č¨ąé€™å€‹įļ˛įĢ™īŧš" - }, "allowWithdrawAndSpend": { "message": "å…č¨ą $1 提æŦžæˆ–æœ€å¤ščŠąč˛ģäģĨ下額åēĻīŧš", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -245,26 +239,6 @@ "connectManually": { "message": "手動é€Ŗįĩåˆ°į›Žå‰įš„įļ˛įĢ™" }, - "connectTo": { - "message": "é€Ŗįĩåˆ° $1", - "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" - }, - "connectToAll": { - "message": "é€Ŗįĩåˆ°æ‚¨įš„全部$1", - "description": "$1 will be replaced by the translation of connectToAllAccounts" - }, - "connectToAllAccounts": { - "message": "å¸ŗæˆļ", - "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" - }, - "connectToMultiple": { - "message": "é€Ŗįĩåˆ° $1", - "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" - }, - "connectToMultipleNumberOfAccounts": { - "message": "$1 個å¸ŗæˆļ", - "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" - }, "connectWithMetaMask": { "message": "é€Ŗįĩåˆ° MetaMask" }, @@ -959,7 +933,8 @@ "message": "æƒĄæ„įš„įļ˛čˇ¯æäž›č€…可äģĨæŦēé¨™æ‚¨å€åĄŠéˆä¸Šįš„į‹€æ…‹æˆ–č¨˜éŒ„æ‚¨įš„įļ˛čˇ¯æ´ģ動。čĢ‹åĒ新åĸžæ‚¨äŋĄäģģįš„č‡Ē訂įļ˛čˇ¯ã€‚" }, "onlyConnectTrust": { - "message": "記äŊīŧŒåĒé€Ŗįˇšåˆ°æ‚¨äŋĄäģģįš„įļ˛įĢ™ã€‚" + "message": "記äŊīŧŒåĒé€Ŗįˇšåˆ°æ‚¨äŋĄäģģįš„įļ˛įĢ™ã€‚", + "description": "Text displayed above the buttons for connection confirmation. $1 is the link to the learn more web page." }, "origin": { "message": "來æē" @@ -1483,7 +1458,7 @@ }, "viewOnCustomBlockExplorer": { "message": "在 $1 į€čĻŊ", - "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Explorer URL" }, "viewOnEtherscan": { "message": "在 Etherscan 上į€čĻŊ", @@ -1509,10 +1484,6 @@ "whatsThis": { "message": "這是äģ€éēŧīŧŸ" }, - "xOfY": { - "message": "$1 之 $2", - "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" - }, "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" diff --git a/app/background.html b/app/background.html index 148842e8f868..c0068295d730 100644 --- a/app/background.html +++ b/app/background.html @@ -4,7 +4,7 @@ - - + + diff --git a/app/home.html b/app/home.html index 3210c30475c9..b94800b13d78 100644 --- a/app/home.html +++ b/app/home.html @@ -8,7 +8,7 @@ <% } else { %> MetaMask <% } %> - +
@@ -16,6 +16,6 @@
- + diff --git a/app/images/arbitrum.svg b/app/images/arbitrum.svg index 8863afe882c8..9c19a48f2d72 100644 --- a/app/images/arbitrum.svg +++ b/app/images/arbitrum.svg @@ -1,12 +1,40 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/filecoin.svg b/app/images/filecoin.svg new file mode 100644 index 000000000000..0dacb5b294ad --- /dev/null +++ b/app/images/filecoin.svg @@ -0,0 +1,22 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/images/logo/metamask-smart-transactions.png b/app/images/logo/metamask-smart-transactions.png deleted file mode 100644 index 6eed753120bf..000000000000 Binary files a/app/images/logo/metamask-smart-transactions.png and /dev/null differ diff --git a/app/images/ramps-card-activity-illustration.png b/app/images/ramps-card-activity-illustration.png new file mode 100644 index 000000000000..0a0d839e8fc8 Binary files /dev/null and b/app/images/ramps-card-activity-illustration.png differ diff --git a/app/images/ramps-card-nft-illustration.png b/app/images/ramps-card-nft-illustration.png new file mode 100644 index 000000000000..1cbc824592f8 Binary files /dev/null and b/app/images/ramps-card-nft-illustration.png differ diff --git a/app/images/ramps-card-token-illustration.png b/app/images/ramps-card-token-illustration.png new file mode 100644 index 000000000000..7a226ede84db Binary files /dev/null and b/app/images/ramps-card-token-illustration.png differ diff --git a/app/images/scroll.svg b/app/images/scroll.svg new file mode 100644 index 000000000000..456c75922ac5 --- /dev/null +++ b/app/images/scroll.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/app/images/smart-transactions/smart-transactions-opt-in-background.svg b/app/images/smart-transactions/smart-transactions-opt-in-background.svg new file mode 100644 index 000000000000..965a348c487a --- /dev/null +++ b/app/images/smart-transactions/smart-transactions-opt-in-background.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/loading.html b/app/loading.html index dd5902a09a39..1c85bd296cc6 100644 --- a/app/loading.html +++ b/app/loading.html @@ -1,9 +1,11 @@ - - + <% if (it.shouldIncludeSnow) { %> + + + <% } %> MetaMask Loading diff --git a/app/manifest/v2/_base.json b/app/manifest/v2/_base.json index c130b94ae1fb..4197c535972e 100644 --- a/app/manifest/v2/_base.json +++ b/app/manifest/v2/_base.json @@ -31,12 +31,12 @@ { "matches": ["file://*/*", "http://*/*", "https://*/*"], "js": [ - "disable-console.js", - "lockdown-install.js", - "lockdown-run.js", - "lockdown-more.js", - "contentscript.js", - "inpage.js" + "scripts/disable-console.js", + "scripts/lockdown-install.js", + "scripts/lockdown-run.js", + "scripts/lockdown-more.js", + "scripts/contentscript.js", + "scripts/inpage.js" ], "run_at": "document_start", "all_frames": true diff --git a/app/manifest/v3/_base.json b/app/manifest/v3/_base.json index 42df2d437671..39e96825add8 100644 --- a/app/manifest/v3/_base.json +++ b/app/manifest/v3/_base.json @@ -14,7 +14,7 @@ }, "author": "https://metamask.io", "background": { - "service_worker": "app-init.js" + "service_worker": "scripts/app-init.js" }, "commands": { "_execute_browser_action": { @@ -30,11 +30,11 @@ { "matches": ["file://*/*", "http://*/*", "https://*/*"], "js": [ - "disable-console.js", - "lockdown-install.js", - "lockdown-run.js", - "lockdown-more.js", - "contentscript.js" + "scripts/disable-console.js", + "scripts/lockdown-install.js", + "scripts/lockdown-run.js", + "scripts/lockdown-more.js", + "scripts/contentscript.js" ], "run_at": "document_start", "all_frames": true diff --git a/app/manifest/v3/chrome.json b/app/manifest/v3/chrome.json index e056267926d5..79656e26f0f9 100644 --- a/app/manifest/v3/chrome.json +++ b/app/manifest/v3/chrome.json @@ -1,6 +1,6 @@ { "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'none'; frame-ancestors 'none';" + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-ancestors 'none';" }, "externally_connectable": { "matches": ["https://metamask.io/*"], diff --git a/app/notification.html b/app/notification.html index a0e0f6dcf5a7..70f02e9ab421 100644 --- a/app/notification.html +++ b/app/notification.html @@ -32,7 +32,7 @@ margin-top: 1rem; } - +
@@ -50,11 +50,6 @@ />
- + diff --git a/app/popup.html b/app/popup.html index c27442faa6af..296b0ceae711 100644 --- a/app/popup.html +++ b/app/popup.html @@ -4,7 +4,7 @@ MetaMask - +
@@ -12,6 +12,6 @@
- + diff --git a/app/scripts/app-init.js b/app/scripts/app-init.js index 579f2b96bdca..e58f760095a8 100644 --- a/app/scripts/app-init.js +++ b/app/scripts/app-init.js @@ -1,14 +1,12 @@ -/* global chrome */ // This file is used only for manifest version 3 // Represents if importAllScripts has been run // eslint-disable-next-line let scriptsLoadInitiated = false; - +const { chrome } = globalThis; const testMode = process.env.IN_TEST; const loadTimeLogs = []; - // eslint-disable-next-line import/unambiguous function tryImport(...fileNames) { try { @@ -51,33 +49,41 @@ function importAllScripts() { const startImportScriptsTime = Date.now(); + // value of useSnow below is dynamically replaced at build time with actual value + const useSnow = process.env.USE_SNOW; + if (typeof useSnow !== 'boolean') { + throw new Error('Missing USE_SNOW environment variable'); + } + // value of applyLavaMoat below is dynamically replaced at build time with actual value const applyLavaMoat = process.env.APPLY_LAVAMOAT; if (typeof applyLavaMoat !== 'boolean') { throw new Error('Missing APPLY_LAVAMOAT environment variable'); } - loadFile('./sentry-install.js'); + loadFile('../scripts/sentry-install.js'); - // eslint-disable-next-line no-undef - const isWorker = !self.document; - if (!isWorker) { - loadFile('./snow.js'); - } + if (useSnow) { + // eslint-disable-next-line no-undef + const isWorker = !self.document; + if (!isWorker) { + loadFile('../scripts/snow.js'); + } - loadFile('./use-snow.js'); + loadFile('../scripts/use-snow.js'); + } // Always apply LavaMoat in e2e test builds, so that we can capture initialization stats if (testMode || applyLavaMoat) { - loadFile('./runtime-lavamoat.js'); - loadFile('./lockdown-more.js'); - loadFile('./policy-load.js'); + loadFile('../scripts/runtime-lavamoat.js'); + loadFile('../scripts/lockdown-more.js'); + loadFile('../scripts/policy-load.js'); } else { - loadFile('./init-globals.js'); - loadFile('./lockdown-install.js'); - loadFile('./lockdown-run.js'); - loadFile('./lockdown-more.js'); - loadFile('./runtime-cjs.js'); + loadFile('../scripts/init-globals.js'); + loadFile('../scripts/lockdown-install.js'); + loadFile('../scripts/lockdown-run.js'); + loadFile('../scripts/lockdown-more.js'); + loadFile('../scripts/runtime-cjs.js'); } // This environment variable is set to a string of comma-separated relative file paths. @@ -145,7 +151,7 @@ const registerInPageContentScript = async () => { { id: 'inpage', matches: ['file://*/*', 'http://*/*', 'https://*/*'], - js: ['inpage.js'], + js: ['scripts/inpage.js'], runAt: 'document_start', world: 'MAIN', }, diff --git a/app/scripts/background.js b/app/scripts/background.js index f154dbb10a0a..ba0d940ce657 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -61,7 +61,11 @@ import rawFirstTimeState from './first-time-state'; import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code'; import getObjStructure from './lib/getObjStructure'; import setupEnsIpfsResolver from './lib/ens-ipfs/setup'; -import { deferredPromise, getPlatform } from './lib/util'; +import { + deferredPromise, + getPlatform, + shouldEmitDappViewedEvent, +} from './lib/util'; /* eslint-enable import/first */ @@ -283,13 +287,16 @@ async function initialize() { ///: END:ONLY_INCLUDE_IF let isFirstMetaMaskControllerSetup; + if (isManifestV3) { // Save the timestamp immediately and then every `SAVE_TIMESTAMP_INTERVAL` // miliseconds. This keeps the service worker alive. - const SAVE_TIMESTAMP_INTERVAL_MS = 2 * 1000; + if (initState.PreferencesController?.enableMV3TimestampSave) { + const SAVE_TIMESTAMP_INTERVAL_MS = 2 * 1000; - saveTimestamp(); - setInterval(saveTimestamp, SAVE_TIMESTAMP_INTERVAL_MS); + saveTimestamp(); + setInterval(saveTimestamp, SAVE_TIMESTAMP_INTERVAL_MS); + } const sessionData = await browser.storage.session.get([ 'isFirstMetaMaskControllerSetup', @@ -466,6 +473,11 @@ function emitDappViewedMetricEvent( connectSitePermissions, preferencesController, ) { + const { metaMetricsId } = controller.metaMetricsController.state; + if (!shouldEmitDappViewedEvent(metaMetricsId)) { + return; + } + // A dapp may have other permissions than eth_accounts. // Since we are only interested in dapps that use Ethereum accounts, we bail out otherwise. if (!hasProperty(connectSitePermissions.permissions, 'eth_accounts')) { diff --git a/app/scripts/constants/contracts.ts b/app/scripts/constants/contracts.ts index 27fa0606dfba..bc27be31d95c 100644 --- a/app/scripts/constants/contracts.ts +++ b/app/scripts/constants/contracts.ts @@ -12,4 +12,9 @@ export const SINGLE_CALL_BALANCES_ADDRESSES = { [CHAIN_IDS.FANTOM]: '0x07f697424ABe762bB808c109860c04eA488ff92B', [CHAIN_IDS.ARBITRUM]: '0x151E24A486D7258dd7C33Fb67E4bB01919B7B32c', [CHAIN_IDS.BLAST]: '0xfd5730e96f9dffae40d99b77015bd42816280998', + [CHAIN_IDS.LINEA_GOERLI]: '0x10dAd7Ca3921471f616db788D9300DC97Db01783', + [CHAIN_IDS.LINEA_MAINNET]: '0xF62e6a41561b3650a69Bb03199C735e3E3328c0D', + [CHAIN_IDS.AURORA]: '0x1286415D333855237f89Df27D388127181448538', + [CHAIN_IDS.BASE]: '0x6AA75276052D96696134252587894ef5FFA520af', + [CHAIN_IDS.ZKSYNC_ERA]: '0x458fEd3144680a5b8bcfaa0F9594aa19B4Ea2D34', }; diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 8d63ec70ac5e..1fd661dc2a2d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -5,7 +5,10 @@ import pump from 'pump'; import { obj as createThoughStream } from 'through2'; import browser from 'webextension-polyfill'; import { EXTENSION_MESSAGES } from '../../shared/constants/app'; -import { checkForLastError } from '../../shared/modules/browser-runtime.utils'; +import { + checkForLastError, + getIsBrowserPrerenderBroken, +} from '../../shared/modules/browser-runtime.utils'; import { isManifestV3 } from '../../shared/modules/mv3.utils'; import shouldInjectProvider from '../../shared/modules/provider-injection'; @@ -519,11 +522,7 @@ const start = () => { if (shouldInjectProvider()) { initStreams(); - // https://bugs.chromium.org/p/chromium/issues/detail?id=1457040 - // Temporary workaround for chromium bug that breaks the content script <=> background connection - // for prerendered pages. This resets potentially broken extension streams if a page transitions - // from the prerendered state to the active state. - if (document.prerendering) { + if (document.prerendering && getIsBrowserPrerenderBroken()) { document.addEventListener('prerenderingchange', () => { onDisconnectDestroyStreams( new Error('Prerendered page has become active.'), diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 44d1a367362e..dea6f4cdb97b 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -48,7 +48,6 @@ export default class AppStateController extends EventEmitter { showTestnetMessageInDropdown: true, showBetaHeader: isBeta(), showPermissionsTour: true, - showProductTour: true, showNetworkBanner: true, showAccountBanner: true, trezorModel: null, @@ -67,6 +66,10 @@ export default class AppStateController extends EventEmitter { }, surveyLinkLastClickedOrClosed: null, signatureSecurityAlertResponses: {}, + // States used for displaying the changed network toast + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage: false, + currentExtensionPopupId: 0, }); this.timer = null; @@ -374,15 +377,6 @@ export default class AppStateController extends EventEmitter { this.store.updateState({ showPermissionsTour }); } - /** - * Sets whether the product tour should be shown - * - * @param showProductTour - */ - setShowProductTour(showProductTour) { - this.store.updateState({ showProductTour }); - } - /** * Sets whether the Network Banner should be shown * @@ -401,6 +395,45 @@ export default class AppStateController extends EventEmitter { this.store.updateState({ showAccountBanner }); } + /** + * Sets a unique ID for the current extension popup + * + * @param currentExtensionPopupId + */ + setCurrentExtensionPopupId(currentExtensionPopupId) { + this.store.updateState({ currentExtensionPopupId }); + } + + /** + * Sets an object with networkName and appName + * or `null` if the message is meant to be cleared + * + * @param {{ origin: string, networkClientId: string } | null} switchedNetworkDetails - Details about the network that MetaMask just switched to. + */ + setSwitchedNetworkDetails(switchedNetworkDetails) { + this.store.updateState({ switchedNetworkDetails }); + } + + /** + * Clears the switched network details in state + */ + clearSwitchedNetworkDetails() { + this.store.updateState({ switchedNetworkDetails: null }); + } + + /** + * Remembers if the user prefers to never see the + * network switched message again + * + * @param {boolean} switchedNetworkNeverShowMessage + */ + setSwitchedNetworkNeverShowMessage(switchedNetworkNeverShowMessage) { + this.store.updateState({ + switchedNetworkDetails: null, + switchedNetworkNeverShowMessage, + }); + } + /** * Sets a property indicating the model of the user's Trezor hardware wallet * diff --git a/app/scripts/controllers/authentication/auth-snap-requests.ts b/app/scripts/controllers/authentication/auth-snap-requests.ts new file mode 100644 index 000000000000..81c8beaafd40 --- /dev/null +++ b/app/scripts/controllers/authentication/auth-snap-requests.ts @@ -0,0 +1,32 @@ +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 new file mode 100644 index 000000000000..db20a5a85043 --- /dev/null +++ b/app/scripts/controllers/authentication/authentication-controller.test.ts @@ -0,0 +1,294 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import AuthenticationController, { + AllowedActions, + AuthenticationControllerState, +} from './authentication-controller'; +import { + MOCK_ACCESS_TOKEN, + MOCK_LOGIN_RESPONSE, + mockEndpointAccessToken, + mockEndpointGetNonce, + mockEndpointLogin, +} from './mocks/mockServices'; + +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, + metametricsId: MOCK_LOGIN_RESPONSE.profile.metametrics_id, + }, + }, +}); + +describe('authentication/authentication-controller - constructor() tests', () => { + test('should initialize with default state', () => { + const controller = new AuthenticationController({ + messenger: createAuthenticationMessenger(), + }); + + expect(controller.state.isSignedIn).toBe(false); + expect(controller.state.sessionData).toBeUndefined(); + }); + + test('should initialize with override state', () => { + const controller = new AuthenticationController({ + messenger: createAuthenticationMessenger(), + state: mockSignedInState(), + }); + + 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 mockEndpoints = mockAuthenticationFlowEndpoints(); + const { messenger, mockSnapGetPublicKey, mockSnapSignMessage } = + createMockAuthenticationMessenger(); + + const controller = new AuthenticationController({ messenger }); + + 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'); + }); + + async function testAndAssertFailingEndpoints( + endpointFail: 'nonce' | 'login' | 'token', + ) { + const mockEndpoints = mockAuthenticationFlowEndpoints({ + endpointFail, + }); + const { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ messenger }); + + 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 { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ + messenger, + state: mockSignedInState(), + }); + + 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 { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ + messenger, + state: { isSignedIn: false }, + }); + + expect(() => controller.performSignOut()).toThrow(); + }); +}); + +describe('authentication/authentication-controller - getBearerToken() tests', () => { + test('Should throw error if not logged in', async () => { + const { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ + messenger, + state: { isSignedIn: false }, + }); + + await expect(controller.getBearerToken()).rejects.toThrow(); + }); + + test('Should return original access token in state', async () => { + const { messenger } = createMockAuthenticationMessenger(); + const originalState = mockSignedInState(); + const controller = new AuthenticationController({ + messenger, + state: originalState, + }); + + 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 { messenger } = createMockAuthenticationMessenger(); + mockAuthenticationFlowEndpoints(); + 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, + }); + + const result = await controller.getBearerToken(); + expect(result).toBeDefined(); + expect(result).toBe(MOCK_ACCESS_TOKEN); + }); +}); + +describe('authentication/authentication-controller - getSessionProfile() tests', () => { + test('Should throw error if not logged in', async () => { + const { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ + messenger, + state: { isSignedIn: false }, + }); + + await expect(controller.getSessionProfile()).rejects.toThrow(); + }); + + test('Should return original access token in state', async () => { + const { messenger } = createMockAuthenticationMessenger(); + const originalState = mockSignedInState(); + const controller = new AuthenticationController({ + messenger, + state: originalState, + }); + + 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 { messenger } = createMockAuthenticationMessenger(); + mockAuthenticationFlowEndpoints(); + 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, + }); + + 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); + expect(result.metametricsId).toBe( + MOCK_LOGIN_RESPONSE.profile.metametrics_id, + ); + }); +}); + +function createAuthenticationMessenger() { + const messenger = new ControllerMessenger(); + return messenger.getRestricted({ + name: 'AuthenticationController', + allowedActions: [`SnapController:handleRequest`], + }); +} + +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'); + + 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}`, + ); + } + + function exhaustedMessengerMocks(action: never) { + throw new Error(`MOCK_FAIL - unsupported messenger call: ${action}`); + } + + return exhaustedMessengerMocks(actionType); + }); + + return { messenger, mockSnapGetPublicKey, mockSnapSignMessage }; +} + +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, + }; +} diff --git a/app/scripts/controllers/authentication/authentication-controller.ts b/app/scripts/controllers/authentication/authentication-controller.ts new file mode 100644 index 000000000000..b8a0b964827b --- /dev/null +++ b/app/scripts/controllers/authentication/authentication-controller.ts @@ -0,0 +1,304 @@ +import { + BaseController, + RestrictedControllerMessenger, + StateMetadata, +} from '@metamask/base-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; + metametricsId: 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; +}; + +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; + +// Messenger +export type AuthenticationControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + Actions | AllowedActions, + never, + AllowedActions['type'], + never +>; + +/** + * Controller that enables authentication for restricted endpoints. + * Used for Global Profile Syncing and Notifications + */ +export default class AuthenticationController extends BaseController< + typeof controllerName, + AuthenticationControllerState, + AuthenticationControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: AuthenticationControllerMessenger; + state?: AuthenticationControllerState; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { ...defaultState, ...state }, + }); + + 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); + if (!loginResponse?.token) { + throw new Error(`Unable to login`); + } + + const profile: SessionProfile = { + identifierId: loginResponse.profile.identifier_id, + profileId: loginResponse.profile.profile_id, + metametricsId: loginResponse.profile.metametrics_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) { + 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; + } + + /** + * Returns the auth snap public key. + * + * @returns The snap public key. + */ + #snapGetPublicKey(): Promise { + return this.messagingSystem.call( + 'SnapController:handleRequest', + createSnapPublicKeyRequest(), + ) as Promise; + } + + /** + * Signs a specific message using an underlying auth snap. + * + * @param message - A specific tagged message to sign. + * @returns A Signature created by the snap. + */ + #snapSignMessage(message: `metamask:${string}`): Promise { + return this.messagingSystem.call( + 'SnapController:handleRequest', + createSnapSignMessageRequest(message), + ) as Promise; + } +} diff --git a/app/scripts/controllers/authentication/mocks/mockServices.ts b/app/scripts/controllers/authentication/mocks/mockServices.ts new file mode 100644 index 000000000000..5db1690517dd --- /dev/null +++ b/app/scripts/controllers/authentication/mocks/mockServices.ts @@ -0,0 +1,62 @@ +import nock from 'nock'; +import { + AUTH_LOGIN_ENDPOINT, + AUTH_NONCE_ENDPOINT, + LoginResponse, + NonceResponse, + OAuthTokenResponse, + OIDC_TOKENS_ENDPOINT, +} from '../services'; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +export const MOCK_NONCE = '4cbfqzoQpcNxVImGv'; +const MOCK_NONCE_RESPONSE: NonceResponse = { + nonce: MOCK_NONCE, +}; +export function mockEndpointGetNonce(mockReply?: MockReply) { + const reply = mockReply ?? { status: 200, body: MOCK_NONCE_RESPONSE }; + const mockNonceEndpoint = nock(AUTH_NONCE_ENDPOINT) + .get('') + .query(true) + .reply(reply.status, reply.body); + + return mockNonceEndpoint; +} + +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', + metametrics_id: 'MOCK_METAMETRICS_ID', + }, +}; +export function mockEndpointLogin(mockReply?: MockReply) { + const reply = mockReply ?? { status: 200, body: MOCK_LOGIN_RESPONSE }; + const mockLoginEndpoint = nock(AUTH_LOGIN_ENDPOINT) + .post('') + .reply(reply.status, reply.body); + + return mockLoginEndpoint; +} + +export const MOCK_ACCESS_TOKEN = `MOCK_ACCESS_TOKEN-${MOCK_JWT}`; +const MOCK_OATH_TOKEN_RESPONSE: OAuthTokenResponse = { + access_token: MOCK_ACCESS_TOKEN, + expires_in: new Date().getTime(), +}; +export function mockEndpointAccessToken(mockReply?: MockReply) { + const reply = mockReply ?? { status: 200, body: MOCK_OATH_TOKEN_RESPONSE }; + const mockOidcTokensEndpoint = nock(OIDC_TOKENS_ENDPOINT) + .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 new file mode 100644 index 000000000000..4806eb4159a1 --- /dev/null +++ b/app/scripts/controllers/authentication/services.test.ts @@ -0,0 +1,100 @@ +import { + MOCK_ACCESS_TOKEN, + MOCK_JWT, + MOCK_NONCE, + mockEndpointAccessToken, + mockEndpointGetNonce, + mockEndpointLogin, +} from './mocks/mockServices'; +import { + createLoginRawMessage, + getAccessToken, + getNonce, + login, +} from './services'; + +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'); + + 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'); + + 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 new file mode 100644 index 000000000000..345302e905cd --- /dev/null +++ b/app/scripts/controllers/authentication/services.ts @@ -0,0 +1,116 @@ +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; + metametrics_id: string; + }; +}; +export async function login( + rawMessage: string, + signature: string, +): Promise { + try { + const response = await fetch(AUTH_LOGIN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + signature, + raw_message: rawMessage, + }), + }); + + 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/decrypt-message.test.ts b/app/scripts/controllers/decrypt-message.test.ts index a1cb0a58f037..848fe3f9986d 100644 --- a/app/scripts/controllers/decrypt-message.test.ts +++ b/app/scripts/controllers/decrypt-message.test.ts @@ -38,6 +38,8 @@ const createMessengerMock = () => registerInitialEventPayload: jest.fn(), publish: jest.fn(), call: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as jest.Mocked); const createDecryptMessageManagerMock = () => @@ -57,6 +59,8 @@ const createDecryptMessageManagerMock = () => hub: { on: jest.fn(), }, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as jest.Mocked); describe('DecryptMessageController', () => { @@ -81,6 +85,8 @@ describe('DecryptMessageController', () => { const mockMessengerAction = ( action: string, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (actionName: string, ...args: any[]) => any, ) => { messengerMock.call.mockImplementation((actionName, ...rest) => { @@ -100,9 +106,17 @@ describe('DecryptMessageController', () => { ); decryptMessageController = new MockDecryptMessageController({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: getStateMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any keyringController: keyringControllerMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any messenger: messengerMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any metricsEvent: metricsEventMock as any, } as DecryptMessageControllerOptions); }); @@ -116,6 +130,8 @@ describe('DecryptMessageController', () => { decryptMessageController.update(() => ({ unapprovedDecryptMsgs: { [messageIdMock]: messageMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, unapprovedDecryptMsgCount: 1, })); @@ -131,6 +147,8 @@ describe('DecryptMessageController', () => { it('should add unapproved messages', async () => { await decryptMessageController.newRequestDecryptMessage( messageMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any undefined as any, ); @@ -220,6 +238,8 @@ describe('DecryptMessageController', () => { const messageToDecrypt = { ...messageMock, data: messageDataMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; decryptMessageManagerMock.getMessage.mockReturnValue(messageToDecrypt); mockMessengerAction( @@ -271,6 +291,8 @@ describe('DecryptMessageController', () => { it('should be able to reject all unapproved messages', async () => { decryptMessageManagerMock.getUnapprovedMessages.mockReturnValue({ [messageIdMock]: messageMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); await decryptMessageController.rejectUnapproved('reason to cancel'); diff --git a/app/scripts/controllers/decrypt-message.ts b/app/scripts/controllers/decrypt-message.ts index 5dbb4d8b43c6..51bf9c4b1250 100644 --- a/app/scripts/controllers/decrypt-message.ts +++ b/app/scripts/controllers/decrypt-message.ts @@ -117,8 +117,12 @@ export type DecryptMessageControllerMessenger = RestrictedControllerMessenger< >; export type DecryptMessageControllerOptions = { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: () => any; messenger: DecryptMessageControllerMessenger; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any metricsEvent: (payload: any, options?: any) => void; }; @@ -132,8 +136,12 @@ export default class DecryptMessageController extends BaseController< > { hub: EventEmitter; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private _getState: () => any; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private _metricsEvent: (payload: any, options?: any) => void; private _decryptMessageManager: DecryptMessageManager; @@ -363,6 +371,8 @@ export default class DecryptMessageController extends BaseController< ) { messageManager.subscribe((state: MessageManagerState) => { const newMessages = this._migrateMessages( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any state.unapprovedMessages as any, ); this.update((draftState) => { diff --git a/app/scripts/controllers/encryption-public-key.test.ts b/app/scripts/controllers/encryption-public-key.test.ts index c36418abffb1..274f0c67d7e9 100644 --- a/app/scripts/controllers/encryption-public-key.test.ts +++ b/app/scripts/controllers/encryption-public-key.test.ts @@ -34,6 +34,8 @@ const messageMock = { status: 'unapproved', type: 'testType', rawSig: undefined, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as AbstractMessage; const coreMessageMock = { @@ -56,6 +58,8 @@ const createMessengerMock = () => registerActionHandler: jest.fn(), publish: jest.fn(), call: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as jest.Mocked); const createEncryptionPublicKeyManagerMock = () => @@ -71,6 +75,8 @@ const createEncryptionPublicKeyManagerMock = () => hub: { on: jest.fn(), }, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any as jest.Mocked); describe('EncryptionPublicKeyController', () => { @@ -96,10 +102,20 @@ describe('EncryptionPublicKeyController', () => { ); encryptionPublicKeyController = new EncryptionPublicKeyController({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any messenger: messengerMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getEncryptionPublicKey: getEncryptionPublicKeyMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getAccountKeyringType: getAccountKeyringTypeMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: getStateMock as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any metricsEvent: metricsEventMock as any, } as EncryptionPublicKeyControllerOptions); }); @@ -120,6 +136,8 @@ describe('EncryptionPublicKeyController', () => { encryptionPublicKeyController.update(() => ({ unapprovedEncryptionPublicKeyMsgs: { [messageIdMock]: messageMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, unapprovedEncryptionPublicKeyMsgCount: 1, })); @@ -140,11 +158,15 @@ describe('EncryptionPublicKeyController', () => { [messageIdMock2]: messageMock, }; encryptionPublicKeyManagerMock.getUnapprovedMessages.mockReturnValueOnce( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any messages as any, ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore encryptionPublicKeyController.update(() => ({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any unapprovedEncryptionPublicKeyMsgs: messages as any, })); }); @@ -353,6 +375,8 @@ describe('EncryptionPublicKeyController', () => { const mockListener = jest.fn(); encryptionPublicKeyController.hub.on('updateBadge', mockListener); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (encryptionPublicKeyManagerMock.hub.on as any).mock.calls[0][1](); expect(mockListener).toHaveBeenCalledTimes(1); @@ -361,6 +385,8 @@ describe('EncryptionPublicKeyController', () => { it('requires approval on unapproved message event from EncryptionPublicKeyManager', () => { messengerMock.call.mockResolvedValueOnce({}); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (encryptionPublicKeyManagerMock.hub.on as any).mock.calls[1][1]( messageParamsMock, ); @@ -379,12 +405,16 @@ describe('EncryptionPublicKeyController', () => { it('updates state on EncryptionPublicKeyManager state change', async () => { await encryptionPublicKeyManagerMock.subscribe.mock.calls[0][0]({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any unapprovedMessages: { [messageIdMock]: coreMessageMock as any }, unapprovedMessagesCount: 3, }); expect(encryptionPublicKeyController.state).toEqual({ unapprovedEncryptionPublicKeyMsgs: { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any [messageIdMock]: stateMessageMock as any, }, unapprovedEncryptionPublicKeyMsgCount: 3, diff --git a/app/scripts/controllers/encryption-public-key.ts b/app/scripts/controllers/encryption-public-key.ts index 4bb019a10b72..2864bab34600 100644 --- a/app/scripts/controllers/encryption-public-key.ts +++ b/app/scripts/controllers/encryption-public-key.ts @@ -87,7 +87,11 @@ export type EncryptionPublicKeyControllerOptions = { messenger: EncryptionPublicKeyControllerMessenger; getEncryptionPublicKey: (address: string) => Promise; getAccountKeyringType: (account: string) => Promise; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: () => any; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any metricsEvent: (payload: any, options?: any) => void; }; @@ -105,10 +109,14 @@ export default class EncryptionPublicKeyController extends BaseController< private _getAccountKeyringType: (account: string) => Promise; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private _getState: () => any; private _encryptionPublicKeyManager: EncryptionPublicKeyManager; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private _metricsEvent: (payload: any, options?: any) => void; /** @@ -352,6 +360,8 @@ export default class EncryptionPublicKeyController extends BaseController< ) { messageManager.subscribe((state: MessageManagerState) => { const newMessages = this._migrateMessages( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any state.unapprovedMessages as any, ); this.update((draftState) => { diff --git a/app/scripts/controllers/metamask-notifications/constants/constants.ts b/app/scripts/controllers/metamask-notifications/constants/constants.ts new file mode 100644 index 000000000000..516b63b96fe3 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/constants/constants.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 000000000000..77b791f3a5a8 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/constants/notification-schema.ts @@ -0,0 +1,162 @@ +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 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][]; +}; + +type OnChainTriggerTypes = Exclude< + TRIGGER_TYPES, + TRIGGER_TYPES.FEATURES_ANNOUNCEMENT +>; + +export const TRIGGERS: Record = { + [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], + }, + [TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN]: { + 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 new file mode 100644 index 000000000000..4343f55ff7ef --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/metamask-notifications.test.ts @@ -0,0 +1,762 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import * as ControllerUtils from '@metamask/controller-utils'; +import { + KeyringControllerGetAccountsAction, + KeyringControllerState, + KeyringControllerStateChangeEvent, +} from '@metamask/keyring-controller'; +import { + AuthenticationControllerGetBearerToken, + AuthenticationControllerIsSignedIn, +} from '../authentication/authentication-controller'; +import { MOCK_ACCESS_TOKEN } from '../authentication/mocks/mockServices'; +import { + UserStorageControllerGetStorageKey, + UserStorageControllerPerformGetStorage, + UserStorageControllerPerformSetStorage, +} from '../user-storage/user-storage-controller'; +import { + AllowedActions, + AllowedEvents, + MetamaskNotificationsController, + PushNotificationsControllerDisablePushNotifications, + PushNotificationsControllerEnablePushNotifications, + PushNotificationsControllerUpdateTriggerPushNotifications, + defaultState, +} from './metamask-notifications'; +import { + createMockFeatureAnnouncementAPIResult, + createMockFeatureAnnouncementRaw, + mockFetchFeatureAnnouncementNotifications, +} from './mocks/mock-feature-announcements'; +import { + MOCK_USER_STORAGE_ACCOUNT, + createMockFullUserStorage, + createMockUserStorageWithTriggers, +} from './mocks/mock-notification-user-storage'; +import { + mockBatchCreateTriggers, + mockBatchDeleteTriggers, + mockListNotifications, + mockMarkNotificationsAsRead, +} from './mocks/mock-onchain-notifications'; +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'; + +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('Keying Change Event with new triggers', async () => { + const { messenger, globalMessenger, mockListAccounts } = arrangeMocks(); + + // initialize controller with 1 address + mockListAccounts.mockResolvedValueOnce(['addr1']); + const controller = new MetamaskNotificationsController({ messenger }); + controller.state.isMetamaskNotificationsEnabled = true; + + 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(); + } + + await act(['addr2'], () => { + expect(mockUpdate).toBeCalled(); + expect(mockDelete).toBeCalled(); + }); + + // Act - new accounts were added + await act(['addr1', 'addr2'], () => { + expect(mockUpdate).toBeCalled(); + expect(mockDelete).not.toBeCalled(); + }); + + // Act - an account was removed + await act(['addr1'], () => { + expect(mockUpdate).not.toBeCalled(); + expect(mockDelete).toBeCalled(); + }); + + // Act - an account was added and removed + await act(['addr2'], () => { + expect(mockUpdate).toBeCalled(); + expect(mockDelete).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 - setMetamaskNotificationsEnabled()', () => { + test('flips enabled state', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(true); + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isMetamaskNotificationsEnabled: false }, + }); + + await controller.setMetamaskNotificationsEnabled(true); + + expect(controller.state.isMetamaskNotificationsEnabled).toBe(true); + }); +}); + +describe('metamask-notifications - setMetamaskNotificationsFeatureSeen()', () => { + test('flips state when the method is called', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(true); + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isMetamaskNotificationsFeatureSeen: false }, + }); + + await controller.setMetamaskNotificationsFeatureSeen(); + + expect(controller.state.isMetamaskNotificationsFeatureSeen).toBe(true); + }); + + test('does not update if auth is not enabled', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(false); // state is off. + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isMetamaskNotificationsFeatureSeen: false }, + }); + + await expect(() => + controller.setMetamaskNotificationsFeatureSeen(), + ).rejects.toThrow(); + + expect(controller.state.isMetamaskNotificationsFeatureSeen).toBe(false); // this flag was never flipped + }); +}); + +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); + }); + + test('does not update if auth is not enabled', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(false); // state is off. + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isFeatureAnnouncementsEnabled: false }, + }); + + await expect(() => + controller.setFeatureAnnouncementsEnabled(false), + ).rejects.toThrow(); + expect(controller.state.isFeatureAnnouncementsEnabled).toBe(false); // this flag was never flipped + }); +}); + +describe('metamask-notifications - setSnapNotificationsEnabled()', () => { + test('flips state when the method is called', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(true); + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isSnapNotificationsEnabled: false }, + }); + + await controller.setSnapNotificationsEnabled(true); + + expect(controller.state.isSnapNotificationsEnabled).toBe(true); + }); + + test('does not update if auth is not enabled', async () => { + const { messenger, mockIsSignedIn } = mockNotificationMessenger(); + mockIsSignedIn.mockReturnValue(false); // state is off. + + const controller = new MetamaskNotificationsController({ + messenger, + state: { ...defaultState, isSnapNotificationsEnabled: false }, + }); + + await controller.setSnapNotificationsEnabled(false); + + expect(controller.state.isSnapNotificationsEnabled).toBe(false); // this flag was never flipped + }); +}); + +describe('metamask-notifications - createOnChainTriggers()', () => { + test('(Re-Creates) triggers and updates push notification links if using an existing User Storage (login for existing user)', async () => { + const { + messenger, + mockInitializeUserStorage, + mockEnablePushNotifications, + mockCreateOnChainTriggers, + } = arrangeMocks(); + const controller = new MetamaskNotificationsController({ messenger }); + + const result = await controller.createOnChainTriggers(); + expect(result).toBeDefined(); + expect(mockInitializeUserStorage).not.toBeCalled(); // not called since we don't need to initialize (this is an existing user) + expect(mockCreateOnChainTriggers).toBeCalled(); + expect(mockEnablePushNotifications).toBeCalled(); + }); + + 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 }); + + 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 }); + + // 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 and wallet notifications 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(2); + }); + + 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, + }; + } +}); + +// 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', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'PushPlatformNotificationsController:disablePushNotifications', + 'PushPlatformNotificationsController:enablePushNotifications', + 'PushPlatformNotificationsController:updateTriggerPushNotifications', + 'UserStorageController:getStorageKey', + 'UserStorageController:performGetStorage', + 'UserStorageController:performSetStorage', + ], + allowedEvents: ['KeyringController:stateChange'], + }); + + 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 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:performGetStorage') { + return mockPerformGetStorage(params[0]); + } + + if (actionType === 'UserStorageController:performSetStorage') { + return mockPerformSetStorage(params[0], params[1]); + } + + 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 new file mode 100644 index 000000000000..2296fa4f9064 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/metamask-notifications.ts @@ -0,0 +1,841 @@ +import { + BaseController, + RestrictedControllerMessenger, + ControllerGetStateAction, + ControllerStateChangeEvent, + StateMetadata, +} from '@metamask/base-controller'; +import log from 'loglevel'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; +import { + KeyringControllerGetAccountsAction, + KeyringControllerStateChangeEvent, +} from '@metamask/keyring-controller'; +import { + AuthenticationControllerGetBearerToken, + AuthenticationControllerIsSignedIn, +} from '../authentication/authentication-controller'; +import { + 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 { FeatureAnnouncementRawNotification } from './types/feature-announcement/feature-announcement'; +import { processNotification } from './processors/process-notifications'; +import * as MetamaskNotificationsUtils from './utils/utils'; + +// Unique name for the controller +const controllerName = 'MetamaskNotificationsController'; + +/** + * State shape for MetamaskNotificationsController + */ +export type MetamaskNotificationsControllerState = { + /** + * 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; + + /** + * Flag that indicates if the Snap notifications are enabled + */ + isSnapNotificationsEnabled: boolean; + + /** + * List of metamask notifications + */ + metamaskNotificationsList: Notification[]; + + /** + * List of read metamask notifications + */ + metamaskNotificationsReadList: string[]; +}; + +const metadata: StateMetadata = { + isMetamaskNotificationsFeatureSeen: { + persist: true, + anonymous: false, + }, + isMetamaskNotificationsEnabled: { + persist: true, + anonymous: false, + }, + isFeatureAnnouncementsEnabled: { + persist: true, + anonymous: false, + }, + isSnapNotificationsEnabled: { + persist: true, + anonymous: false, + }, + metamaskNotificationsList: { + persist: true, + anonymous: true, + }, + metamaskNotificationsReadList: { + persist: true, + anonymous: true, + }, +}; +export const defaultState: MetamaskNotificationsControllerState = { + isMetamaskNotificationsFeatureSeen: false, + isMetamaskNotificationsEnabled: false, + isFeatureAnnouncementsEnabled: false, + isSnapNotificationsEnabled: false, + metamaskNotificationsList: [], + metamaskNotificationsReadList: [], +}; + +// Mock Push Notification Controller Actions, added in a separate PR. +export type PushNotificationsControllerEnablePushNotifications = { + type: 'PushPlatformNotificationsController:enablePushNotifications'; + handler: (UUIDs: string[]) => Promise; +}; +export type PushNotificationsControllerDisablePushNotifications = { + type: 'PushPlatformNotificationsController:disablePushNotifications'; + handler: (UUIDs: string[]) => Promise; +}; +export type PushNotificationsControllerUpdateTriggerPushNotifications = { + type: 'PushPlatformNotificationsController:updateTriggerPushNotifications'; + handler: (UUIDs: string[]) => Promise; +}; + +// Messenger Actions +export type Actions = ControllerGetStateAction< + 'state', + MetamaskNotificationsControllerState +>; + +// Allowed Actions +export type AllowedActions = + // Keyring Controller Requests + | KeyringControllerGetAccountsAction + // Auth Controller Requests + | AuthenticationControllerGetBearerToken + | AuthenticationControllerIsSignedIn + // User Storage Controller Requests + | UserStorageControllerGetStorageKey + | UserStorageControllerPerformGetStorage + | UserStorageControllerPerformSetStorage + // Push Notifications Controller Requests + | PushNotificationsControllerEnablePushNotifications + | PushNotificationsControllerDisablePushNotifications + | PushNotificationsControllerUpdateTriggerPushNotifications; + +// Events +export type MetamaskNotificationsControllerMessengerEvents = + ControllerStateChangeEvent< + typeof controllerName, + MetamaskNotificationsControllerState + >; + +// Allowed Events +export type AllowedEvents = KeyringControllerStateChangeEvent; + +// Type for the messenger of MetamaskNotificationsController +export type MetamaskNotificationsControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + Actions | AllowedActions, + AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] + >; + +/** + * Controller that enables wallet notifications and feature announcements + */ +export class MetamaskNotificationsController extends BaseController< + typeof controllerName, + MetamaskNotificationsControllerState, + MetamaskNotificationsControllerMessenger +> { + #auth = { + getBearerToken: async () => { + return await this.messagingSystem.call( + 'AuthenticationController:getBearerToken', + ); + }, + isSignedIn: () => { + return this.messagingSystem.call('AuthenticationController:isSignedIn'); + }, + }; + + #storage = { + 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[]) => { + return await this.messagingSystem.call( + 'PushPlatformNotificationsController:enablePushNotifications', + UUIDs, + ); + }, + disablePushNotifications: async (UUIDs: string[]) => { + return await this.messagingSystem.call( + 'PushPlatformNotificationsController:disablePushNotifications', + UUIDs, + ); + }, + updatePushNotifications: async (UUIDs: string[]) => { + return await this.messagingSystem.call( + 'PushPlatformNotificationsController:updateTriggerPushNotifications', + UUIDs, + ); + }, + }; + + #prevAccountsSet = new Set(); + + #accounts = { + /** + * Used to get list of addresses from keyring (wallet addresses) + * + * @returns addresses removed, added, and latest list of addresses + */ + listAccounts: async () => { + const nonChecksumAccounts = await this.messagingSystem.call( + 'KeyringController:getAccounts', + ); + const accounts = nonChecksumAccounts.map((a) => toChecksumHexAddress(a)); + const currentAccountsSet = new Set(accounts); + + const accountsAdded = accounts.filter( + (a) => !this.#prevAccountsSet.has(a), + ); + + const accountsRemoved = [...this.#prevAccountsSet.values()].filter( + (a) => !currentAccountsSet.has(a), + ); + + this.#prevAccountsSet = new Set(accounts); + 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?: MetamaskNotificationsControllerState; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { ...defaultState, ...state }, + }); + + this.#accounts.initialize(); + this.#accounts.subscribe(); + } + + #assertAuthEnabled() { + if (!this.#auth.isSignedIn()) { + this.update((s) => { + s.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 }; + } + + #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); + } + + /** + * Returns if an account or multiple accounts are present in User Storage. + * This is to ensure we show the correct UI in the notification settings page, + * on which notifications are enabled or disabled. + * + * **Action** - If an account is enabled or disabled + * + * @param accounts - An array of account addresses to be checked 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. + * @throws {Error} If user storage does not exist. + */ + public async checkAccountsPresence( + accounts: string[], + ): Promise> { + // Retrieve user storage + const userStorage = await this.#getUserStorage(); + this.#assertUserStorage(userStorage); + + // Use MetamaskNotificationsUtils to check the presence of accounts + return MetamaskNotificationsUtils.checkAccountsPresence( + userStorage, + accounts, + ); + } + + /** + * Sets the enabled state of MetaMask notifications. + * This method first checks if the user is authenticated before attempting to toggle the notification settings. + * + * **Action** - This method is used to enable or disable MetaMask notifications based on the provided state. + * + * @param state - A boolean value indicating the desired enabled state of the notifications. + * @async + * @throws {Error} If the user is not authenticated or if there is an error updating the state. + */ + public async setMetamaskNotificationsEnabled(state: boolean) { + try { + this.#assertAuthEnabled(); + + this.update((s) => { + s.isMetamaskNotificationsEnabled = state; + }); + } catch (e) { + log.error('Unable to toggle notifications', e); + throw new Error('Unable to toggle notifications'); + } + } + + /** + * This is for a 1-time flag/CTA for notifications. When dismissed we will invoke this. + * + * **Action** - use to dismiss the Notification CTA in the UI + * + * @async + * @throws {Error} Throws an error if the BearerToken token or storage key is missing. + */ + public async setMetamaskNotificationsFeatureSeen() { + try { + this.#assertAuthEnabled(); + + this.update((s) => { + s.isMetamaskNotificationsFeatureSeen = true; + }); + } catch (e) { + log.error('Unable to declare feature/CTA was seen', e); + throw new Error('Unable to declare feature/CTA was seen'); + } + } + + /** + * Sets the enabled state of feature announcements. + * + * **Action** - used in the notification settings to enable/disable feature announcements. + * + * @param state - A boolean value indicating the desired enabled state of the feature announcements. + * @async + * @throws {Error} If the BearerToken token or storage key is missing. + */ + public async setFeatureAnnouncementsEnabled(state: boolean) { + try { + this.#assertAuthEnabled(); + + this.update((s) => { + s.isFeatureAnnouncementsEnabled = state; + }); + } catch (e) { + log.error('Unable to toggle feature announcements', e); + throw new Error('Unable to toggle feature announcements'); + } + } + + /** + * Sets the enabled state of Snap notifications. + * + * **Action** - used in the notifications settings page to enable/disable snap notifications. + * + * @param state - A boolean value indicating the desired enabled state of the snap notifications. + * @async + * @throws {Error} If the BearerToken token or storage key is missing. + */ + public async setSnapNotificationsEnabled(state: boolean) { + try { + this.#assertAuthEnabled(); + + this.update((s) => { + s.isSnapNotificationsEnabled = state; + }); + } catch (e) { + log.error('Unable to toggle snap notifications', e); + } + } + + /** + * 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 { + 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.setFeatureAnnouncementsEnabled(true); + this.setMetamaskNotificationsEnabled(true); + this.setSnapNotificationsEnabled(true); + + return userStorage; + } catch (err) { + log.error('Failed to create On Chain triggers', err); + throw new Error('Failed to create On Chain triggers'); + } + } + + /** + * 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 { + // 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'); + } + } + + /** + * 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 { + // 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), + ); + + // 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'); + } + } + + /** + * 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 { + // Raw Feature Notifications + const rawFeatureAnnouncementNotifications = + 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: (FeatureAnnouncementRawNotification | OnChainRawNotification)[], + ) => + 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((s) => { + s.metamaskNotificationsList = metamaskNotifications; + }); + + return metamaskNotifications; + } catch (err) { + 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 (state is also used on counter & badge) + this.update((s) => { + const currentReadList = s.metamaskNotificationsReadList; + const newReadIds = [ + ...onchainNotificationIds, + ...featureAnnouncementNotificationIds, + ]; + s.metamaskNotificationsReadList = [ + ...new Set([...currentReadList, ...newReadIds]), + ]; + }); + } +} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts new file mode 100644 index 000000000000..7410b78ee3be --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-feature-announcements.ts @@ -0,0 +1,224 @@ +import nock from 'nock'; +import { + ContentfulResult, + FEATURE_ANNOUNCEMENT_URL, +} from '../services/feature-announcements'; +import { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; +import { TRIGGER_TYPES } from '../constants/notification-schema'; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +export function mockFetchFeatureAnnouncementNotifications( + mockReply?: MockReply, +) { + const reply = mockReply ?? { status: 200, body: { items: [] } }; + const mockEndpoint = nock(FEATURE_ANNOUNCEMENT_URL) + .get('') + .query(true) + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +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: { + linkText: 'Try now', + linkUrl: 'https://portfolio.metamask.io/explore', + isExternal: false, + }, + }, + ], + 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', + }, + link: { + linkText: 'Try now', + linkUrl: 'https://portfolio.metamask.io/explore', + isExternal: false, + }, + }, + }; +} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts new file mode 100644 index 000000000000..d4ce4a61114f --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-notification-trigger.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000000..831a9633d6c6 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-notification-user-storage.ts @@ -0,0 +1,72 @@ +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 } = {}, +): UserStorage { + return initializeUserStorage( + [{ address: MOCK_USER_STORAGE_ACCOUNT }], + props.triggersEnabled ?? true, + ); +} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts new file mode 100644 index 000000000000..cd8f9e5979c9 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-onchain-notifications.ts @@ -0,0 +1,58 @@ +import nock from 'nock'; +import { + NOTIFICATION_API_LIST_ENDPOINT, + NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT, + TRIGGER_API_BATCH_ENDPOINT, +} from '../services/onchain-notifications'; +import { createMockRawOnChainNotifications } from './mock-raw-notifications'; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +export function mockBatchCreateTriggers(mockReply?: MockReply) { + const reply = mockReply ?? { status: 204 }; + + const mockEndpoint = nock(TRIGGER_API_BATCH_ENDPOINT) + .post('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockBatchDeleteTriggers(mockReply?: MockReply) { + const reply = mockReply ?? { status: 204 }; + + const mockEndpoint = nock(TRIGGER_API_BATCH_ENDPOINT) + .delete('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockListNotifications(mockReply?: MockReply) { + const reply = mockReply ?? { + status: 200, + body: createMockRawOnChainNotifications(), + }; + + const mockEndpoint = nock(NOTIFICATION_API_LIST_ENDPOINT) + .post('') + .query(true) + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockMarkNotificationsAsRead(mockReply?: MockReply) { + const reply = mockReply ?? { + status: 200, + }; + + const mockEndpoint = nock(NOTIFICATION_API_MARK_ALL_AS_READ_ENDPOINT) + .post('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} diff --git a/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts b/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts new file mode 100644 index 000000000000..daf9c995f413 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/mocks/mock-raw-notifications.ts @@ -0,0 +1,590 @@ +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', + 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', + 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', + 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', + 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', + data: { + to: '0xf47f628fe3bd2595e9ab384bfffc3859b448e451', + nft: { + 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', + data: { + to: '0xba7f3daa8adfdad686574406ab9bd5d2f0a49d2e', + nft: { + 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', + 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', + 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', + 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', + 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', + 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', + 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', + 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', + 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', + 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/processors/process-feature-announcement.test.ts b/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.test.ts new file mode 100644 index 000000000000..673610aef7eb --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.test.ts @@ -0,0 +1,52 @@ +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 30 days', () => { + const mockDate = new Date(); + mockDate.setDate(mockDate.getDate() - 31); + + 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 new file mode 100644 index 000000000000..a350ddedd4cc --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-feature-announcement.ts @@ -0,0 +1,32 @@ +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 isThirtyDaysOld(oldDate: Date) { + const differenceInTime = Date.now() - oldDate.getTime(); + const differenceInDays = differenceInTime / ONE_DAY_MS; + return differenceInDays >= 30; +} + +export function isFeatureAnnouncementRead( + notification: Pick, + readPlatformNotificationsList: string[], +): boolean { + if (readPlatformNotificationsList.includes(notification.id)) { + return true; + } + return isThirtyDaysOld(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 new file mode 100644 index 000000000000..c1ed0c03b67e --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-notifications.test.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000000..b6c618b190d3 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-notifications.ts @@ -0,0 +1,38 @@ +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 { processOnChainNotification } from './process-onchain-notifications'; +import { + isFeatureAnnouncementRead, + processFeatureAnnouncement, +} from './process-feature-announcement'; + +const isOnChainNotification = ( + n: OnChainRawNotification, +): n is OnChainRawNotification => Object.values(TRIGGER_TYPES).includes(n.type); + +export function processNotification( + notification: FeatureAnnouncementRawNotification | OnChainRawNotification, + readNotifications: string[] = [], +): Notification { + const exhaustedAllCases = (_: never) => { + throw new Error( + `No processor found for notification kind ${notification.type}`, + ); + }; + + if (notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT) { + const n = processFeatureAnnouncement( + notification as FeatureAnnouncementRawNotification, + ); + n.isRead = isFeatureAnnouncementRead(n, readNotifications); + return n; + } + + if (isOnChainNotification(notification as OnChainRawNotification)) { + 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 new file mode 100644 index 000000000000..cd2159a9fb01 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.test.ts @@ -0,0 +1,52 @@ +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()', () => { + test.each(rawNotificationTestSuite)( + 'Converts Raw On-Chain Notification (%s) to a shared Notification Type', + (_, rawNotification) => { + 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 new file mode 100644 index 000000000000..793bfb1b1f6f --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/processors/process-onchain-notifications.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000000..ea000423db0c --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/services/feature-announcements.test.ts @@ -0,0 +1,62 @@ +import { TRIGGER_TYPES } from '../constants/notification-schema'; +import { + createMockFeatureAnnouncementAPIResult, + mockFetchFeatureAnnouncementNotifications, +} from '../mocks/mock-feature-announcements'; +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 new file mode 100644 index 000000000000..af85fbef5750 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/services/feature-announcements.ts @@ -0,0 +1,129 @@ +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 { TypeLinkFields } from '../types/feature-announcement/type-link'; +import { TypeActionFields } from '../types/feature-announcement/type-action'; + +const spaceId = process.env.CONTENTFUL_ACCESS_SPACE_ID || ''; +const accessToken = process.env.CONTENTFUL_ACCESS_TOKEN || ''; +export const FEATURE_ANNOUNCEMENT_URL = `https://cdn.contentful.com/spaces/${spaceId}/environments/master/entries?access_token=${accessToken}&content_type=productAnnouncement&include=10`; + +export type ContentfulResult = { + includes?: { + Entry?: Entry[]; + Asset?: Asset[]; + }; + items?: TypeFeatureAnnouncement[]; +}; + +async function fetchFromContentful( + url: string, + retries = 3, + 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 actionFields = fields.action + ? (findIncludedItem(fields.action.sys.id) as TypeActionFields['fields']) + : undefined; + const linkFields = fields.link + ? (findIncludedItem(fields.link.sys.id) as TypeLinkFields['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 ?? '', + }, + link: linkFields && { + linkText: linkFields?.linkText, + linkUrl: linkFields?.linkUrl, + isExternal: linkFields?.isExternal, + }, + action: actionFields && { + actionText: actionFields?.actionText, + actionUrl: actionFields?.actionUrl, + isExternal: actionFields?.isExternal, + }, + }, + }; + + return notification; + }); + + return rawNotifications; +} + +export async function getFeatureAnnouncementNotifications(): Promise< + Notification[] +> { + const rawNotifications = await fetchFeatureAnnouncementNotifications(); + const notifications = rawNotifications.map((notification) => + processFeatureAnnouncement(notification), + ); + + return notifications; +} diff --git a/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts new file mode 100644 index 000000000000..3ee5b311a9f7 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.test.ts @@ -0,0 +1,278 @@ +import { TRIGGER_TYPES } from '../constants/notification-schema'; +import { + mockBatchCreateTriggers, + mockBatchDeleteTriggers, + mockListNotifications, + mockMarkNotificationsAsRead, +} from '../mocks/mock-onchain-notifications'; +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 new file mode 100644 index 000000000000..3012352c7f49 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/services/onchain-notifications.ts @@ -0,0 +1,288 @@ +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 type { 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 new file mode 100644 index 000000000000..4141dab2618a --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/feature-announcement/feature-announcement.ts @@ -0,0 +1,33 @@ +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' | 'link' | 'action' +> & { + longDescription: string; + image: { + title?: string; + description?: string; + url: string; + }; + link?: { + linkText: string; + linkUrl: string; + isExternal: boolean; + }; + action?: { + actionText: string; + actionUrl: string; + isExternal: boolean; + }; +}; + +export type FeatureAnnouncementRawNotification = { + type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT; + createdAt: string; + data: FeatureAnnouncementRawNotificationData; +}; diff --git a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-action.ts b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-action.ts new file mode 100644 index 000000000000..453bd5d9b206 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-action.ts @@ -0,0 +1,12 @@ +import type { Entry } from 'contentful'; + +export type TypeActionFields = { + fields: { + actionText: string; + actionUrl: string; + isExternal: boolean; + }; + contentTypeId: 'action'; +}; + +export type TypeAction = Entry; 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 new file mode 100644 index 000000000000..e93e577b2600 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-feature-announcement.ts @@ -0,0 +1,42 @@ +import type { Entry, EntryFieldTypes } from 'contentful'; +import type { TypeActionFields } from './type-action'; +import type { TypeLinkFields } from './type-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; + link?: EntryFieldTypes.EntryLink; + action?: EntryFieldTypes.EntryLink; + }; + contentTypeId: 'productAnnouncement'; +}; + +export type TypeFeatureAnnouncement = Entry< + TypeFeatureAnnouncementFields, + 'WITHOUT_UNRESOLVABLE_LINKS' +>; diff --git a/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-link.ts b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-link.ts new file mode 100644 index 000000000000..fec717ce9da2 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/feature-announcement/type-link.ts @@ -0,0 +1,12 @@ +import type { Entry } from 'contentful'; + +export type TypeLinkFields = { + fields: { + linkText: string; + linkUrl: string; + isExternal: boolean; + }; + contentTypeId: 'link'; +}; + +export type TypeLink = Entry; diff --git a/app/scripts/controllers/metamask-notifications/types/notification/notification.ts b/app/scripts/controllers/metamask-notifications/types/notification/notification.ts new file mode 100644 index 000000000000..666e4b129a71 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/notification/notification.ts @@ -0,0 +1,32 @@ +import type { FeatureAnnouncementRawNotification } from '../feature-announcement/feature-announcement'; +import type { OnChainRawNotification } from '../on-chain-notification/on-chain-notification'; +import type { Compute } from '../type-utils'; + +/** + * 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< + (FeatureAnnouncementRawNotification | OnChainRawNotification) & { + id: string; + createdAt: string; + isRead: boolean; + } +>; + +// 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 new file mode 100644 index 000000000000..022702933bea --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/on-chain-notification/on-chain-notification.ts @@ -0,0 +1,50 @@ +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 new file mode 100644 index 000000000000..27fb46003367 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/on-chain-notification/schema.d.ts @@ -0,0 +1,300 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +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; + 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 new file mode 100644 index 000000000000..f05a763f5d2c --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/type-utils.ts @@ -0,0 +1,4 @@ +/** + * 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 new file mode 100644 index 000000000000..c68c202e9e92 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/types.ts @@ -0,0 +1,17 @@ +import type { FeatureAnnouncementRawNotification } from './feature-announcement/feature-announcement'; +import type { Compute } from './type-utils'; +import type { OnChainRawNotification } from './on-chain-notification/on-chain-notification'; + +/** + * 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< + (FeatureAnnouncementRawNotification | OnChainRawNotification) & { + 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 new file mode 100644 index 000000000000..086d4cc7e4d4 --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/types/user-storage/user-storage.ts @@ -0,0 +1,32 @@ +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 new file mode 100644 index 000000000000..b533f6c4559b --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/utils/utils.test.ts @@ -0,0 +1,314 @@ +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 new file mode 100644 index 000000000000..c0bfc89add6f --- /dev/null +++ b/app/scripts/controllers/metamask-notifications/utils/utils.ts @@ -0,0 +1,621 @@ +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: + case TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN: + return TRIGGER_TYPES_GROUPS.DEFI; + 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 = 3, + 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/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index df8de77c2212..b926be047da5 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -201,10 +201,10 @@ describe('MMIController', function () { initState: { custodyAccountDetails: { [mockAccount.address]: { - custodyType: 'Custody - Jupiter', + custodyType: 'Custody - ECA3', }, [mockAccount2.address]: { - custodyType: 'Custody - Jupiter', + custodyType: 'Custody - ECA3', }, }, }, @@ -438,11 +438,11 @@ describe('MMIController', function () { getCustodianAccounts: mockCustodialKeyring, }); - await mmiController.getCustodianAccounts('token', 'mock url', 'JUPITER'); + await mmiController.getCustodianAccounts('token', 'neptune-custody', 'ECA3'); expect(selectedAccountSpy).toHaveBeenCalledTimes(0); - expect(keyringControllerSpy).toHaveBeenCalledWith('Custody - Jupiter'); + expect(keyringControllerSpy).toHaveBeenCalledWith('Custody - ECA3'); expect(mockCustodialKeyring).toHaveBeenCalled(); }); @@ -454,14 +454,14 @@ describe('MMIController', function () { getCustodianAccounts: mockCustodialKeyring, }); - await mmiController.getCustodianAccounts('token', 'mock url'); + await mmiController.getCustodianAccounts('token', 'neptune-custody'); expect(selectedAccountSpy).toHaveBeenCalledWith( 'AccountsController:getSelectedAccount', ); expect(selectedAccountSpy).toHaveReturnedWith(mockAccount); - expect(keyringControllerSpy).toHaveBeenCalledWith('Custody - Jupiter'); + expect(keyringControllerSpy).toHaveBeenCalledWith('Custody - ECA3'); expect(mockCustodialKeyring).toHaveBeenCalled(); }); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index b2d66f36afdf..31de78659952 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -35,6 +35,7 @@ import { IInteractiveRefreshTokenChangeEvent, Label, Signature, + ConnectionRequest, } from '../../../shared/constants/mmi-controller'; import AccountTracker from '../lib/account-tracker'; import AppStateController from './app-state'; @@ -48,6 +49,8 @@ type UpdateCustodianTransactionsParameters = { txList: string[]; custodyController: CustodyController; transactionUpdateController: TransactionUpdateController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any txStateManager: any; getPendingNonce: (address: string) => Promise; setTxHash: (txId: string, txHash: string) => void; @@ -58,6 +61,8 @@ export default class MMIController extends EventEmitter { public mmiConfigurationController: MmiConfigurationController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any public keyringController: any; public preferencesController: PreferencesController; @@ -68,8 +73,12 @@ export default class MMIController extends EventEmitter { private custodyController: CustodyController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private getState: () => any; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private getPendingNonce: (address: string) => Promise; private accountTracker: AccountTracker; @@ -78,28 +87,46 @@ export default class MMIController extends EventEmitter { private networkController: NetworkController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private permissionController: any; private signatureController: SignatureController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private messenger: any; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private platform: any; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any private extension: any; private updateTransactionHash: (txId: string, txHash: string) => void; + private setChannelId: (channelId: string) => void; + + private setConnectionRequest: (payload: ConnectionRequest | null) => void; + public trackTransactionEvents: ( args: { transactionMeta: TransactionMeta }, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any event: any, ) => void; private txStateManager: { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getTransactions: (query?: any, opts?: any, fullTx?: boolean) => any[]; setTxStatusSigned: (txId: string) => void; setTxStatusSubmitted: (txId: string) => void; setTxStatusFailed: (txId: string) => void; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any updateTransaction: (txMeta: any) => void; }; @@ -125,6 +152,8 @@ export default class MMIController extends EventEmitter { this.extension = opts.extension; this.updateTransactionHash = opts.updateTransactionHash; + this.setChannelId = opts.setChannelId; + this.setConnectionRequest = opts.setConnectionRequest; this.trackTransactionEvents = opts.trackTransactionEvents; this.txStateManager = { @@ -163,6 +192,20 @@ export default class MMIController extends EventEmitter { await this.handleSigningEvents(signature, messageId, 'v4'); }, ); + + this.transactionUpdateController.on( + 'handshake', + async ({ channelId }: { channelId: string }) => { + this.setChannelId(channelId); + }, + ); + + this.transactionUpdateController.on( + 'connection.request', + async (payload: ConnectionRequest) => { + this.setConnectionRequest(payload); + }, + ); } // End of constructor async persistKeyringsAfterRefreshTokenChange() { @@ -171,6 +214,8 @@ export default class MMIController extends EventEmitter { async trackTransactionEventFromCustodianEvent( txMeta: TransactionMeta, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any event: any, ) { // transactionMetricsRequest parameter is already bound in the constructor @@ -322,6 +367,8 @@ export default class MMIController extends EventEmitter { string, { name: string; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any custodianDetails: any; labels: Label[]; token: string; diff --git a/app/scripts/controllers/onboarding.test.ts b/app/scripts/controllers/onboarding.test.ts new file mode 100644 index 000000000000..61b9cf8de589 --- /dev/null +++ b/app/scripts/controllers/onboarding.test.ts @@ -0,0 +1,57 @@ +import { FirstTimeFlowType } from '../../../shared/constants/onboarding'; +import OnboardingController, { OnboardingControllerState } from './onboarding'; + +describe('OnboardingController', () => { + let onboardingController: OnboardingController; + + beforeEach(() => { + onboardingController = new OnboardingController({ + initState: { + seedPhraseBackedUp: null, + firstTimeFlowType: null, + completedOnboarding: false, + onboardingTabs: {}, + }, + }); + }); + + it('should set the seedPhraseBackedUp property', () => { + const newSeedPhraseBackUpState = true; + onboardingController.setSeedPhraseBackedUp(newSeedPhraseBackUpState); + const state: OnboardingControllerState = + onboardingController.store.getState(); + expect(state.seedPhraseBackedUp).toBe(newSeedPhraseBackUpState); + }); + + it('should set the firstTimeFlowType property', () => { + const type: FirstTimeFlowType = FirstTimeFlowType.create; + onboardingController.setFirstTimeFlowType(type); + const state: OnboardingControllerState = + onboardingController.store.getState(); + expect(state.firstTimeFlowType).toBe(type); + }); + + it('should register a site for onboarding', async () => { + const location = 'example.com'; + const tabId = '123'; + await onboardingController.registerOnboarding(location, tabId); + const state: OnboardingControllerState = + onboardingController.store.getState(); + expect(state.onboardingTabs?.[location]).toBe(tabId); + }); + + it('should skip update state if the location is already onboard', async () => { + const location = 'example.com'; + const tabId = '123'; + await onboardingController.registerOnboarding(location, tabId); + const state: OnboardingControllerState = + onboardingController.store.getState(); + const updateStateSpy = jest.spyOn( + onboardingController.store, + 'updateState', + ); + + expect(state.onboardingTabs?.[location]).toBe(tabId); + expect(updateStateSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/app/scripts/controllers/permissions/background-api.js b/app/scripts/controllers/permissions/background-api.js index fe5ee6c755b1..5a543492379f 100644 --- a/app/scripts/controllers/permissions/background-api.js +++ b/app/scripts/controllers/permissions/background-api.js @@ -7,13 +7,13 @@ import { export function getPermissionBackgroundApiMethods(permissionController) { return { addPermittedAccount: (origin, account) => { - const existing = permissionController.getCaveat( + const { value: existingAccounts } = permissionController.getCaveat( origin, RestrictedMethods.eth_accounts, CaveatTypes.restrictReturnedAccounts, ); - if (existing.value.includes(account)) { + if (existingAccounts.includes(account)) { return; } @@ -21,41 +21,49 @@ export function getPermissionBackgroundApiMethods(permissionController) { origin, RestrictedMethods.eth_accounts, CaveatTypes.restrictReturnedAccounts, - [...existing.value, account], + [...existingAccounts, account], ); }, - // To add more than one accounts when already connected to the dapp + // To add more than one account when already connected to the dapp addMorePermittedAccounts: (origin, accounts) => { - const existing = permissionController.getCaveat( + const { value: existingAccounts } = permissionController.getCaveat( origin, RestrictedMethods.eth_accounts, CaveatTypes.restrictReturnedAccounts, ); - // Since this function will be called for unconnected accounts, we dodn't need an extra check + + const updatedAccounts = Array.from( + new Set([...existingAccounts, ...accounts]), + ); + + if (updatedAccounts.length === existingAccounts.length) { + return; + } + permissionController.updateCaveat( origin, RestrictedMethods.eth_accounts, CaveatTypes.restrictReturnedAccounts, - [...existing.value, ...accounts], + updatedAccounts, ); }, removePermittedAccount: (origin, account) => { - const existing = permissionController.getCaveat( + const { value: existingAccounts } = permissionController.getCaveat( origin, RestrictedMethods.eth_accounts, CaveatTypes.restrictReturnedAccounts, ); - if (!existing.value.includes(account)) { - return; - } - - const remainingAccounts = existing.value.filter( + const remainingAccounts = existingAccounts.filter( (existingAccount) => existingAccount !== account, ); + if (remainingAccounts.length === existingAccounts.length) { + return; + } + if (remainingAccounts.length === 0) { permissionController.revokePermission( origin, diff --git a/app/scripts/controllers/permissions/background-api.test.js b/app/scripts/controllers/permissions/background-api.test.js index a7f433eb1d25..b1b10b12bfb4 100644 --- a/app/scripts/controllers/permissions/background-api.test.js +++ b/app/scripts/controllers/permissions/background-api.test.js @@ -34,7 +34,7 @@ describe('permission background API methods', () => { ); }); - it('does not add a permitted account', () => { + it('does not add an already permitted account', () => { const permissionController = { getCaveat: jest.fn().mockImplementationOnce(() => { return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] }; @@ -56,6 +56,140 @@ describe('permission background API methods', () => { }); }); + describe('addMorePermittedAccounts', () => { + it('adds a permitted account', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] }; + }), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addMorePermittedAccounts('foo.com', ['0x2']); + + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ); + + expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.updateCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ['0x1', '0x2'], + ); + }); + + it('adds multiple permitted accounts', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] }; + }), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addMorePermittedAccounts('foo.com', ['0x2', '0x3']); + + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ); + + expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.updateCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ['0x1', '0x2', '0x3'], + ); + }); + + it('adds multiple permitted accounts (partial overlap)', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x1', '0x2', '0x3'], + }; + }), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addMorePermittedAccounts('foo.com', ['0x2', '0x4']); + + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ); + + expect(permissionController.updateCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.updateCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ['0x1', '0x2', '0x3', '0x4'], + ); + }); + + it('does not add an already permitted account', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { type: CaveatTypes.restrictReturnedAccounts, value: ['0x1'] }; + }), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addMorePermittedAccounts('foo.com', ['0x1']); + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ); + + expect(permissionController.updateCaveat).not.toHaveBeenCalled(); + }); + + it('does not add multiple already permitted accounts', () => { + const permissionController = { + getCaveat: jest.fn().mockImplementationOnce(() => { + return { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x1', '0x2', '0x3'], + }; + }), + updateCaveat: jest.fn(), + }; + + getPermissionBackgroundApiMethods( + permissionController, + ).addMorePermittedAccounts('foo.com', ['0x1', '0x3']); + expect(permissionController.getCaveat).toHaveBeenCalledTimes(1); + expect(permissionController.getCaveat).toHaveBeenCalledWith( + 'foo.com', + RestrictedMethods.eth_accounts, + CaveatTypes.restrictReturnedAccounts, + ); + + expect(permissionController.updateCaveat).not.toHaveBeenCalled(); + }); + }); + describe('removePermittedAccount', () => { it('removes a permitted account', () => { const permissionController = { diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js deleted file mode 100644 index 4ede66f22cb9..000000000000 --- a/app/scripts/controllers/permissions/enums.js +++ /dev/null @@ -1,5 +0,0 @@ -export const NOTIFICATION_NAMES = { - accountsChanged: 'metamask_accountsChanged', - unlockStateChanged: 'metamask_unlockStateChanged', - chainChanged: 'metamask_chainChanged', -}; diff --git a/app/scripts/controllers/permissions/enums.ts b/app/scripts/controllers/permissions/enums.ts new file mode 100644 index 000000000000..c170bd78aa67 --- /dev/null +++ b/app/scripts/controllers/permissions/enums.ts @@ -0,0 +1,5 @@ +export enum NOTIFICATION_NAMES { + accountsChanged = 'metamask_accountsChanged', + unlockStateChanged = 'metamask_unlockStateChanged', + chainChanged = 'metamask_chainChanged', +} diff --git a/app/scripts/controllers/permissions/specifications.js b/app/scripts/controllers/permissions/specifications.js index 8d5b8ec8a789..e86971e42863 100644 --- a/app/scripts/controllers/permissions/specifications.js +++ b/app/scripts/controllers/permissions/specifications.js @@ -97,12 +97,6 @@ export const getPermissionSpecifications = ({ allowedCaveats: [CaveatTypes.restrictReturnedAccounts], factory: (permissionOptions, requestData) => { - if (Array.isArray(permissionOptions.caveats)) { - throw new Error( - `${PermissionNames.eth_accounts} error: Received unexpected caveats. Any permitted caveats will be added automatically.`, - ); - } - // This value will be further validated as part of the caveat. if (!requestData.approvedAccounts) { throw new Error( diff --git a/app/scripts/controllers/permissions/specifications.test.js b/app/scripts/controllers/permissions/specifications.test.js index a90ac4e87e63..3f140b39d052 100644 --- a/app/scripts/controllers/permissions/specifications.test.js +++ b/app/scripts/controllers/permissions/specifications.test.js @@ -232,7 +232,7 @@ describe('PermissionController specifications', () => { ).toThrow(/No approved accounts specified\.$/u); }); - it('throws an error if any caveats are specified directly', () => { + it('uses the approved accounts even if caveats are specified', () => { const getInternalAccounts = jest.fn(); const getAllAccounts = jest.fn(); const { factory } = getPermissionSpecifications({ @@ -240,7 +240,7 @@ describe('PermissionController specifications', () => { getAllAccounts, })[RestrictedMethods.eth_accounts]; - expect(() => + expect( factory( { caveats: [ @@ -252,9 +252,20 @@ describe('PermissionController specifications', () => { invoker: 'foo.bar', target: 'eth_accounts', }, - { approvedAccounts: ['0x1'] }, + { approvedAccounts: ['0x1', '0x3'] }, ), - ).toThrow(/Received unexpected caveats./u); + ).toStrictEqual({ + caveats: [ + { + type: CaveatTypes.restrictReturnedAccounts, + value: ['0x1', '0x3'], + }, + ], + date: 1, + id: expect.any(String), + invoker: 'foo.bar', + parentCapability: 'eth_accounts', + }); }); }); diff --git a/app/scripts/controllers/preferences.d.ts b/app/scripts/controllers/preferences.d.ts index 563c7e822f4a..cc6a99af3671 100644 --- a/app/scripts/controllers/preferences.d.ts +++ b/app/scripts/controllers/preferences.d.ts @@ -12,7 +12,6 @@ export type PreferencesController = { setSelectedAddress(addressToLowerCase: string): void; getSelectedAddress(): string; setAccountLabel(address: string, label: string): void; - setAddresses(allAccounts: string[]): void; store: { getState: () => PreferencesControllerState; subscribe: (callback: (state: PreferencesControllerState) => void) => void; diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 7a50baaffb0d..86dc13cba38f 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -15,7 +15,6 @@ const mainNetworks = { const testNetworks = { [CHAIN_IDS.GOERLI]: true, [CHAIN_IDS.SEPOLIA]: true, - [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }; @@ -57,11 +56,11 @@ export default class PreferencesController { useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used // set to false will be using the static list from contract-metadata - useTokenDetection: false, + useTokenDetection: opts?.initState?.useTokenDetection ?? true, useNftDetection: false, use4ByteResolution: true, useCurrencyRateCheck: true, - useRequestQueue: false, + useRequestQueue: true, openSeaEnabled: false, ///: BEGIN:ONLY_INCLUDE_IF(blockaid) securityAlertsEnabled: true, @@ -91,9 +90,12 @@ export default class PreferencesController { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, + smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, + redesignedConfirmationsEnabled: true, + featureNotificationsEnabled: false, }, // ENS decentralized website resolution ipfsGateway: IPFS_DEFAULT_GATEWAY_URL, @@ -111,6 +113,11 @@ export default class PreferencesController { ///: END:ONLY_INCLUDE_IF useExternalNameSources: true, useTransactionSimulations: true, + enableMV3TimestampSave: true, + // Turning OFF basic functionality toggle means turning OFF this useExternalServices flag. + // Whenever useExternalServices is false, certain features will be disabled. + // The flag is true by Default, meaning the toggle is ON by default. + useExternalServices: true, ...opts.initState, }; @@ -127,7 +134,7 @@ export default class PreferencesController { } } if (accounts.size > 0) { - this.syncAddresses(Array.from(accounts)); + this.#syncAddresses(Array.from(accounts)); } }); @@ -209,6 +216,16 @@ export default class PreferencesController { this.store.updateState({ useSafeChainsListValidation: val }); } + toggleExternalServices(useExternalServices) { + this.store.updateState({ useExternalServices }); + this.setUseTokenDetection(useExternalServices); + this.setUseCurrencyRateCheck(useExternalServices); + this.setUsePhishDetect(useExternalServices); + this.setUseAddressBarEnsResolution(useExternalServices); + this.setOpenSeaEnabled(useExternalServices); + this.setUseNftDetection(useExternalServices); + } + /** * Setter for the `useTokenDetection` property * @@ -368,24 +385,6 @@ export default class PreferencesController { return textDirection; } - /** - * Updates identities to only include specified addresses. Removes identities - * not included in addresses array - * - * @param {string[]} addresses - An array of hex addresses - */ - setAddresses(addresses) { - const oldIdentities = this.store.getState().identities; - - const identities = addresses.reduce((ids, address, index) => { - const oldId = oldIdentities[address] || {}; - ids[address] = { name: `Account ${index + 1}`, address, ...oldId }; - return ids; - }, {}); - - this.store.updateState({ identities }); - } - /** * Removes an address from state * @@ -431,50 +430,6 @@ export default class PreferencesController { this.store.updateState({ identities }); } - /** - * Synchronizes identity entries with known accounts. - * Removes any unknown identities, and returns the resulting selected address. - * - * @param {Array} addresses - known to the vault. - * @returns {string} selectedAddress the selected address. - */ - syncAddresses(addresses) { - if (!Array.isArray(addresses) || addresses.length === 0) { - throw new Error('Expected non-empty array of addresses. Error #11201'); - } - - const { identities, lostIdentities } = this.store.getState(); - - const newlyLost = {}; - Object.keys(identities).forEach((identity) => { - if (!addresses.includes(identity)) { - newlyLost[identity] = identities[identity]; - delete identities[identity]; - } - }); - - // Identities are no longer present. - if (Object.keys(newlyLost).length > 0) { - // store lost accounts - Object.keys(newlyLost).forEach((key) => { - lostIdentities[key] = newlyLost[key]; - }); - } - - this.store.updateState({ identities, lostIdentities }); - this.addAddresses(addresses); - - // If the selected account is no longer valid, - // select an arbitrary other account: - let selected = this.getSelectedAddress(); - if (!addresses.includes(selected)) { - [selected] = addresses; - this.setSelectedAddress(selected); - } - - return selected; - } - /** * Setter for the `selectedAddress` property * @@ -672,6 +627,10 @@ export default class PreferencesController { this.store.updateState({ incomingTransactionsPreferences: updatedValue }); } + setServiceWorkerKeepAlivePreference(value) { + this.store.updateState({ enableMV3TimestampSave: value }); + } + getRpcMethodPreferences() { return this.store.getState().disabledRpcMethodPreferences; } @@ -681,4 +640,40 @@ export default class PreferencesController { this.store.updateState({ snapsAddSnapAccountModalDismissed: value }); } ///: END:ONLY_INCLUDE_IF + + /** + * Synchronizes identity entries with known accounts. + * Removes any unknown identities, and returns the resulting selected address. + * + * @param {Array} addresses - known to the vault. + * @returns {string} selectedAddress the selected address. + */ + #syncAddresses(addresses) { + if (!Array.isArray(addresses) || addresses.length === 0) { + throw new Error('Expected non-empty array of addresses. Error #11201'); + } + + const { identities, lostIdentities } = this.store.getState(); + + Object.keys(identities).forEach((identity) => { + if (!addresses.includes(identity)) { + // store lost accounts + lostIdentities[identity] = identities[identity]; + delete identities[identity]; + } + }); + + this.store.updateState({ identities, lostIdentities }); + this.addAddresses(addresses); + + // If the selected account is no longer valid, + // select an arbitrary other account: + let selected = this.getSelectedAddress(); + if (!addresses.includes(selected)) { + [selected] = addresses; + this.setSelectedAddress(selected); + } + + return selected; + } } diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index 4f666c03e527..6425139c5049 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -76,44 +76,20 @@ describe('preferences controller', () => { }); }); - describe('setAddresses', () => { - it('should keep a map of addresses to names and addresses in the store', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - const { identities } = preferencesController.store.getState(); - expect(identities).toStrictEqual({ - '0xda22le': { - name: 'Account 1', - address: '0xda22le', - }, - '0x7e57e2': { - name: 'Account 2', - address: '0x7e57e2', - }, - }); - }); - - it('should replace its list of addresses', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); - - const { identities } = preferencesController.store.getState(); - expect(identities).toStrictEqual({ - '0xda22le77': { - name: 'Account 1', - address: '0xda22le77', - }, - '0x7e57e277': { - name: 'Account 2', - address: '0x7e57e277', - }, - }); - }); - }); - describe('removeAddress', () => { it('should remove an address from state', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); + preferencesController.store.updateState({ + identities: { + '0xda22le': { + name: 'Account 1', + address: '0xda22le', + }, + '0x7e57e2': { + name: 'Account 2', + address: '0x7e57e2', + }, + }, + }); preferencesController.removeAddress('0xda22le'); @@ -123,10 +99,22 @@ describe('preferences controller', () => { }); it('should switch accounts if the selected address is removed', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - + preferencesController.store.updateState({ + identities: { + '0xda22le': { + name: 'Account 1', + address: '0xda22le', + }, + '0x7e57e2': { + name: 'Account 2', + address: '0x7e57e2', + }, + }, + }); preferencesController.setSelectedAddress('0x7e57e2'); + preferencesController.removeAddress('0x7e57e2'); + expect(preferencesController.getSelectedAddress()).toStrictEqual( '0xda22le', ); @@ -135,16 +123,21 @@ describe('preferences controller', () => { describe('setAccountLabel', () => { it('should update a label for the given account', () => { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - expect( - preferencesController.store.getState().identities['0xda22le'], - ).toStrictEqual({ - name: 'Account 1', - address: '0xda22le', + preferencesController.store.updateState({ + identities: { + '0xda22le': { + name: 'Account 1', + address: '0xda22le', + }, + '0x7e57e2': { + name: 'Account 2', + address: '0x7e57e2', + }, + }, }); preferencesController.setAccountLabel('0xda22le', 'Dazzle'); + expect( preferencesController.store.getState().identities['0xda22le'], ).toStrictEqual({ @@ -237,10 +230,10 @@ describe('preferences controller', () => { }); describe('setUseTokenDetection', function () { - it('should default to false', function () { + it('should default to true for new users', function () { const state = preferencesController.store.getState(); - expect(state.useTokenDetection).toStrictEqual(false); + expect(state.useTokenDetection).toStrictEqual(true); }); it('should set the useTokenDetection property in state', () => { @@ -249,6 +242,22 @@ describe('preferences controller', () => { preferencesController.store.getState().useTokenDetection, ).toStrictEqual(true); }); + + it('should keep initial value of useTokenDetection for existing users', function () { + const preferencesControllerExistingUser = new PreferencesController({ + initLangCode: 'en_US', + tokenListController, + initState: { + useTokenDetection: false, + }, + networkConfigurations: NETWORK_CONFIGURATION_DATA, + onKeyringStateChange: (listener) => { + onKeyringStateChangeListener = listener; + }, + }); + const state = preferencesControllerExistingUser.store.getState(); + expect(state.useTokenDetection).toStrictEqual(false); + }); }); describe('setUseNftDetection', () => { @@ -363,7 +372,6 @@ describe('preferences controller', () => { [NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[1]].chainId]: true, [CHAIN_IDS.GOERLI]: true, [CHAIN_IDS.SEPOLIA]: true, - [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }); }); @@ -381,7 +389,6 @@ describe('preferences controller', () => { [NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[1]].chainId]: true, [CHAIN_IDS.GOERLI]: true, [CHAIN_IDS.SEPOLIA]: true, - [CHAIN_IDS.LINEA_GOERLI]: true, [CHAIN_IDS.LINEA_SEPOLIA]: true, }); }); @@ -436,4 +443,19 @@ describe('preferences controller', () => { ).toStrictEqual(false); }); }); + + describe('setServiceWorkerKeepAlivePreference', () => { + it('should default to true', () => { + expect( + preferencesController.store.getState().enableMV3TimestampSave, + ).toStrictEqual(true); + }); + + it('should set the setServiceWorkerKeepAlivePreference property in state', () => { + preferencesController.setServiceWorkerKeepAlivePreference(false); + expect( + preferencesController.store.getState().enableMV3TimestampSave, + ).toStrictEqual(false); + }); + }); }); 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 new file mode 100644 index 000000000000..a237fd0a4684 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.test.ts @@ -0,0 +1,152 @@ +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', () => { + 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); + + await controller.enablePushNotifications(MOCK_TRIGGERS); + expect(controller.state.fcmToken).toBe(MOCK_FCM_TOKEN); + }); + }); + + 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(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'], + }) 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 new file mode 100644 index 000000000000..80c799074853 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/push-platform-notifications.ts @@ -0,0 +1,182 @@ +import { + BaseController, + RestrictedControllerMessenger, + ControllerGetStateAction, +} from '@metamask/base-controller'; +import log from 'loglevel'; + +import type { AuthenticationControllerGetBearerToken } from '../authentication/authentication-controller'; +import { + activatePushNotifications, + deactivatePushNotifications, + updateTriggerPushNotifications, +} from './services/services'; + +const controllerName = 'PushPlatformNotificationsController'; + +export type PushPlatformNotificationsControllerState = { + fcmToken: string; +}; + +export declare type PushPlatformNotificationsControllerEnablePushNotificationsAction = + { + type: `${typeof controllerName}:enablePushNotifications`; + handler: PushPlatformNotificationsController['enablePushNotifications']; + }; + +export declare type PushPlatformNotificationsControllerDisablePushNotificationsAction = + { + type: `${typeof controllerName}:disablePushNotifications`; + handler: PushPlatformNotificationsController['disablePushNotifications']; + }; + +export type PushPlatformNotificationsControllerMessengerActions = + | PushPlatformNotificationsControllerEnablePushNotificationsAction + | PushPlatformNotificationsControllerDisablePushNotificationsAction + | ControllerGetStateAction<'state', PushPlatformNotificationsControllerState>; + +type AllowedActions = AuthenticationControllerGetBearerToken; + +export type PushPlatformNotificationsControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + PushPlatformNotificationsControllerMessengerActions | AllowedActions, + never, + AllowedActions['type'], + never + >; + +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 +> { + constructor({ + messenger, + state, + }: { + messenger: PushPlatformNotificationsControllerMessenger; + state: PushPlatformNotificationsControllerState; + }) { + super({ + messenger, + metadata, + name: controllerName, + state: { + fcmToken: state?.fcmToken || '', + }, + }); + } + + 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[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + + try { + // 2. Call the activatePushNotifications method from PushPlatformNotificationsUtils + const regToken = await activatePushNotifications(bearerToken, UUIDs); + + // 3. Update the state with the FCM token + if (regToken) { + 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[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + let isPushNotificationsDisabled: boolean; + + try { + // 1. 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); + } + + // 2. Remove the FCM token from the 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[]) { + const bearerToken = await this.#getAndAssertBearerToken(); + + try { + updateTriggerPushNotifications(this.state.fcmToken, bearerToken, UUIDs); + } 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/services.test.ts b/app/scripts/controllers/push-platform-notifications/services/services.test.ts new file mode 100644 index 000000000000..c15473849dfe --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/services/services.test.ts @@ -0,0 +1,209 @@ +import * as services from './services'; + +type MockResponse = { + trigger_ids: string[]; + registration_tokens: string[]; +}; + +const MOCK_REG_TOKEN = 'REG_TOKEN'; +const MOCK_NEW_REG_TOKEN = 'NEW_REG_TOKEN'; +const MOCK_TRIGGERS = ['1', '2', '3']; +const MOCK_RESPONSE: MockResponse = { + trigger_ids: ['1', '2', '3'], + registration_tokens: ['reg-token-1', 'reg-token-2'], +}; +const MOCK_JWT = 'MOCK_JWT'; + +describe('PushPlatformNotificationsServices', () => { + describe('getPushNotificationLinks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const utils = services; + + it('Should return reg token links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValue(MOCK_RESPONSE); + + const res = await services.getPushNotificationLinks(MOCK_JWT); + + expect(res).toBeDefined(); + expect(res?.trigger_ids).toBeDefined(); + expect(res?.registration_tokens).toBeDefined(); + }); + + it('Should return null if api call fails', async () => { + jest.spyOn(services, 'getPushNotificationLinks').mockResolvedValue(null); + + const res = await utils.getPushNotificationLinks(MOCK_JWT); + expect(res).toBeNull(); + }); + }); + + describe('updateLinksAPI', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should return true if links are updated', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(true); + + const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ + MOCK_NEW_REG_TOKEN, + ]); + + expect(res).toBe(true); + }); + + it('Should return false if links are not updated', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + + const res = await services.updateLinksAPI(MOCK_JWT, MOCK_TRIGGERS, [ + MOCK_NEW_REG_TOKEN, + ]); + + expect(res).toBe(false); + }); + }); + + describe('activatePushNotifications()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should append registration token when enabling push', async () => { + jest + .spyOn(services, 'activatePushNotifications') + .mockResolvedValue(MOCK_NEW_REG_TOKEN); + 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 () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(null); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + it('should fail if unable to create new reg token', async () => { + jest.spyOn(services, 'createRegToken').mockResolvedValueOnce(null); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + it('should fail if unable to update links', async () => { + jest.spyOn(services, 'updateLinksAPI').mockResolvedValueOnce(false); + const res = await services.activatePushNotifications( + MOCK_JWT, + MOCK_TRIGGERS, + ); + expect(res).toBeNull(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); + + describe('deactivatePushNotifications()', () => { + it('should fail if unable to get existing notification links', async () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(null); + + 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 () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValue(MOCK_RESPONSE); + jest.spyOn(services, 'updateLinksAPI').mockResolvedValue(false); + + 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 () => { + jest + .spyOn(services, 'getPushNotificationLinks') + .mockResolvedValueOnce(MOCK_RESPONSE); + jest.spyOn(services, 'deleteRegToken').mockResolvedValue(false); + + const res = await services.deactivatePushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + }); + + describe('updateTriggerPushNotifications()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should update triggers for push notifications', async () => { + jest + .spyOn(services, 'updateTriggerPushNotifications') + .mockResolvedValue(true); + + const res = await services.updateTriggerPushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(true); + }); + + it('should fail if unable to update triggers', async () => { + jest + .spyOn(services, 'updateTriggerPushNotifications') + .mockResolvedValue(false); + + const res = await services.updateTriggerPushNotifications( + MOCK_REG_TOKEN, + MOCK_JWT, + MOCK_TRIGGERS, + ); + + expect(res).toBe(false); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + }); +}); diff --git a/app/scripts/controllers/push-platform-notifications/services/services.ts b/app/scripts/controllers/push-platform-notifications/services/services.ts new file mode 100644 index 000000000000..c73f11f6cc66 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/services/services.ts @@ -0,0 +1,278 @@ +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 { onPushNotification } from '../utils/get-notification-message'; + +const url = process.env.PUSH_NOTIFICATIONS_SERVICE_URL; +const REGISTRATION_TOKENS_ENDPOINT = `${url}/v1/link`; +const sw = self as unknown as ServiceWorkerGlobalScope; + +export type LinksResult = { + trigger_ids: string[]; + registration_tokens: string[]; +}; + +/** + * 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. + */ +export 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. + */ +export 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. + */ +export 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. + */ +export 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) { + throw new Error('Failed to fetch links'); + } + return response.json() as Promise; + } catch { + 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: string[], +): Promise { + try { + const body: LinksResult = { + trigger_ids: triggers, + registration_tokens: regTokens, + }; + const response = await fetch( + `${process.env.PUSH_NOTIFICATIONS_SERVICE_URL}/v1/link`, + { + 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 messaging = await getFirebaseMessaging(); + + onBackgroundMessage( + messaging, + async (payload: MessagePayload): Promise => { + const typedPayload = payload; + + // if the payload does not contain data, do nothing + try { + const notificationData = typedPayload?.data?.data + ? JSON.parse(typedPayload?.data?.data) + : undefined; + if (!notificationData) { + return; + } + + await onPushNotification(notificationData); + } 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'); + } + }, + ); + + const newRegTokens = new Set(notificationLinks.registration_tokens); + newRegTokens.add(regToken); + + await updateLinksAPI(bearerToken, triggers, Array.from(newRegTokens)); + return regToken; +} + +/** + * 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 regTokenSet = new Set(notificationLinks.registration_tokens); + regTokenSet.delete(regToken); + + const isTokenRemovedFromAPI = await updateLinksAPI( + bearerToken, + triggers, + Array.from(regTokenSet), + ); + 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. + * + * @param regToken - The registration token to update triggers for. + * @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 true if the triggers were successfully updated, false otherwise. + */ +export async function updateTriggerPushNotifications( + regToken: string, + bearerToken: string, + triggers: string[], +): Promise { + const notificationLinks = await getPushNotificationLinks(bearerToken); + if (!notificationLinks) { + return false; + } + + // Create new registration token if doesn't exist + const regTokenSet = new Set(notificationLinks.registration_tokens); + if (!regToken || !regTokenSet.has(regToken)) { + await deleteRegToken(); + const newRegToken = await createRegToken(); + if (!newRegToken) { + throw new Error('Failed to create a new registration token'); + } + regTokenSet.add(newRegToken); + } + + const isTriggersLinkedToPushNotifications = await updateLinksAPI( + bearerToken, + triggers, + Array.from(regTokenSet), + ); + + return isTriggersLinkedToPushNotifications; +} diff --git a/app/scripts/controllers/push-platform-notifications/types/firebase.ts b/app/scripts/controllers/push-platform-notifications/types/firebase.ts new file mode 100644 index 000000000000..91dc2c2e4d6f --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/types/firebase.ts @@ -0,0 +1,46 @@ +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 new file mode 100644 index 000000000000..2ffbe89fdd9e --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.test.ts @@ -0,0 +1,80 @@ +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 new file mode 100644 index 000000000000..f95149c54c19 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-data.ts @@ -0,0 +1,90 @@ +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.test.ts b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts new file mode 100644 index 000000000000..75f65d822467 --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.test.ts @@ -0,0 +1,150 @@ +import { + createMockNotificationERC1155Received, + createMockNotificationERC1155Sent, + createMockNotificationERC20Received, + createMockNotificationERC20Sent, + createMockNotificationERC721Received, + createMockNotificationERC721Sent, + createMockNotificationEthReceived, + createMockNotificationEthSent, + createMockNotificationLidoReadyToBeWithdrawn, + createMockNotificationLidoStakeCompleted, + createMockNotificationLidoWithdrawalCompleted, + createMockNotificationLidoWithdrawalRequested, + createMockNotificationMetaMaskSwapsCompleted, + createMockNotificationRocketPoolStakeCompleted, + createMockNotificationRocketPoolUnStakeCompleted, +} from '../../metamask-notifications/mocks/mock-raw-notifications'; +import { createNotificationMessage } from './get-notification-message'; + +describe('notification-message tests', () => { + test('displays erc20 sent notification', () => { + const notification = createMockNotificationERC20Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds sent'); + expect(result?.description).toContain('You successfully sent 4.96K USDC'); + }); + + test('displays erc20 received notification', () => { + const notification = createMockNotificationERC20Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds received'); + expect(result?.description).toContain('You received 8.38B SHIB'); + }); + + test('displays eth/native sent notification', () => { + const notification = createMockNotificationEthSent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds sent'); + expect(result?.description).toContain('You successfully sent 0.005 ETH'); + }); + + test('displays eth/native received notification', () => { + const notification = createMockNotificationEthReceived(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Funds received'); + expect(result?.description).toContain('You received 808 ETH'); + }); + + test('displays metamask swap completed notification', () => { + const notification = createMockNotificationMetaMaskSwapsCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Swap completed'); + expect(result?.description).toContain('Your MetaMask Swap was successful'); + }); + + test('displays erc721 sent notification', () => { + const notification = createMockNotificationERC721Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT sent'); + expect(result?.description).toContain('You have successfully sent an NFT'); + }); + + test('displays erc721 received notification', () => { + const notification = createMockNotificationERC721Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT received'); + expect(result?.description).toContain('You received new NFTs'); + }); + + test('displays erc1155 sent notification', () => { + const notification = createMockNotificationERC1155Sent(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT sent'); + expect(result?.description).toContain('You have successfully sent an NFT'); + }); + + test('displays erc1155 received notification', () => { + const notification = createMockNotificationERC1155Received(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('NFT received'); + expect(result?.description).toContain('You received new NFTs'); + }); + + test('displays rocketpool stake completed notification', () => { + const notification = createMockNotificationRocketPoolStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake complete'); + expect(result?.description).toContain( + 'Your RocketPool stake was successful', + ); + }); + + test('displays rocketpool unstake completed notification', () => { + const notification = createMockNotificationRocketPoolUnStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Unstake complete'); + expect(result?.description).toContain( + 'Your RocketPool unstake was successful', + ); + }); + + test('displays lido stake completed notification', () => { + const notification = createMockNotificationLidoStakeCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake complete'); + expect(result?.description).toContain('Your Lido stake was successful'); + }); + + test('displays lido stake ready to be withdrawn notification', () => { + const notification = createMockNotificationLidoReadyToBeWithdrawn(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Stake ready for withdrawal'); + expect(result?.description).toContain( + 'Your Lido stake is now ready to be withdrawn', + ); + }); + + test('displays lido withdrawal requested notification', () => { + const notification = createMockNotificationLidoWithdrawalRequested(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Withdrawal requested'); + expect(result?.description).toContain( + 'Your Lido withdrawal request was submitted', + ); + }); + + test('displays lido withdrawal completed notification', () => { + const notification = createMockNotificationLidoWithdrawalCompleted(); + const result = createNotificationMessage(notification); + + expect(result?.title).toBe('Withdrawal completed'); + expect(result?.description).toContain( + 'Your Lido withdrawal was successful', + ); + }); +}); 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 new file mode 100644 index 000000000000..9fb296b51b4d --- /dev/null +++ b/app/scripts/controllers/push-platform-notifications/utils/get-notification-message.ts @@ -0,0 +1,246 @@ +// 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 { getAmount, formatAmount } from './get-notification-data'; + +type PushNotificationMessage = { + title: string; + description: string; +}; + +type NotificationMessage< + N extends OnChainRawNotification = OnChainRawNotification, +> = { + 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; + +function getChainSymbol(chainId: number) { + return CHAIN_SYMBOLS[chainId] ?? null; +} + +export async function onPushNotification(notification: unknown): Promise { + if (!notification) { + return; + } + if (!isOnChainNotification(notification)) { + return; + } + + const notificationMessage = createNotificationMessage(notification); + if (!notificationMessage) { + return; + } + + const registration = sw?.registration; + if (!registration) { + return; + } + + await registration.showNotification(notificationMessage.title, { + body: notificationMessage.description, + icon: './images/icon-64.png', + tag: notification?.id, + data: notification, + }); +} + +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: OnChainRawNotification, +): PushNotificationMessage | null { + if (!n?.data?.kind) { + return null; + } + const notificationMessage = notificationMessageDict[n.data.kind] 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.js b/app/scripts/controllers/swaps.js index 165faed699e0..38c9fb761ca6 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -43,7 +43,6 @@ import { calcGasTotal, calcTokenAmount, } from '../../../shared/lib/transactions-controller-utils'; -import fetchEstimatedL1Fee from '../../../ui/helpers/utils/optimism/fetchEstimatedL1Fee'; import { Numeric } from '../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../shared/constants/common'; @@ -118,6 +117,7 @@ export default class SwapsController { getTokenRatesState, fetchTradesInfo = defaultFetchTradesInfo, getCurrentChainId, + getLayer1GasFee, getEIP1559GasFeeEstimates, trackMetaMetricsEvent, }, @@ -143,6 +143,8 @@ export default class SwapsController { this._getCurrentChainId = getCurrentChainId; this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates; + this._getLayer1GasFee = getLayer1GasFee; + this.getBufferedGasLimit = getBufferedGasLimit; this.getTokenRatesState = getTokenRatesState; this.trackMetaMetricsEvent = trackMetaMetricsEvent; @@ -321,14 +323,11 @@ export default class SwapsController { await Promise.all( Object.values(newQuotes).map(async (quote) => { if (quote.trade) { - const multiLayerL1TradeFeeTotal = await fetchEstimatedL1Fee( + const multiLayerL1TradeFeeTotal = await this._getLayer1GasFee({ + transactionParams: quote.trade, chainId, - { - txParams: quote.trade, - chainId, - }, - this.ethersProvider, - ); + }); + quote.multiLayerL1TradeFeeTotal = multiLayerL1TradeFeeTotal; } return quote; diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index a7ebd0aaed74..9494a94abc09 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -128,7 +128,11 @@ const EMPTY_INIT_STATE = { const sandbox = sinon.createSandbox(); let fetchTradesInfoStub = sandbox.stub(); const getCurrentChainIdStub = sandbox.stub(); +const getLayer1GasFeeStub = sandbox.stub(); +const getNetworkClientIdStub = sandbox.stub(); getCurrentChainIdStub.returns(CHAIN_IDS.MAINNET); +getNetworkClientIdStub.returns('1'); +getLayer1GasFeeStub.resolves('0x1'); const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => { return { gasFeeEstimates: { @@ -150,6 +154,8 @@ describe('SwapsController', function () { fetchTradesInfo: fetchTradesInfoStub, getCurrentChainId: getCurrentChainIdStub, getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, + getNetworkClientId: getNetworkClientIdStub, + getLayer1GasFee: getLayer1GasFeeStub, }); }; @@ -667,9 +673,9 @@ describe('SwapsController', function () { total: '5.43388249494949494949494949494949495', medianMetaMaskFee: '0.444444444444444444444444444444444444', }, - ethFee: '0.113822', - multiLayerL1TradeFeeTotal: '0x0103c18816d4e8', - overallValueOfQuote: '49.886178', + ethFee: '0.113536', + multiLayerL1TradeFeeTotal: '0x1', + overallValueOfQuote: '49.886464', metaMaskFeeInEth: '0.50505050505050505050505050505050505', ethValueOfTokens: '50', }); @@ -851,6 +857,8 @@ describe('SwapsController', function () { getTokenRatesState: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, getCurrentChainId: getCurrentChainIdStub, + getLayer1GasFee: getLayer1GasFeeStub, + getNetworkClientId: getNetworkClientIdStub, }); const firstEthersInstance = _swapsController.ethersProvider; const firstEthersProviderChainId = diff --git a/app/scripts/controllers/user-storage/encryption.test.ts b/app/scripts/controllers/user-storage/encryption.test.ts new file mode 100644 index 000000000000..31e1aa151367 --- /dev/null +++ b/app/scripts/controllers/user-storage/encryption.test.ts @@ -0,0 +1,38 @@ +import encryption, { createSHA256Hash } from './encryption'; + +describe('encryption tests', () => { + const PASSWORD = '123'; + const DATA1 = 'Hello World'; + const DATA2 = JSON.stringify({ foo: 'bar' }); + + 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); + }); + + it('Should decrypt some existing data', () => { + const encryptedData = `{"v":"1","d":"R+sCbzS6clo5iLbSzBr889miNfHhCBmOCk2CFwTH55IkbOIL9f5Nm2t0nmWOVtFbjLpnj6cKyw==","iterations":900000}`; + 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.ts b/app/scripts/controllers/user-storage/encryption.ts new file mode 100644 index 000000000000..f8081e97587a --- /dev/null +++ b/app/scripts/controllers/user-storage/encryption.ts @@ -0,0 +1,138 @@ +import { pbkdf2 } from '@noble/hashes/pbkdf2'; +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'; + +export type EncryptedPayload = { + v: '1'; // version + d: string; // data + iterations: number; +}; + +function byteArrayToBase64(byteArray: Uint8Array) { + return Buffer.from(byteArray).toString('base64'); +} + +function base64ToByteArray(base64: string) { + return new Uint8Array(Buffer.from(base64, 'base64')); +} + +function bytesToUtf8(byteArray: Uint8Array) { + const decoder = new TextDecoder('utf-8'); + return decoder.decode(byteArray); +} + +class EncryptorDecryptor { + #ALGORITHM_NONCE_SIZE: number = 12; // 12 bytes + + #ALGORITHM_KEY_SIZE: number = 16; // 16 bytes + + #PBKDF2_SALT_SIZE: number = 16; // 16 bytes + + #PBKDF2_ITERATIONS: number = 900_000; + + 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') { + return this.#decryptStringV1(encryptedData, password); + } + throw new Error(`Unsupported encrypted data payload - ${encryptedData}`); + } 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 salt = randomBytes(this.#PBKDF2_SALT_SIZE); + + // Derive a key using PBKDF2. + const key = pbkdf2(sha256, password, salt, { + c: this.#PBKDF2_ITERATIONS, + 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', + d: encryptedData, + iterations: this.#PBKDF2_ITERATIONS, + }; + + return JSON.stringify(encryptedPayload); + } + + #decryptStringV1(data: EncryptedPayload, password: string): string { + const { iterations, d: base64CiphertextAndNonceAndSalt } = data; + + // Decode the base64. + const ciphertextAndNonceAndSalt = base64ToByteArray( + base64CiphertextAndNonceAndSalt, + ); + + // Create buffers of salt and ciphertextAndNonce. + const salt = ciphertextAndNonceAndSalt.slice(0, this.#PBKDF2_SALT_SIZE); + const ciphertextAndNonce = ciphertextAndNonceAndSalt.slice( + this.#PBKDF2_SALT_SIZE, + ciphertextAndNonceAndSalt.length, + ); + + // Derive the key using PBKDF2. + const key = pbkdf2(sha256, password, salt, { + c: iterations, + dkLen: this.#ALGORITHM_KEY_SIZE, + }); + + // 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); + } +} + +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/mocks/mockServices.ts b/app/scripts/controllers/user-storage/mocks/mockServices.ts new file mode 100644 index 000000000000..97ffa703dc9c --- /dev/null +++ b/app/scripts/controllers/user-storage/mocks/mockServices.ts @@ -0,0 +1,40 @@ +import nock from 'nock'; +import { USER_STORAGE_ENDPOINT, GetUserStorageResponse } from '../services'; +import { createEntryPath } from '../schema'; +import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage'; + +export const MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT = `${USER_STORAGE_ENDPOINT}${createEntryPath( + 'notification_settings', + MOCK_STORAGE_KEY, +)}`; + +type MockReply = { + status: nock.StatusCode; + body?: nock.Body; +}; + +const MOCK_GET_USER_STORAGE_RESPONSE: GetUserStorageResponse = { + HashedKey: 'HASHED_KEY', + Data: MOCK_ENCRYPTED_STORAGE_DATA, +}; +export function mockEndpointGetUserStorage(mockReply?: MockReply) { + const reply = mockReply ?? { + status: 200, + body: MOCK_GET_USER_STORAGE_RESPONSE, + }; + + const mockEndpoint = nock(MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT) + .get('') + .reply(reply.status, reply.body); + + return mockEndpoint; +} + +export function mockEndpointUpsertUserStorage( + mockReply?: Pick, +) { + const mockEndpoint = nock(MOCK_USER_STORAGE_NOTIFICATIONS_ENDPOINT) + .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 new file mode 100644 index 000000000000..4a43a80556e1 --- /dev/null +++ b/app/scripts/controllers/user-storage/mocks/mockStorage.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000000..d08b0802bf49 --- /dev/null +++ b/app/scripts/controllers/user-storage/schema.test.ts @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000000..19bc0ccfae52 --- /dev/null +++ b/app/scripts/controllers/user-storage/schema.ts @@ -0,0 +1,38 @@ +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 new file mode 100644 index 000000000000..a746dcee858f --- /dev/null +++ b/app/scripts/controllers/user-storage/services.test.ts @@ -0,0 +1,89 @@ +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 new file mode 100644 index 000000000000..269009850079 --- /dev/null +++ b/app/scripts/controllers/user-storage/services.ts @@ -0,0 +1,83 @@ +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 new file mode 100644 index 000000000000..efb0cd4f4ef7 --- /dev/null +++ b/app/scripts/controllers/user-storage/user-storage-controller.test.ts @@ -0,0 +1,333 @@ +import nock from 'nock'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { + AuthenticationControllerGetBearerToken, + AuthenticationControllerGetSessionProfile, + AuthenticationControllerIsSignedIn, + AuthenticationControllerPerformSignIn, +} from '../authentication/authentication-controller'; +import { + MOCK_STORAGE_DATA, + MOCK_STORAGE_KEY, + MOCK_STORAGE_KEY_SIGNATURE, +} from './mocks/mockStorage'; +import UserStorageController, { + AllowedActions, +} 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, + }); + + 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, + }); + + 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, + state: { isProfileSyncingEnabled: false }, + }); + + await expect( + controller.performGetStorage('notification_settings'), + ).rejects.toThrow(); + }); + + 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 (_, arrangeFailureCase) => { + const { messengerMocks } = arrangeMocks(); + arrangeFailureCase(messengerMocks); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + }); + + 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, + }); + + 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, + state: { isProfileSyncingEnabled: false }, + }); + + await expect( + controller.performSetStorage('notification_settings', 'new data'), + ).rejects.toThrow(); + }); + + 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 (_, arrangeFailureCase) => { + const { messengerMocks } = arrangeMocks(); + arrangeFailureCase(messengerMocks); + const controller = new UserStorageController({ + messenger: messengerMocks.messenger, + }); + + 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, + }); + 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, + }); + + 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, + state: { isProfileSyncingEnabled: 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, + }); + + 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, + state: { isProfileSyncingEnabled: 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, + never + >().getRestricted({ + name: 'UserStorageController', + allowedActions: [ + 'SnapController:handleRequest', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:getSessionProfile', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignIn', + ], + }); + + 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: '', + metametricsId: '', + profileId: 'MOCK_PROFILE_ID', + }); + + const mockAuthPerformSignIn = + typedMockFn< + AuthenticationControllerPerformSignIn['handler'] + >().mockResolvedValue('New Access Token'); + + const mockAuthIsSignedIn = + typedMockFn< + AuthenticationControllerIsSignedIn['handler'] + >().mockReturnValue(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(); + } + + function exhaustedMessengerMocks(action: never) { + throw new Error(`MOCK_FAIL - unsupported messenger call: ${action}`); + } + + return exhaustedMessengerMocks(actionType); + }); + + return { + messenger, + mockSnapGetPublicKey, + mockSnapSignMessage, + mockAuthGetBearerToken, + mockAuthGetSessionProfile, + mockAuthPerformSignIn, + mockAuthIsSignedIn, + }; +} diff --git a/app/scripts/controllers/user-storage/user-storage-controller.ts b/app/scripts/controllers/user-storage/user-storage-controller.ts new file mode 100644 index 000000000000..bb2c66cfd222 --- /dev/null +++ b/app/scripts/controllers/user-storage/user-storage-controller.ts @@ -0,0 +1,303 @@ +import { + BaseController, + RestrictedControllerMessenger, + StateMetadata, +} from '@metamask/base-controller'; +import { HandleSnapRequest } from '@metamask/snaps-controllers'; +import { + AuthenticationControllerGetBearerToken, + AuthenticationControllerGetSessionProfile, + AuthenticationControllerIsSignedIn, + AuthenticationControllerPerformSignIn, +} from '../authentication/authentication-controller'; +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; +}; +const defaultState: UserStorageControllerState = { + isProfileSyncingEnabled: true, +}; +const metadata: StateMetadata = { + isProfileSyncingEnabled: { + persist: true, + anonymous: true, + }, +}; + +// 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']; + +// Allowed Actions +export type AllowedActions = + // Snap Requests + | HandleSnapRequest + // Auth Requests + | AuthenticationControllerGetBearerToken + | AuthenticationControllerGetSessionProfile + | AuthenticationControllerPerformSignIn + | AuthenticationControllerIsSignedIn; + +// Messenger +export type UserStorageControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + Actions | AllowedActions, + never, + AllowedActions['type'], + never +>; + +/** + * 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', + ); + }, + }; + + constructor(params: { + messenger: UserStorageControllerMessenger; + state?: UserStorageControllerState; + }) { + super({ + messenger: params.messenger, + metadata, + name: controllerName, + state: { ...defaultState, ...params.state }, + }); + + 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 { + const isAlreadyEnabled = this.state.isProfileSyncingEnabled; + if (isAlreadyEnabled) { + return; + } + + try { + const authEnabled = this.#auth.isAuthEnabled(); + if (!authEnabled) { + await this.#auth.signIn(); + } + + this.update((state) => { + state.isProfileSyncingEnabled = true; + }); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : e; + throw new Error( + `${controllerName} - failed to enable profile syncing - ${errorMessage}`, + ); + } + } + + public async disableProfileSyncing(): Promise { + const isAlreadyDisabled = !this.state.isProfileSyncingEnabled; + if (isAlreadyDisabled) { + return; + } + + this.update((state) => { + state.isProfileSyncingEnabled = false; + }); + } + + /** + * 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; + } + + /** + * Signs a specific message using an underlying auth snap. + * + * @param message - A specific tagged message to sign. + * @returns A Signature created by the snap. + */ + #snapSignMessage(message: `metamask:${string}`): Promise { + return this.messagingSystem.call( + 'SnapController:handleRequest', + createSnapSignMessageRequest(message), + ) as Promise; + } +} diff --git a/app/scripts/lib/AbstractPetnamesBridge.test.ts b/app/scripts/lib/AbstractPetnamesBridge.test.ts index 608454b38afe..3347c90f13f5 100644 --- a/app/scripts/lib/AbstractPetnamesBridge.test.ts +++ b/app/scripts/lib/AbstractPetnamesBridge.test.ts @@ -94,6 +94,8 @@ function createNameControllerMock(state: NameControllerState) { function createMessengerMock(): jest.Mocked { return { subscribe: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; } diff --git a/app/scripts/lib/AccountIdentitiesPetnamesBridge.test.ts b/app/scripts/lib/AccountIdentitiesPetnamesBridge.test.ts index 6431381d7099..b7ac979f3a1c 100644 --- a/app/scripts/lib/AccountIdentitiesPetnamesBridge.test.ts +++ b/app/scripts/lib/AccountIdentitiesPetnamesBridge.test.ts @@ -105,16 +105,22 @@ function createNameControllerMock( return { state, setName: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; } function simulateSubscribe( messenger: jest.Mocked, stateChange: AccountsControllerState, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any patch: any[], ) { const listener = messenger.subscribe.mock.calls[0][1] as ( stateChange: AccountsControllerState, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any patch: any[], ) => void; listener(stateChange, patch); diff --git a/app/scripts/lib/AddressBookPetnamesBridge.test.ts b/app/scripts/lib/AddressBookPetnamesBridge.test.ts index c98e6a5e309e..f95726fba7b2 100644 --- a/app/scripts/lib/AddressBookPetnamesBridge.test.ts +++ b/app/scripts/lib/AddressBookPetnamesBridge.test.ts @@ -37,12 +37,16 @@ function createNameControllerMock( return { state, setName: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; } function createMessengerMock(): jest.Mocked { return { subscribe: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; } diff --git a/app/scripts/lib/AddressBookPetnamesBridge.ts b/app/scripts/lib/AddressBookPetnamesBridge.ts index e6f7e9813fa1..141814a9ef62 100644 --- a/app/scripts/lib/AddressBookPetnamesBridge.ts +++ b/app/scripts/lib/AddressBookPetnamesBridge.ts @@ -35,9 +35,13 @@ export class AddressBookPetnamesBridge extends AbstractPetnamesBridge { const entries: PetnameEntry[] = []; const { state } = this.#addressBookController; for (const chainId of Object.keys(state.addressBook)) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const chainEntries = state.addressBook[chainId as any]; for (const address of Object.keys(chainEntries)) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const entry = state.addressBook[chainId as any][address]; const normalizedChainId = chainId.toLowerCase(); const { name, isEns } = entry; @@ -64,11 +68,17 @@ export class AddressBookPetnamesBridge extends AbstractPetnamesBridge { */ protected updateSourceEntry(type: ChangeType, entry: PetnameEntry): void { if (type === ChangeType.DELETED) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.#addressBookController.delete(entry.variation as any, entry.value); } else { this.#addressBookController.set( entry.value, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any entry.name as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any entry.variation as any, ); } diff --git a/app/scripts/lib/SnapsNameProvider.test.ts b/app/scripts/lib/SnapsNameProvider.test.ts index 6d6f0dd6a120..25a5f22a3f80 100644 --- a/app/scripts/lib/SnapsNameProvider.test.ts +++ b/app/scripts/lib/SnapsNameProvider.test.ts @@ -94,6 +94,8 @@ function createMockMessenger({ return { call: callMock, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; } diff --git a/app/scripts/lib/WeakRefObjectMap.test.ts b/app/scripts/lib/WeakRefObjectMap.test.ts new file mode 100644 index 000000000000..289bc4a8548c --- /dev/null +++ b/app/scripts/lib/WeakRefObjectMap.test.ts @@ -0,0 +1,147 @@ +import { WeakRefObjectMap } from './WeakRefObjectMap'; + +describe('WeakDomainProxyMap', () => { + let map: WeakRefObjectMap>; + + beforeEach(() => { + map = new WeakRefObjectMap(); + }); + + it('sets and gets a value', () => { + const key: string = 'testKey'; + const value: { [key: string]: object } = { objKey: {} }; + map.set(key, value); + + const retrieved = map.get(key); + expect(retrieved).toHaveProperty('objKey'); + expect(retrieved?.objKey).toBe(value.objKey); + }); + + it('confirms presence of a key with has()', () => { + const key: string = 'testKey'; + const value: { [key: string]: object } = { objKey: {} }; + map.set(key, value); + + expect(map.has(key)).toBe(true); + }); + + it('deletes a key-value pair', () => { + const key: string = 'testKey'; + const value: { [key: string]: object } = { objKey: {} }; + map.set(key, value); + + expect(map.has(key)).toBe(true); + map.delete(key); + expect(map.has(key)).toBe(false); + }); + + it('clears the map', () => { + map.set('key1', { objKey: {} }); + map.set('key2', { objKey: {} }); + + map.clear(); + expect(map.has('key1')).toBe(false); + expect(map.has('key2')).toBe(false); + }); + + it('get returns undefined for non-existent key', () => { + expect(map.get('nonExistentKey')).toBeUndefined(); + }); + + it('delete returns false when key does not exist', () => { + expect(map.delete('nonExistentKey')).toBe(false); + }); + + describe('iterators', () => { + beforeEach(() => { + map = new WeakRefObjectMap(); + map.set('key1', { objKey1: { detail: 'value1' } }); + map.set('key2', { objKey2: { detail: 'value2' } }); + }); + + it('iterates over entries correctly', () => { + const entries = Array.from(map.entries()); + expect(entries.length).toBe(2); + expect(entries).toEqual( + expect.arrayContaining([ + ['key1', { objKey1: { detail: 'value1' } }], + ['key2', { objKey2: { detail: 'value2' } }], + ]), + ); + }); + + it('iterates over keys correctly', () => { + const keys = Array.from(map.keys()); + expect(keys.length).toBe(2); + expect(keys).toEqual(expect.arrayContaining(['key1', 'key2'])); + }); + + it('iterates over values correctly', () => { + const values = Array.from(map.values()); + expect(values.length).toBe(2); + expect(values).toEqual( + expect.arrayContaining([ + { objKey1: { detail: 'value1' } }, + { objKey2: { detail: 'value2' } }, + ]), + ); + }); + + it('executes forEach callback correctly', () => { + const mockCallback = jest.fn(); + map.forEach(mockCallback); + + expect(mockCallback.mock.calls.length).toBe(2); + expect(mockCallback).toHaveBeenCalledWith( + { objKey1: { detail: 'value1' } }, + 'key1', + map, + ); + expect(mockCallback).toHaveBeenCalledWith( + { objKey2: { detail: 'value2' } }, + 'key2', + map, + ); + }); + + it('handles empty map in iterations', () => { + const emptyMap = new WeakRefObjectMap>(); + expect(Array.from(emptyMap.entries()).length).toBe(0); + expect(Array.from(emptyMap.keys()).length).toBe(0); + expect(Array.from(emptyMap.values()).length).toBe(0); + + const mockCallback = jest.fn(); + emptyMap.forEach(mockCallback); + expect(mockCallback).not.toHaveBeenCalled(); + }); + + it('[Symbol.iterator] behaves like entries', () => { + const iterator = map[Symbol.iterator](); + expect(Array.from(iterator)).toEqual(Array.from(map.entries())); + }); + }); +}); + +// Commenting until we figure out how best to expose garbage collection in jest env +// describe('WeakDomainProxyMap with garbage collection', () => { +// it('cleans up weakly referenced objects after garbage collection', () => { +// if ((global as any).gc) { +// const map = new WeakDomainProxyMap(); +// let obj: object = { a: 1 }; +// map.set('key', { obj }); + +// expect(map.get('key')).toHaveProperty('obj', obj); + +// obj = null!; // Remove the strong reference to the object + +// (global as any).gc(); // Force garbage collection + +// // The weakly referenced object should be gone after garbage collection. +// expect(map.get('key')).toBeUndefined(); +// } else { +// console.warn( +// 'Garbage collection is not exposed. Run Node.js with the --expose-gc flag.', +// ); +// } +// }); +// }); diff --git a/app/scripts/lib/WeakRefObjectMap.ts b/app/scripts/lib/WeakRefObjectMap.ts new file mode 100644 index 000000000000..56907926daa0 --- /dev/null +++ b/app/scripts/lib/WeakRefObjectMap.ts @@ -0,0 +1,218 @@ +type WeakRefObject> = { + [P in keyof RecordType]: WeakRef; +}; + +/** + * `WeakRefObjectMap` is a custom map-like structure designed to hold key-value pairs where the values are objects. + * Unlike a standard `Map`, this implementation stores each property of the value objects as weak references. + * This means that the properties of the objects are not prevented from being garbage collected when there are no other + * references to them outside of this map. + * + * It is important to note that while the map itself behaves similarly to a standard `Map`, the weak references apply + * to each property of the objects stored as values. This means that individual properties of these objects may become + * unavailable (i.e., garbage collected) independently of one another. Users of this map should be prepared to handle + * cases where a property's value has been collected and is therefore `undefined`. + * + * This class was implemented to help with memory management of network client proxies used by the SelectedNetworkController + * to keep per domain selected networks in sync. The properties of the NetworkClient object (provider and blockTracker) are weakly + * referenced so that they can be garbage collected if/when a dapp connection ends without effective cleanup. + */ + +export class WeakRefObjectMap> + implements Map +{ + /** + * Internal map to store keys and their corresponding weakly referenced object values. + */ + private map: Map>; + + constructor() { + this.map = new Map(); + } + + /** + * Associates a key with a value in the map. If the key already exists, its associated value is updated. + * The values are stored as weak references. + * + * @param key - The key under which to store the value. + * @param value - The value to store under the specified key. Must be an object. + * @returns The `WeakRefObjectMap` instance. + */ + set(key: string, value: RecordType): this { + const weakRefValue: Partial> = {}; + for (const keyValue in value) { + if (!Object.hasOwn(value, keyValue)) { + continue; + } + const item: RecordType[typeof keyValue] = value[keyValue]; + if (typeof item === 'object' && item !== null) { + weakRefValue[keyValue] = new WeakRef(item); + } else { + throw new Error( + `Property ${String( + keyValue, + )} is not an object and cannot be weakly referenced.`, + ); + } + } + this.map.set(key, weakRefValue as WeakRefObject); + return this; + } + + /** + * Retrieves the value associated with the specified key. The value is dereferenced before being returned. + * If the key does not exist or the value has been garbage collected, `undefined` is returned. + * + * @param key - The key whose associated value is to be returned. + * @returns The dereferenced value associated with the key, or `undefined`. + */ + get(key: string): RecordType | undefined { + const weakRefValue = this.map.get(key); + if (!weakRefValue) { + return undefined; + } + + const deRefValue: Partial = {}; + for (const keyValue in weakRefValue) { + if (!Object.hasOwn(weakRefValue, keyValue)) { + continue; + } + const deref = weakRefValue[keyValue].deref(); + if (deref === undefined) { + this.map.delete(key); + return undefined; + } + deRefValue[keyValue] = deref; + } + + return deRefValue as RecordType; + } + + /** + * Checks whether the map contains the specified key. + * + * @param key - The key to check for presence in the map. + * @returns `true` if the map contains the key, otherwise `false`. + */ + has(key: string): boolean { + return this.get(key) !== undefined; + } + + /** + * Removes the specified key and its associated value from the map. + * + * @param key - The key to remove along with its associated value. + * @returns `true` if the element was successfully removed, otherwise `false`. + */ + delete(key: string): boolean { + const value = this.get(key); + if (value !== undefined) { + return this.map.delete(key); + } + return false; + } + + /** + * Removes all key-value pairs from the map. + */ + clear() { + this.map.clear(); + } + + /** + * Returns the number of key-value pairs present in the map. + */ + get size(): number { + return this.map.size; + } + + /** + * Returns a new iterator object that contains an array of `[key, value]` for each element in the map. + * The values are dereferenced before being returned. + */ + entries(): IterableIterator<[string, RecordType]> { + const entries: [string, RecordType][] = []; + this.map.forEach((_, key) => { + const derefValue = this.get(key); + if (derefValue !== undefined) { + entries.push([key, derefValue]); + } + }); + return entries.values(); + } + + /** + * Returns a new iterator object that contains the keys for each element in the map. + */ + keys(): IterableIterator { + return this.map.keys(); + } + + /** + * Returns a new iterator object that contains the values for each element in the map. + * The values are dereferenced before being returned. + */ + values(): IterableIterator { + const values: RecordType[] = []; + this.map.forEach((_, key) => { + const derefValue = this.get(key); + if (derefValue !== undefined) { + values.push(derefValue); + } + }); + return values.values(); + } + + /** + * Returns a new iterator object that contains an array of `[key, value]` for each element in the map, + * making the map itself iterable. + */ + [Symbol.iterator](): IterableIterator<[string, RecordType]> { + return this.entries(); + } + + /** + * Returns a string representing the map. This is used when converting the map to a string, + * e.g., by `Object.prototype.toString`. + */ + get [Symbol.toStringTag](): string { + return 'WeakRefObjectMap'; + } + + /** + * Executes a provided function once for each key-value pair in the map, in insertion order. + * Note that the values passed to the callback function are the `WeakRefObject`s, + * not the dereferenced objects. This allows consumers to manage dereferencing according to their needs, + * acknowledging that some references may have been garbage collected. + * + * @param callback - Function to execute for each element, taking three arguments: + * - `value`: The value part of the key-value pair. Note that this is the weakly referenced object, + * encapsulated within a `WeakRefObject`, allowing for manual dereferencing. + * -`key`: The key part of the key-value pair. + * - `map`: The `WeakRefObjectMap` instance that the `forEach` method was called on. + * @param thisArg - Optional. Value to use as `this` when executing `callback`. + */ + forEach( + callback: ( + value: RecordType, + key: string, + map: Map, + ) => void, + // this is an unbound method, so the this value is unknown. + // Also the Map type this is based on uses any for this parameter as well. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thisArg?: any, + ): void { + this.map.forEach((_, key) => { + const deRefValue = this.get(key); + if (deRefValue === undefined) { + return; + } + if (thisArg) { + callback.call(thisArg, deRefValue, key, this); + } else { + callback(deRefValue, key, this); + } + }); + } +} diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index ceab30f28188..81a7cd8c0093 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -161,6 +161,7 @@ const jsonData = JSON.stringify({ showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: true, + smartTransactionsOptInStatus: false, useNativeCurrencyAsPrimaryCurrency: true, }, ipfsGateway: 'dweb.link', @@ -168,7 +169,7 @@ const jsonData = JSON.stringify({ theme: 'light', customNetworkListEnabled: false, textDirection: 'auto', - useRequestQueue: false, + useRequestQueue: true, }, internalAccounts: { accounts: { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js index ed3ebeae6f6a..e5175326edea 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -1,4 +1,4 @@ -import { detectSIWE } from '@metamask/controller-utils'; +import { ApprovalType, detectSIWE } from '@metamask/controller-utils'; import { errorCodes } from 'eth-rpc-errors'; import { isValidAddress } from 'ethereumjs-util'; import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; @@ -7,7 +7,6 @@ import { MetaMetricsEventName, MetaMetricsEventUiCustomization, } from '../../../shared/constants/metametrics'; -import { SECOND } from '../../../shared/constants/time'; import { BlockaidResultType, @@ -18,25 +17,25 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(blockaid) import { SIGNING_METHODS } from '../../../shared/constants/transaction'; - import { getBlockaidMetricsProps } from '../../../ui/helpers/utils/metrics'; ///: END:ONLY_INCLUDE_IF +import { REDESIGN_APPROVAL_TYPES } from '../../../ui/pages/confirmations/utils/confirm'; import { getSnapAndHardwareInfoForMetrics } from './snap-keyring/metrics'; /** * These types determine how the method tracking middleware handles incoming - * requests based on the method name. There are three options right now but - * the types could be expanded to cover other options in the future. + * requests based on the method name. */ const RATE_LIMIT_TYPES = { - RATE_LIMITED: 'rate_limited', + TIMEOUT: 'timeout', BLOCKED: 'blocked', NON_RATE_LIMITED: 'non_rate_limited', + RANDOM_SAMPLE: 'random_sample', }; /** * This object maps a method name to a RATE_LIMIT_TYPE. If not in this map the - * default is 'RATE_LIMITED' + * default is RANDOM_SAMPLE */ const RATE_LIMIT_MAP = { [MESSAGE_TYPE.ETH_SIGN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, @@ -47,12 +46,25 @@ const RATE_LIMIT_MAP = { [MESSAGE_TYPE.ETH_DECRYPT]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, - [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: RATE_LIMIT_TYPES.RATE_LIMITED, - [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: RATE_LIMIT_TYPES.RATE_LIMITED, + [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: RATE_LIMIT_TYPES.TIMEOUT, + [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: RATE_LIMIT_TYPES.TIMEOUT, [MESSAGE_TYPE.SEND_METADATA]: RATE_LIMIT_TYPES.BLOCKED, + [MESSAGE_TYPE.ETH_CHAIN_ID]: RATE_LIMIT_TYPES.BLOCKED, + [MESSAGE_TYPE.ETH_ACCOUNTS]: RATE_LIMIT_TYPES.BLOCKED, + [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE]: RATE_LIMIT_TYPES.BLOCKED, [MESSAGE_TYPE.GET_PROVIDER_STATE]: RATE_LIMIT_TYPES.BLOCKED, }; +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, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: ApprovalType.EthSignTypedData, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: ApprovalType.EthSignTypedData, +}; + /** * For events with user interaction (approve / reject | cancel) this map will * return an object with APPROVED, REJECTED, REQUESTED, and FAILED keys that map to the @@ -107,7 +119,8 @@ const EVENT_NAME_MAP = { }, }; -const rateLimitTimeouts = {}; +const rateLimitTimeoutsByMethod = {}; +let globalRateLimitCount = 0; ///: BEGIN:ONLY_INCLUDE_IF(blockaid) /** @@ -120,12 +133,19 @@ const rateLimitTimeouts = {}; * MetaMetricsController * @param {Function} opts.getMetricsState - get the state of * MetaMetricsController - * @param {number} [opts.rateLimitSeconds] - number of seconds to wait before - * allowing another set of events to be tracked. + * @param {number} [opts.rateLimitTimeout] - time, in milliseconds, to wait before + * allowing another set of events to be tracked for methods rate limited by timeout. + * @param {number} [opts.rateLimitSamplePercent] - percentage, in decimal, of events + * that should be tracked for methods rate limited by random sample. * @param {Function} opts.getAccountType * @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 + * tracked within the globalRateLimitTimeout time window. * @returns {Function} */ ///: END:ONLY_INCLUDE_IF @@ -133,9 +153,13 @@ const rateLimitTimeouts = {}; export default function createRPCMethodTrackingMiddleware({ trackEvent, getMetricsState, - rateLimitSeconds = 60 * 5, + rateLimitTimeout = 60 * 5 * 1000, // 5 minutes + rateLimitSamplePercent = 0.001, // 0.1% + globalRateLimitTimeout = 60 * 5 * 1000, // 5 minutes + globalRateLimitMaxAmount = 10, // max of events in the globalRateLimitTimeout window. pass 0 for no global rate limit getAccountType, getDeviceModel, + isConfirmationRedesignEnabled, snapAndHardwareMessenger, ///: BEGIN:ONLY_INCLUDE_IF(blockaid) appStateController, @@ -148,14 +172,30 @@ export default function createRPCMethodTrackingMiddleware({ ) { const { origin, method } = req; - // Determine what type of rate limit to apply based on method const rateLimitType = - RATE_LIMIT_MAP[method] ?? RATE_LIMIT_TYPES.RATE_LIMITED; + RATE_LIMIT_MAP[method] ?? RATE_LIMIT_TYPES.RANDOM_SAMPLE; + + let isRateLimited; + switch (rateLimitType) { + case RATE_LIMIT_TYPES.TIMEOUT: + isRateLimited = + typeof rateLimitTimeoutsByMethod[method] !== 'undefined'; + break; + case RATE_LIMIT_TYPES.NON_RATE_LIMITED: + isRateLimited = false; + break; + case RATE_LIMIT_TYPES.BLOCKED: + isRateLimited = true; + break; + default: + case RATE_LIMIT_TYPES.RANDOM_SAMPLE: + isRateLimited = Math.random() >= rateLimitSamplePercent; + break; + } - // If the rateLimitType is RATE_LIMITED check the rateLimitTimeouts - const rateLimited = - rateLimitType === RATE_LIMIT_TYPES.RATE_LIMITED && - typeof rateLimitTimeouts[method] !== 'undefined'; + const isGlobalRateLimited = + globalRateLimitMaxAmount > 0 && + globalRateLimitCount >= globalRateLimitMaxAmount; // Get the participateInMetaMetrics state to determine if we should track // anything. This is extra redundancy because this value is checked in @@ -173,10 +213,10 @@ export default function createRPCMethodTrackingMiddleware({ const shouldTrackEvent = // Don't track if the request came from our own UI or background origin !== ORIGIN_METAMASK && - // Don't track if this is a blocked method - rateLimitType !== RATE_LIMIT_TYPES.BLOCKED && // Don't track if the rate limit has been hit - rateLimited === false && + !isRateLimited && + // Don't track if the global rate limit has been hit + !isGlobalRateLimited && // Don't track if the user isn't participating in metametrics userParticipatingInMetaMetrics === true; @@ -223,6 +263,18 @@ export default function createRPCMethodTrackingMiddleware({ req.securityAlertResponse.description; } ///: END:ONLY_INCLUDE_IF + const isConfirmationRedesign = + isConfirmationRedesignEnabled() && + REDESIGN_APPROVAL_TYPES.find( + (type) => type === MESSAGE_TYPE_TO_APPROVAL_TYPE[method], + ); + + if (isConfirmationRedesign) { + eventProperties.ui_customizations = [ + ...(eventProperties.ui_customizations || []), + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ]; + } const snapAndHardwareInfo = await getSnapAndHardwareInfoForMetrics( getAccountType, @@ -237,9 +289,10 @@ export default function createRPCMethodTrackingMiddleware({ if (method === MESSAGE_TYPE.PERSONAL_SIGN) { const { isSIWEMessage } = detectSIWE({ data }); if (isSIWEMessage) { - eventProperties.ui_customizations = ( - eventProperties.ui_customizations || [] - ).concat(MetaMetricsEventUiCustomization.Siwe); + eventProperties.ui_customizations = [ + ...(eventProperties.ui_customizations || []), + MetaMetricsEventUiCustomization.Siwe, + ]; } } } catch (e) { @@ -258,9 +311,16 @@ export default function createRPCMethodTrackingMiddleware({ properties: eventProperties, }); - rateLimitTimeouts[method] = setTimeout(() => { - delete rateLimitTimeouts[method]; - }, SECOND * rateLimitSeconds); + if (rateLimitType === RATE_LIMIT_TYPES.TIMEOUT) { + rateLimitTimeoutsByMethod[method] = setTimeout(() => { + delete rateLimitTimeoutsByMethod[method]; + }, rateLimitTimeout); + } + + globalRateLimitCount += 1; + setTimeout(() => { + globalRateLimitCount -= 1; + }, globalRateLimitTimeout); } next(async (callback) => { diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js index 68dec85ccb12..347ef963b1b6 100644 --- a/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js @@ -2,6 +2,7 @@ import { errorCodes } from 'eth-rpc-errors'; import { detectSIWE } from '@metamask/controller-utils'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { + MetaMetricsEventCategory, MetaMetricsEventName, MetaMetricsEventUiCustomization, } from '../../../shared/constants/metametrics'; @@ -34,12 +35,18 @@ const appStateController = { }, }; -const handler = createRPCMethodTrackingMiddleware({ - trackEvent, - getMetricsState, - rateLimitSeconds: 1, - appStateController, -}); +const createHandler = (opts) => + createRPCMethodTrackingMiddleware({ + trackEvent, + getMetricsState, + rateLimitTimeout: 1000, + rateLimitSamplePercent: 0.1, + globalRateLimitTimeout: 0, + globalRateLimitMaxAmount: 0, + appStateController, + isConfirmationRedesignEnabled: () => false, + ...opts, + }); function getNext(timeout = 500) { let deferred; @@ -98,6 +105,7 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { executeMiddlewareStack, next } = getNext(); + const handler = createHandler(); handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).not.toHaveBeenCalled(); @@ -119,6 +127,7 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { executeMiddlewareStack, next } = getNext(); + const handler = createHandler(); handler(req, res, next); await executeMiddlewareStack(); expect(trackEvent).not.toHaveBeenCalled(); @@ -145,10 +154,11 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next } = getNext(); + const handler = createHandler(); await handler(req, res, next); expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -178,6 +188,7 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next } = getNext(); + const handler = createHandler(); await handler(req, res, next); expect(trackEvent).toHaveBeenCalledTimes(1); /** @@ -188,7 +199,7 @@ describe('createRPCMethodTrackingMiddleware', () => { * */ expect(trackEvent.mock.calls[0][0]).toStrictEqual({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -211,11 +222,12 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; 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: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureApproved, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4, @@ -234,11 +246,12 @@ describe('createRPCMethodTrackingMiddleware', () => { error: { code: errorCodes.provider.userRejectedRequest }, }; 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: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRejected, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, @@ -255,11 +268,12 @@ describe('createRPCMethodTrackingMiddleware', () => { const res = {}; 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: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.PermissionsApproved, properties: { method: MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS }, referrer: { url: 'some.dapp' }, @@ -276,36 +290,189 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler(); handler(req, res, next); expect(trackEvent).not.toHaveBeenCalled(); executeMiddlewareStack(); }); - it(`should only track events when not rate limited`, async () => { + describe('events rated limited by timeout', () => { + it.each([ + ['wallet_requestPermissions', 2], + ['eth_requestAccounts', 2], + ])( + `should only track '%s' events while the timeout rate limit is not active`, + async (method, eventsTrackedPerRequest) => { + const req = { + method, + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + + const handler = createHandler(); + + let callCount = 0; + while (callCount < 3) { + callCount += 1; + const { next, executeMiddlewareStack } = getNext(); + handler(req, res, next); + await executeMiddlewareStack(); + if (callCount !== 3) { + await waitForSeconds(0.6); + } + } + + const expectedNumberOfCalls = 2 * eventsTrackedPerRequest; + expect(trackEvent).toHaveBeenCalledTimes(expectedNumberOfCalls); + trackEvent.mock.calls.forEach((call) => { + expect(call[0].properties.method).toBe(method); + }); + }, + ); + }); + + describe('events rated limited by random', () => { + beforeEach(() => { + jest + .spyOn(Math, 'random') + .mockReturnValueOnce(0) // not rate limited + .mockReturnValueOnce(0.09) // not rate limited + .mockReturnValueOnce(0.1) // rate limited + .mockReturnValueOnce(0.11) // rate limited + .mockReturnValueOnce(1); // rate limited + }); + afterEach(() => { + jest.spyOn(Math, 'random').mockRestore(); + }); + it.each([ + ['any_method_without_rate_limit_type_set', 1], + ['eth_getBalance', 1], + ])( + `should only track a random percentage of '%s' events`, + async (method, eventsTrackedPerRequest) => { + const req = { + method, + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + + const handler = createHandler(); + + let callCount = 0; + while (callCount < 5) { + callCount += 1; + const { next, executeMiddlewareStack } = getNext(); + handler(req, res, next); + await executeMiddlewareStack(); + } + + const expectedNumberOfCalls = 2 * eventsTrackedPerRequest; + expect(trackEvent).toHaveBeenCalledTimes(expectedNumberOfCalls); + trackEvent.mock.calls.forEach((call) => { + expect(call[0].properties.method).toBe(method); + }); + }, + ); + }); + + describe('events rated globally rate limited', () => { + it('should only track events if the global rate limit has not been hit', async () => { + const req = { + method: 'some_method_rate_limited_by_sample', + origin: 'some.dapp', + }; + + const res = { + error: null, + }; + + const handler = createHandler({ + rateLimitSamplePercent: 1, // track every event for this spec + globalRateLimitTimeout: 1000, + globalRateLimitMaxAmount: 3, + }); + + let callCount = 0; + while (callCount < 4) { + callCount += 1; + const { next, executeMiddlewareStack } = getNext(); + handler(req, res, next); + await executeMiddlewareStack(); + if (callCount !== 4) { + await waitForSeconds(0.6); + } + } + + expect(trackEvent).toHaveBeenCalledTimes(3); + trackEvent.mock.calls.forEach((call) => { + expect(call[0].properties.method).toBe( + 'some_method_rate_limited_by_sample', + ); + }); + }); + }); + + it('should track Confirmation Redesign through ui_customizations prop if enabled', async () => { const req = { - method: 'eth_chainId', + method: MESSAGE_TYPE.PERSONAL_SIGN, origin: 'some.dapp', }; - const res = { error: null, }; + const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler({ + isConfirmationRedesignEnabled: () => true, + }); - let callCount = 0; + await handler(req, res, next); + await executeMiddlewareStack(); - while (callCount < 3) { - callCount += 1; - const { next, executeMiddlewareStack } = getNext(); - handler(req, res, next); - await executeMiddlewareStack(); - if (callCount !== 3) { - await waitForSeconds(0.6); - } - } + expect(trackEvent).toHaveBeenCalledTimes(2); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: MetaMetricsEventCategory.InpageProvider, + event: MetaMetricsEventName.SignatureApproved, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + ui_customizations: [ + MetaMetricsEventUiCustomization.RedesignedConfirmation, + ], + }, + referrer: { url: 'some.dapp' }, + }); + }); + + it('should not track Confirmation Redesign through ui_customizations prop if not enabled', async () => { + const req = { + method: MESSAGE_TYPE.PERSONAL_SIGN, + origin: 'some.dapp', + }; + const res = { + error: null, + }; + const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler(); + + await handler(req, res, next); + await executeMiddlewareStack(); expect(trackEvent).toHaveBeenCalledTimes(2); - expect(trackEvent.mock.calls[0][0].properties.method).toBe('eth_chainId'); - expect(trackEvent.mock.calls[1][0].properties.method).toBe('eth_chainId'); + + expect(trackEvent.mock.calls[1][0]).toMatchObject({ + category: MetaMetricsEventCategory.InpageProvider, + event: MetaMetricsEventName.SignatureApproved, + properties: { + signature_type: MESSAGE_TYPE.PERSONAL_SIGN, + }, + referrer: { url: 'some.dapp' }, + }); }); it('should track Sign-in With Ethereum (SIWE) message if detected', async () => { @@ -317,6 +484,7 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler(); detectSIWE.mockImplementation(() => { return { isSIWEMessage: true }; @@ -328,7 +496,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureApproved, properties: { signature_type: MESSAGE_TYPE.PERSONAL_SIGN, @@ -349,6 +517,7 @@ describe('createRPCMethodTrackingMiddleware', () => { error: mockError, }; const { next, executeMiddlewareStack } = getNext(); + const handler = createHandler(); await handler(req, res, next); await executeMiddlewareStack(); @@ -356,7 +525,7 @@ describe('createRPCMethodTrackingMiddleware', () => { expect(trackEvent).toHaveBeenCalledTimes(2); expect(trackEvent.mock.calls[1][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureFailed, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, @@ -377,12 +546,13 @@ describe('createRPCMethodTrackingMiddleware', () => { error: null, }; const { next } = getNext(); + const handler = createHandler(); await handler(req, res, next); expect(trackEvent).toHaveBeenCalledTimes(1); expect(trackEvent.mock.calls[0][0]).toMatchObject({ - category: 'inpage_provider', + category: MetaMetricsEventCategory.InpageProvider, event: MetaMetricsEventName.SignatureRequested, properties: { signature_type: MESSAGE_TYPE.ETH_SIGN, diff --git a/app/scripts/lib/encryptor-factory.test.ts b/app/scripts/lib/encryptor-factory.test.ts new file mode 100644 index 000000000000..5e8b10feb152 --- /dev/null +++ b/app/scripts/lib/encryptor-factory.test.ts @@ -0,0 +1,257 @@ +import * as browserPassworder from '@metamask/browser-passworder'; +import { encryptorFactory } from './encryptor-factory'; + +jest.mock('@metamask/browser-passworder'); + +const mockIterations = 100; +const mockPassword = 'password'; +const mockSalt = 'salt'; +const mockData = 'data'; + +describe('encryptorFactory', () => { + afterEach(async () => { + jest.resetAllMocks(); + }); + + const mockBrowserPassworder = browserPassworder as jest.Mocked< + typeof browserPassworder + >; + + it('should return an object with browser passworder methods', () => { + const encryptor = encryptorFactory(mockIterations); + + [ + 'encrypt', + 'encryptWithDetail', + 'encryptWithKey', + 'decrypt', + 'decryptWithDetail', + 'decryptWithKey', + 'keyFromPassword', + 'importKey', + 'exportKey', + 'generateSalt', + 'isVaultUpdated', + ].forEach((method) => { + expect(encryptor).toHaveProperty(method); + }); + }); + + describe('encrypt', () => { + it('should call browser-passworder.encrypt with the given password, data, and iterations', async () => { + const encryptor = encryptorFactory(mockIterations); + + await encryptor.encrypt(mockPassword, mockData); + + expect(mockBrowserPassworder.encrypt).toHaveBeenCalledWith( + mockPassword, + mockData, + undefined, + undefined, + { + algorithm: 'PBKDF2', + params: { + iterations: mockIterations, + }, + }, + ); + }); + + it('should return the result of browser-passworder.encrypt', async () => { + const encryptor = encryptorFactory(mockIterations); + const mockResult = 'result'; + mockBrowserPassworder.encrypt.mockResolvedValue(mockResult); + + expect(await encryptor.encrypt(mockPassword, mockData)).toBe(mockResult); + }); + }); + + describe('encryptWithDetail', () => { + it('should call browser-passworder.encryptWithDetail with the given password, object, and iterations', async () => { + const encryptor = encryptorFactory(mockIterations); + + await encryptor.encryptWithDetail(mockPassword, { foo: 'bar' }, 'salt'); + + expect(mockBrowserPassworder.encryptWithDetail).toHaveBeenCalledWith( + mockPassword, + { foo: 'bar' }, + 'salt', + { + algorithm: 'PBKDF2', + params: { + iterations: mockIterations, + }, + }, + ); + }); + + it('should return the result of browser-passworder.encryptWithDetail', async () => { + const encryptor = encryptorFactory(mockIterations); + const mockResult = { + vault: 'vault', + exportedKeyString: 'salt', + }; + mockBrowserPassworder.encryptWithDetail.mockResolvedValue(mockResult); + + expect( + await encryptor.encryptWithDetail(mockPassword, { foo: 'bar' }, 'salt'), + ).toBe(mockResult); + }); + }); + + describe('decrypt', () => { + it('should call browser-passworder.decrypt with the given password, data, and iterations', async () => { + const encryptor = encryptorFactory(mockIterations); + + await encryptor.decrypt(mockPassword, mockData); + + expect(mockBrowserPassworder.decrypt).toHaveBeenCalledWith( + mockPassword, + mockData, + ); + }); + + it('should return the result of browser-passworder.decrypt', async () => { + const encryptor = encryptorFactory(mockIterations); + const mockResult = 'result'; + mockBrowserPassworder.decrypt.mockResolvedValue(mockResult); + + expect(await encryptor.decrypt(mockPassword, mockData)).toBe(mockResult); + }); + }); + + describe('decryptWithDetail', () => { + it('should call browser-passworder.decryptWithDetail with the given password and object', async () => { + const encryptor = encryptorFactory(mockIterations); + + await encryptor.decryptWithDetail(mockPassword, mockData); + + expect(mockBrowserPassworder.decryptWithDetail).toHaveBeenCalledWith( + mockPassword, + mockData, + ); + }); + + it('should return the result of browser-passworder.decryptWithDetail', async () => { + const encryptor = encryptorFactory(mockIterations); + const mockResult = { + exportedKeyString: 'key', + vault: 'data', + salt: 'salt', + }; + mockBrowserPassworder.decryptWithDetail.mockResolvedValue(mockResult); + + expect(await encryptor.decryptWithDetail(mockPassword, mockData)).toBe( + mockResult, + ); + }); + }); + + describe('keyFromPassword', () => { + it('should call browser-passworder.keyFromPassword with the given parameters', async () => { + const encryptor = encryptorFactory(mockIterations); + + const keyDerivationOpts = { + algorithm: 'PBKDF2' as const, + params: { + iterations: 1, + }, + }; + + const mockResult = { + key: 'key', + derivationOptions: keyDerivationOpts, + }; + + // @ts-expect-error The key type is a mock type and not valid. + mockBrowserPassworder.keyFromPassword.mockResolvedValue(mockResult); + + expect( + await encryptor.keyFromPassword( + mockPassword, + mockSalt, + true, + keyDerivationOpts, + ), + ).toBe(mockResult); + + expect(mockBrowserPassworder.keyFromPassword).toHaveBeenCalledWith( + mockPassword, + mockSalt, + true, + { + algorithm: 'PBKDF2', + params: { + iterations: 1, + }, + }, + ); + }); + + it('should call browser-passworder.keyFromPassword with overriden opts', async () => { + const encryptor = encryptorFactory(mockIterations); + + const mockResult = { + key: 'key', + derivationOptions: { + algorithm: 'PBKDF2', + params: { + iterations: mockIterations, + }, + }, + }; + + // @ts-expect-error The key type is a mock type and not valid. + mockBrowserPassworder.keyFromPassword.mockResolvedValue(mockResult); + + expect( + await encryptor.keyFromPassword( + mockPassword, + mockSalt, + true, + undefined, + ), + ).toBe(mockResult); + + expect(mockBrowserPassworder.keyFromPassword).toHaveBeenCalledWith( + mockPassword, + mockSalt, + true, + { + algorithm: 'PBKDF2', + params: { + iterations: mockIterations, + }, + }, + ); + }); + }); + + describe('isVaultUpdated', () => { + it('should call browser-passworder.isVaultUpdated with the given vault and iterations', () => { + const encryptor = encryptorFactory(mockIterations); + const mockVault = 'vault'; + + encryptor.isVaultUpdated(mockVault); + + expect(mockBrowserPassworder.isVaultUpdated).toHaveBeenCalledWith( + mockVault, + { + algorithm: 'PBKDF2', + params: { + iterations: mockIterations, + }, + }, + ); + }); + + it('should return the result of browser-passworder.isVaultUpdated', () => { + const encryptor = encryptorFactory(mockIterations); + const mockResult = false; + const mockVault = 'vault'; + mockBrowserPassworder.isVaultUpdated.mockReturnValue(mockResult); + + expect(encryptor.isVaultUpdated(mockVault)).toBe(mockResult); + }); + }); +}); diff --git a/app/scripts/lib/encryptor-factory.ts b/app/scripts/lib/encryptor-factory.ts index 68f8ee42307e..9f8ba0a303fb 100644 --- a/app/scripts/lib/encryptor-factory.ts +++ b/app/scripts/lib/encryptor-factory.ts @@ -8,7 +8,10 @@ import { isVaultUpdated, keyFromPassword, importKey, + exportKey, + generateSalt, EncryptionKey, + KeyDerivationOptions, } from '@metamask/browser-passworder'; /** @@ -50,6 +53,36 @@ const encryptWithDetailFactory = }, }); +/** + * A factory function for the keyFromPassword method of the browser-passworder library, + * that generates a key from a password and a salt. + * + * This factory function overrides the default key derivation options with the specified + * number of iterations, unless existing key derivation options are passed in. + * + * @param iterations - The number of iterations to use for the PBKDF2 algorithm. + * @returns A function that generates a key with a potentially overriden number of iterations. + */ +const keyFromPasswordFactory = + (iterations: number) => + async ( + password: string, + salt: string, + exportable?: boolean, + opts?: KeyDerivationOptions, + ) => + keyFromPassword( + password, + salt, + exportable, + opts ?? { + algorithm: 'PBKDF2', + params: { + iterations, + }, + }, + ); + /** * A factory function for the isVaultUpdated method of the browser-passworder library, * that checks if the given vault was encrypted with the given number of iterations. @@ -57,7 +90,7 @@ const encryptWithDetailFactory = * @param iterations - The number of iterations to use for the PBKDF2 algorithm. * @returns A function that checks if the vault was encrypted with the given number of iterations. */ -const isVaultUpdatedFactory = (iterations: number) => async (vault: string) => +const isVaultUpdatedFactory = (iterations: number) => (vault: string) => isVaultUpdated(vault, { algorithm: 'PBKDF2', params: { @@ -81,7 +114,9 @@ export const encryptorFactory = (iterations: number) => ({ decrypt, decryptWithKey, decryptWithDetail, - keyFromPassword, + keyFromPassword: keyFromPasswordFactory(iterations), isVaultUpdated: isVaultUpdatedFactory(iterations), importKey, + exportKey, + generateSalt, }); diff --git a/app/scripts/lib/keyring-snaps-permissions.test.ts b/app/scripts/lib/keyring-snaps-permissions.test.ts index afe4b40fefa6..3e457df2e258 100644 --- a/app/scripts/lib/keyring-snaps-permissions.test.ts +++ b/app/scripts/lib/keyring-snaps-permissions.test.ts @@ -15,6 +15,8 @@ describe('keyringSnapPermissionsBuilder', () => { registerActionHandler: jest.fn(), registerInitialEventPayload: jest.fn(), publish: jest.fn(), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, state: {}, }); @@ -85,6 +87,8 @@ describe('keyringSnapPermissionsBuilder', () => { ])('"%s" cannot call any methods', (origin) => { const permissions = keyringSnapPermissionsBuilder( mockController, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any origin as any, ); expect(permissions()).toStrictEqual([]); @@ -106,6 +110,8 @@ describe('isProtocolAllowed', () => { [1, false], [0, false], [-1, false], + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any ])('"%s" cannot call any methods', (origin: any, expected: boolean) => { expect(isProtocolAllowed(origin)).toBe(expected); }); diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 225d2f3c5ef5..4fe538188ba3 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -1,8 +1,9 @@ import EventEmitter from '@metamask/safe-event-emitter'; import ExtensionPlatform from '../platforms/extension'; - -const NOTIFICATION_HEIGHT = 620; -const NOTIFICATION_WIDTH = 360; +import { + NOTIFICATION_HEIGHT, + NOTIFICATION_WIDTH, +} from '../../../shared/constants/notifications'; export const NOTIFICATION_MANAGER_EVENTS = { POPUP_CLOSED: 'onPopupClosed', diff --git a/app/scripts/lib/offscreen-bridge/lattice-offscreen-keyring.ts b/app/scripts/lib/offscreen-bridge/lattice-offscreen-keyring.ts index c690c1004382..27993e04d03c 100644 --- a/app/scripts/lib/offscreen-bridge/lattice-offscreen-keyring.ts +++ b/app/scripts/lib/offscreen-bridge/lattice-offscreen-keyring.ts @@ -61,6 +61,8 @@ class LatticeKeyringOffscreen extends LatticeKeyring { }); return creds; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { throw new Error(err); } diff --git a/app/scripts/lib/ppom/indexed-db-backend.ts b/app/scripts/lib/ppom/indexed-db-backend.ts index 41c5af2629d1..8fbf63c1aced 100644 --- a/app/scripts/lib/ppom/indexed-db-backend.ts +++ b/app/scripts/lib/ppom/indexed-db-backend.ts @@ -38,6 +38,8 @@ export class IndexedDBPPOMStorage implements StorageBackend { reject( new Error( `Failed to open database ${this.storeName}: ${ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (event.target as any)?.error }`, ), @@ -65,8 +67,12 @@ export class IndexedDBPPOMStorage implements StorageBackend { private async objectStoreAction( method: 'get' | 'delete' | 'put' | 'getAllKeys', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any args?: any, mode: IDBTransactionMode = 'readonly', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { return new Promise((resolve, reject) => { this.#getObjectStore(mode) @@ -81,6 +87,8 @@ export class IndexedDBPPOMStorage implements StorageBackend { reject( new Error( `Error in indexDB operation ${method}: ${ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (event.target as any)?.error }`, ), @@ -95,6 +103,8 @@ export class IndexedDBPPOMStorage implements StorageBackend { async read(key: StorageKey, checksum: string): Promise { const event = await this.objectStoreAction('get', [key.name, key.chainId]); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = (event.target as any)?.result?.data; await validateChecksum(key, data, checksum); return data; @@ -119,6 +129,8 @@ export class IndexedDBPPOMStorage implements StorageBackend { async dir(): Promise { const event = await this.objectStoreAction('getAllKeys'); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (event.target as any)?.result.map(([name, chainId]: string[]) => ({ name, chainId, diff --git a/app/scripts/lib/ppom/ppom-middleware.test.ts b/app/scripts/lib/ppom/ppom-middleware.test.ts index de3cff83f93a..370acc51e944 100644 --- a/app/scripts/lib/ppom/ppom-middleware.test.ts +++ b/app/scripts/lib/ppom/ppom-middleware.test.ts @@ -1,4 +1,11 @@ +import { + type Hex, + JsonRpcRequestStruct, + JsonRpcResponseStruct, +} from '@metamask/utils'; +import { waitFor } from '@testing-library/react'; import { CHAIN_IDS } from '../../../../shared/constants/network'; + import { BlockaidReason, BlockaidResultType, @@ -19,10 +26,24 @@ Object.defineProperty(globalThis, 'performance', { }); const createMiddleWare = ( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any usePPOM?: any, - securityAlertsEnabled?: boolean, - chainId?: string, + options: { + securityAlertsEnabled?: boolean; + chainId?: Hex; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockUpdateSecurityAlertResponseByTxId?: any; + } = { + mockUpdateSecurityAlertResponseByTxId: () => undefined, + }, ) => { + const { + securityAlertsEnabled, + chainId, + mockUpdateSecurityAlertResponseByTxId, + } = options; const usePPOMMock = jest.fn(); const ppomController = { usePPOM: usePPOM || usePPOMMock, @@ -43,11 +64,19 @@ const createMiddleWare = ( }; return createPPOMMiddleware( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any ppomController as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any preferenceController as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any networkController as any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any appStateController as any, - () => undefined, + mockUpdateSecurityAlertResponseByTxId, ); }; @@ -64,28 +93,87 @@ describe('PPOMMiddleware', () => { const usePPOMMock = jest.fn(); const middlewareFunction = createMiddleWare(usePPOMMock); await middlewareFunction( - { method: 'eth_sendTransaction' }, - undefined, + { ...JsonRpcRequestStruct, method: 'eth_sendTransaction' }, + { ...JsonRpcResponseStruct }, () => undefined, ); expect(usePPOMMock).toHaveBeenCalledTimes(1); }); - it('should add validation response on confirmation requests', async () => { + it('adds loading response to confirmation requests while validation is in progress', async () => { const usePPOM = async () => Promise.resolve('VALIDATION_RESULT'); const middlewareFunction = createMiddleWare(usePPOM); const req = { + ...JsonRpcRequestStruct, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; - await middlewareFunction(req, undefined, () => undefined); - expect(req.securityAlertResponse).toBeDefined(); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct }, + () => undefined, + ); + + expect(req.securityAlertResponse.reason).toBe(BlockaidResultType.Loading); + expect(req.securityAlertResponse.result_type).toBe( + BlockaidReason.inProgress, + ); + }); + + it('adds validation response to confirmation requests on supported networks', async () => { + const validateMock = jest.fn().mockImplementation(() => + Promise.resolve({ + result_type: BlockaidResultType.Malicious, + reason: BlockaidReason.permitFarming, + }), + ); + + const ppom = { + validateJsonRpc: validateMock, + }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usePPOM = async (callback: any) => { + callback(ppom); + }; + const mockUpdateSecurityAlertResponseByTxId = jest.fn(); + const middlewareFunction = createMiddleWare(usePPOM, { + chainId: '0xa', + mockUpdateSecurityAlertResponseByTxId, + }); + const req = { + ...JsonRpcRequestStruct, + method: 'eth_sendTransaction', + securityAlertResponse: undefined, + }; + await middlewareFunction( + req, + { ...JsonRpcResponseStruct }, + () => undefined, + ); + + await waitFor(() => { + const mockCallSecurityAlertResponse = + mockUpdateSecurityAlertResponseByTxId.mock.calls[0][1]; + + expect(mockCallSecurityAlertResponse.result_type).toBe( + BlockaidResultType.Malicious, + ); + expect(mockCallSecurityAlertResponse.reason).toBe( + BlockaidReason.permitFarming, + ); + expect(mockCallSecurityAlertResponse.securityAlertId).toBeDefined(); + expect(req.securityAlertResponse).toBeDefined(); + }); }); - it('should not do validation if user has not enabled preference', async () => { + it('does not do validation if the user has not enabled the preference', async () => { const usePPOM = async () => Promise.resolve('VALIDATION_RESULT'); - const middlewareFunction = createMiddleWare(usePPOM, false); + const middlewareFunction = createMiddleWare(usePPOM, { + securityAlertsEnabled: false, + }); const req = { + ...JsonRpcRequestStruct, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; @@ -93,94 +181,124 @@ describe('PPOMMiddleware', () => { expect(req.securityAlertResponse).toBeUndefined(); }); - it('should not do validation if user is not on mainnet', async () => { + it('does not do validation if user is not on a supported network', async () => { const usePPOM = async () => Promise.resolve('VALIDATION_RESULT'); - const middlewareFunction = createMiddleWare(usePPOM, false, '0x2'); + const middlewareFunction = createMiddleWare(usePPOM, { + chainId: '0x2', + }); const req = { + ...JsonRpcRequestStruct, method: 'eth_sendTransaction', securityAlertResponse: undefined, }; - await middlewareFunction(req, undefined, () => undefined); + await middlewareFunction( + req, + { ...JsonRpcResponseStruct }, + () => undefined, + ); expect(req.securityAlertResponse).toBeUndefined(); }); - it('should set error type in response if usePPOM throw error', async () => { + it('sets error types in the response if usePPOM throws an error', async () => { const usePPOM = async () => { throw new Error('some error'); }; - const middlewareFunction = createMiddleWare({ usePPOM }); + const mockUpdateSecurityAlertResponseByTxId = jest.fn(); + const middlewareFunction = createMiddleWare(usePPOM, { + mockUpdateSecurityAlertResponseByTxId, + }); const req = { + ...JsonRpcRequestStruct, method: 'eth_sendTransaction', - securityAlertResponse: undefined, }; - await middlewareFunction(req, undefined, () => undefined); - expect((req.securityAlertResponse as any)?.result_type).toBe( - BlockaidResultType.Errored, - ); - expect((req.securityAlertResponse as any)?.reason).toBe( - BlockaidReason.errored, + await middlewareFunction( + req, + { ...JsonRpcResponseStruct }, + () => undefined, ); + + await waitFor(() => { + const mockCallSecurityAlertResponse = + mockUpdateSecurityAlertResponseByTxId.mock.calls[0][1]; + expect(mockCallSecurityAlertResponse.description).toBe( + 'Error: some error', + ); + expect(mockCallSecurityAlertResponse.result_type).toBe( + BlockaidResultType.Errored, + ); + expect(mockCallSecurityAlertResponse.result_type).toBe( + BlockaidReason.errored, + ); + expect(mockCallSecurityAlertResponse.securityAlertId).toBeDefined(); + }); }); - it('should call next method when ppomController.usePPOM completes', async () => { + it('calls next method when ppomController.usePPOM completes', async () => { const ppom = { validateJsonRpc: () => undefined, }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const usePPOM = async (callback: any) => { callback(ppom); }; const middlewareFunction = createMiddleWare(usePPOM); const nextMock = jest.fn(); await middlewareFunction( - { method: 'eth_sendTransaction' }, - undefined, + { ...JsonRpcRequestStruct, method: 'eth_sendTransaction' }, + { ...JsonRpcResponseStruct }, nextMock, ); expect(nextMock).toHaveBeenCalledTimes(1); }); - it('should call next method when ppomController.usePPOM throws error', async () => { + it('calls next method when ppomController.usePPOM throws error', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const usePPOM = async (_callback: any) => { throw Error('Some error'); }; const middlewareFunction = createMiddleWare(usePPOM); const nextMock = jest.fn(); await middlewareFunction( - { method: 'eth_sendTransaction' }, - undefined, + { ...JsonRpcRequestStruct, method: 'eth_sendTransaction' }, + { ...JsonRpcResponseStruct }, nextMock, ); expect(nextMock).toHaveBeenCalledTimes(1); }); - - it('should call ppom.validateJsonRpc when invoked', async () => { + it('calls ppom.validateJsonRpc when invoked', async () => { const validateMock = jest.fn(); const ppom = { validateJsonRpc: validateMock, }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const usePPOM = async (callback: any) => { callback(ppom); }; const middlewareFunction = createMiddleWare(usePPOM); await middlewareFunction( - { method: 'eth_sendTransaction' }, - undefined, + { ...JsonRpcRequestStruct, method: 'eth_sendTransaction' }, + { ...JsonRpcResponseStruct }, () => undefined, ); expect(validateMock).toHaveBeenCalledTimes(1); }); - it('should not call ppom.validateJsonRpc when request is not for confirmation method', async () => { + it('does not call ppom.validateJsonRpc when request is not for confirmation method', async () => { const validateMock = jest.fn(); const ppom = { validateJsonRpc: validateMock, }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const usePPOM = async (callback: any) => { callback(ppom); }; const middlewareFunction = createMiddleWare(usePPOM); await middlewareFunction( - { method: 'eth_someRequest' }, + { ...JsonRpcRequestStruct, method: 'eth_someRequest' }, undefined, () => undefined, ); @@ -189,6 +307,7 @@ describe('PPOMMiddleware', () => { it('normalizes transaction requests before validation', async () => { const requestMock1 = { + ...JsonRpcRequestStruct, method: 'eth_sendTransaction', params: [{ data: '0x1' }], }; @@ -206,13 +325,19 @@ describe('PPOMMiddleware', () => { validateJsonRpc: validateMock, }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const usePPOM = async (callback: any) => { callback(ppom); }; const middlewareFunction = createMiddleWare(usePPOM); - await middlewareFunction(requestMock1, undefined, () => undefined); + await middlewareFunction( + requestMock1, + { ...JsonRpcResponseStruct }, + () => undefined, + ); expect(normalizePPOMRequestMock).toHaveBeenCalledTimes(1); expect(normalizePPOMRequestMock).toHaveBeenCalledWith(requestMock1); diff --git a/app/scripts/lib/ppom/ppom-middleware.ts b/app/scripts/lib/ppom/ppom-middleware.ts index c30c38fa8622..f7763bf7e4a8 100644 --- a/app/scripts/lib/ppom/ppom-middleware.ts +++ b/app/scripts/lib/ppom/ppom-middleware.ts @@ -1,6 +1,13 @@ import { PPOM } from '@blockaid/ppom_release'; import { PPOMController } from '@metamask/ppom-validator'; import { NetworkController } from '@metamask/network-controller'; +import { + Hex, + Json, + JsonRpcParams, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; import { v4 as uuid } from 'uuid'; import { @@ -13,7 +20,7 @@ import { PreferencesController } from '../../controllers/preferences'; import { SecurityAlertResponse } from '../transaction/util'; import { normalizePPOMRequest } from './ppom-util'; -const { sentry } = global as any; +const { sentry } = global; const CONFIRMATION_METHODS = Object.freeze([ 'eth_sendRawTransaction', @@ -21,13 +28,14 @@ const CONFIRMATION_METHODS = Object.freeze([ ...SIGNING_METHODS, ]); -export const SUPPORTED_CHAIN_IDS: string[] = [ +export const SUPPORTED_CHAIN_IDS: Hex[] = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.AVALANCHE, CHAIN_IDS.BASE, CHAIN_IDS.BSC, CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.MAINNET, + CHAIN_IDS.OPBNB, CHAIN_IDS.OPTIMISM, CHAIN_IDS.POLYGON, CHAIN_IDS.SEPOLIA, @@ -49,17 +57,30 @@ export const SUPPORTED_CHAIN_IDS: string[] = [ * @param updateSecurityAlertResponseByTxId * @returns PPOMMiddleware function. */ -export function createPPOMMiddleware( +export function createPPOMMiddleware< + Params extends JsonRpcParams, + Result extends Json, +>( ppomController: PPOMController, preferencesController: PreferencesController, networkController: NetworkController, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any appStateController: any, updateSecurityAlertResponseByTxId: ( - req: any, + req: JsonRpcRequest & { + securityAlertResponse: SecurityAlertResponse; + }, securityAlertResponse: SecurityAlertResponse, ) => void, ) { - return async (req: any, _res: any, next: () => void) => { + return async ( + req: JsonRpcRequest & { + securityAlertResponse: SecurityAlertResponse; + }, + _res: JsonRpcResponse, + next: () => void, + ) => { try { const securityAlertsEnabled = preferencesController.store.getState()?.securityAlertsEnabled; @@ -71,32 +92,59 @@ export function createPPOMMiddleware( ) { // eslint-disable-next-line require-atomic-updates const securityAlertId = uuid(); + let securityAlertResponse: SecurityAlertResponse = { + reason: BlockaidResultType.Loading, + result_type: BlockaidReason.inProgress, + securityAlertId, + }; ppomController .usePPOM(async (ppom: PPOM) => { try { const normalizedRequest = normalizePPOMRequest(req); - const securityAlertResponse = await ppom.validateJsonRpc( + securityAlertResponse = await ppom.validateJsonRpc( normalizedRequest, ); - securityAlertResponse.securityAlertId = securityAlertId; - return securityAlertResponse; - } catch (error: any) { + } catch (error: unknown) { sentry?.captureException(error); - const errorObject = error as unknown as Error; - console.error('Error validating JSON RPC using PPOM: ', error); - const securityAlertResponse = { + console.error( + 'Error validating JSON RPC using PPOM: ', + typeof error === 'object' || typeof error === 'string' + ? error + : JSON.stringify(error), + ); + + securityAlertResponse = { result_type: BlockaidResultType.Errored, reason: BlockaidReason.errored, - description: `${errorObject.name}: ${errorObject.message}`, + description: + error instanceof Error + ? `${error.name}: ${error.message}` + : JSON.stringify(error), }; - - return securityAlertResponse; } }) - .then((securityAlertResponse) => { + .catch((error: unknown) => { + sentry?.captureException(error); + console.error( + 'Error createPPOMMiddleware#usePPOM: ', + typeof error === 'object' || typeof error === 'string' + ? error + : JSON.stringify(error), + ); + + securityAlertResponse = { + result_type: BlockaidResultType.Errored, + reason: BlockaidReason.errored, + description: + error instanceof Error + ? `${error.name}: ${error.message}` + : JSON.stringify(error), + }; + }) + .finally(() => { updateSecurityAlertResponseByTxId(req, { ...securityAlertResponse, securityAlertId, @@ -104,32 +152,31 @@ export function createPPOMMiddleware( }); if (SIGNING_METHODS.includes(req.method)) { - req.securityAlertResponse = { - reason: BlockaidResultType.Loading, - result_type: BlockaidReason.inProgress, - securityAlertId, - }; appStateController.addSignatureSecurityAlertResponse({ reason: BlockaidResultType.Loading, result_type: BlockaidReason.inProgress, securityAlertId, }); - } else { - req.securityAlertResponse = { - reason: BlockaidResultType.Loading, - result_type: BlockaidReason.inProgress, - securityAlertId, - }; } + + req.securityAlertResponse = { ...securityAlertResponse }; } - } catch (error: any) { - const errorObject = error as unknown as Error; + } catch (error: unknown) { sentry?.captureException(error); - console.error('Error validating JSON RPC using PPOM: ', error); + console.error( + 'Error createPPOMMiddleware: ', + typeof error === 'object' || typeof error === 'string' + ? error + : JSON.stringify(error), + ); + req.securityAlertResponse = { result_type: BlockaidResultType.Errored, reason: BlockaidReason.errored, - description: `${errorObject.name}: ${errorObject.message}`, + description: + error instanceof Error + ? `${error.name}: ${error.message}` + : JSON.stringify(error), }; } finally { next(); diff --git a/app/scripts/lib/ppom/ppom-util.ts b/app/scripts/lib/ppom/ppom-util.ts index d0f9141996c0..95806d41bb4a 100644 --- a/app/scripts/lib/ppom/ppom-util.ts +++ b/app/scripts/lib/ppom/ppom-util.ts @@ -2,6 +2,8 @@ import { normalizeTransactionParams } from '@metamask/transaction-controller'; const METHOD_SEND_TRANSACTION = 'eth_sendTransaction'; +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function normalizePPOMRequest(request: any) { if (request.method !== METHOD_SEND_TRANSACTION) { return request; diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js index d0653fd6332a..db9cde9ed3e5 100644 --- a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.js @@ -1,7 +1,7 @@ import { permissionRpcMethods } from '@metamask/permission-controller'; import { selectHooks } from '@metamask/snaps-rpc-methods'; +import { hasProperty } from '@metamask/utils'; import { ethErrors } from 'eth-rpc-errors'; -import { flatten } from 'lodash'; import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network'; import localHandlers from './handlers'; @@ -9,15 +9,13 @@ const allHandlers = [...localHandlers, ...permissionRpcMethods.handlers]; const handlerMap = allHandlers.reduce((map, handler) => { for (const methodName of handler.methodNames) { - map.set(methodName, handler); + map[methodName] = handler; } return map; -}, new Map()); +}, {}); -const expectedHookNames = Array.from( - new Set( - flatten(allHandlers.map(({ hookNames }) => Object.keys(hookNames))), - ).values(), +const expectedHookNames = new Set( + allHandlers.flatMap(({ hookNames }) => Object.getOwnPropertyNames(hookNames)), ); /** @@ -26,20 +24,12 @@ const expectedHookNames = Array.from( * Handlers consume functions that hook into the background, and only depend * on their signatures, not e.g. controller internals. * - * @param {Record} hooks - Required "hooks" into our + * @param {Record unknown | Promise>} hooks - Required "hooks" into our * controllers. - * @returns {(req: object, res: object, next: Function, end: Function) => void} + * @returns {import('json-rpc-engine').JsonRpcMiddleware} The method middleware function. */ export function createMethodMiddleware(hooks) { - // Fail immediately if we forgot to provide any expected hooks. - const missingHookNames = expectedHookNames.filter( - (hookName) => !Object.hasOwnProperty.call(hooks, hookName), - ); - if (missingHookNames.length > 0) { - throw new Error( - `Missing expected hooks:\n\n${missingHookNames.join('\n')}\n`, - ); - } + assertExpectedHook(hooks); return async function methodMiddleware(req, res, next, end) { // Reject unsupported methods. @@ -47,7 +37,7 @@ export function createMethodMiddleware(hooks) { return end(ethErrors.rpc.methodNotSupported()); } - const handler = handlerMap.get(req.method); + const handler = handlerMap[req.method]; if (handler) { const { implementation, hookNames } = handler; try { @@ -63,10 +53,42 @@ export function createMethodMiddleware(hooks) { if (process.env.METAMASK_DEBUG) { console.error(error); } - return end(error); + return end( + error instanceof Error + ? error + : ethErrors.rpc.internal({ data: error }), + ); } } return next(); }; } + +/** + * Asserts that the hooks object only has all expected hooks and no extraneous ones. + * + * @param {Record} hooks - Required "hooks" into our controllers. + */ +function assertExpectedHook(hooks) { + const missingHookNames = []; + expectedHookNames.forEach((hookName) => { + if (!hasProperty(hooks, hookName)) { + missingHookNames.push(hookName); + } + }); + if (missingHookNames.length > 0) { + throw new Error( + `Missing expected hooks:\n\n${missingHookNames.join('\n')}\n`, + ); + } + + const extraneousHookNames = Object.getOwnPropertyNames(hooks).filter( + (hookName) => !expectedHookNames.has(hookName), + ); + if (extraneousHookNames.length > 0) { + throw new Error( + `Received unexpected hooks:\n\n${extraneousHookNames.join('\n')}\n`, + ); + } +} diff --git a/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js new file mode 100644 index 000000000000..cb26fc8061de --- /dev/null +++ b/app/scripts/lib/rpc-method-middleware/createMethodMiddleware.test.js @@ -0,0 +1,190 @@ +import { JsonRpcEngine } from 'json-rpc-engine'; +import { + assertIsJsonRpcFailure, + assertIsJsonRpcSuccess, +} from '@metamask/utils'; +import { createMethodMiddleware } from '.'; + +jest.mock('@metamask/permission-controller', () => ({ + permissionRpcMethods: { handlers: [] }, +})); + +jest.mock('./handlers', () => [ + { + implementation: (req, res, _next, end, hooks) => { + if (Array.isArray(req.params)) { + switch (req.params[0]) { + case 1: + res.result = hooks.hook1(); + break; + case 2: + res.result = hooks.hook2(); + break; + case 3: + return end(new Error('test error')); + case 4: + throw new Error('test error'); + case 5: + // eslint-disable-next-line no-throw-literal + throw 'foo'; + default: + throw new Error(`unexpected param "${req.params[0]}"`); + } + } + return end(); + }, + hookNames: { hook1: true, hook2: true }, + methodNames: ['method1', 'method2'], + }, +]); + +describe('createMethodMiddleware', () => { + const method1 = 'method1'; + + const getDefaultHooks = () => ({ + hook1: () => 42, + hook2: () => 99, + }); + + it('should return a function', () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + expect(typeof middleware).toBe('function'); + }); + + it('should throw an error if a required hook is missing', () => { + const hooks = { hook1: () => 42 }; + + // @ts-expect-error Intentional destructive testing + expect(() => createMethodMiddleware(hooks)).toThrow( + 'Missing expected hooks', + ); + }); + + it('should throw an error if an extraneous hook is provided', () => { + const hooks = { + ...getDefaultHooks(), + extraneousHook: () => 100, + }; + + expect(() => createMethodMiddleware(hooks)).toThrow( + 'Received unexpected hooks', + ); + }); + + it('should call the handler for the matching method (uses hook1)', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: method1, + params: [1], + }); + assertIsJsonRpcSuccess(response); + + expect(response.result).toBe(42); + }); + + it('should call the handler for the matching method (uses hook2)', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: method1, + params: [2], + }); + assertIsJsonRpcSuccess(response); + + expect(response.result).toBe(99); + }); + + it('should not call the handler for a non-matching method', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'nonMatchingMethod', + }); + assertIsJsonRpcFailure(response); + + expect(response.error).toMatchObject({ + message: expect.stringMatching( + /Response has no error or result for request/u, + ), + }); + }); + + it('should reject unsupported methods', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'eth_signTransaction', + }); + assertIsJsonRpcFailure(response); + + expect(response.error.message).toBe('Method not supported.'); + }); + + it('should handle errors returned by the implementation', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: method1, + params: [3], + }); + assertIsJsonRpcFailure(response); + + expect(response.error.message).toBe('test error'); + }); + + it('should handle errors thrown by the implementation', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: method1, + params: [4], + }); + assertIsJsonRpcFailure(response); + + expect(response.error.message).toBe('test error'); + }); + + it('should handle non-errors thrown by the implementation', async () => { + const middleware = createMethodMiddleware(getDefaultHooks()); + const engine = new JsonRpcEngine(); + engine.push(middleware); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: method1, + params: [5], + }); + assertIsJsonRpcFailure(response); + + expect(response.error).toMatchObject({ + message: 'Internal JSON-RPC error.', + data: 'foo', + }); + }); +}); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/index.js b/app/scripts/lib/rpc-method-middleware/handlers/index.ts similarity index 99% rename from app/scripts/lib/rpc-method-middleware/handlers/index.js rename to app/scripts/lib/rpc-method-middleware/handlers/index.ts index 4474b4f8da75..0c4e7acbb4c8 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/index.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/index.ts @@ -34,4 +34,5 @@ const handlers = [ mmiOpenAddHardwareWallet, ///: END:ONLY_INCLUDE_IF ]; + export default handlers; diff --git a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js index 0a73b5e0519a..f90fb5bd0d42 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/request-accounts.js @@ -4,6 +4,7 @@ import { MetaMetricsEventName, MetaMetricsEventCategory, } from '../../../../../shared/constants/metametrics'; +import { shouldEmitDappViewedEvent } from '../../util'; /** * This method attempts to retrieve the Ethereum accounts available to the @@ -114,18 +115,20 @@ async function requestEthereumAccountsHandler( const isFirstVisit = !Object.keys(metamaskState.permissionHistory).includes( origin, ); - sendMetrics({ - event: MetaMetricsEventName.DappViewed, - category: MetaMetricsEventCategory.InpageProvider, - referrer: { - url: origin, - }, - properties: { - is_first_visit: isFirstVisit, - number_of_accounts: Object.keys(metamaskState.accounts).length, - number_of_accounts_connected: numberOfConnectedAccounts, - }, - }); + if (shouldEmitDappViewedEvent(metamaskState.metaMetricsId)) { + sendMetrics({ + event: MetaMetricsEventName.DappViewed, + category: MetaMetricsEventCategory.InpageProvider, + referrer: { + url: origin, + }, + properties: { + is_first_visit: isFirstVisit, + number_of_accounts: Object.keys(metamaskState.accounts).length, + number_of_accounts_connected: numberOfConnectedAccounts, + }, + }); + } } else { // This should never happen, because it should be caught in the // above catch clause diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index b03a15a45dd3..afcecd02cb5f 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -86,6 +86,7 @@ export const SENTRY_BACKGROUND_STATE = { browserEnvironment: true, connectedStatusPopoverHasBeenShown: true, currentPopupId: false, + currentExtensionPopupId: false, defaultHomeActiveTabName: true, fullScreenGasPollTokens: true, hadAdvancedGasFeesSetPriorToMigration92_3: true, @@ -99,9 +100,10 @@ export const SENTRY_BACKGROUND_STATE = { recoveryPhraseReminderLastShown: true, showBetaHeader: true, showPermissionsTour: true, - showProductTour: true, showNetworkBanner: true, showAccountBanner: true, + switchedNetworkDetails: false, + switchedNetworkNeverShowMessage: false, showTestnetMessageInDropdown: true, surveyLinkLastClickedOrClosed: true, snapsInstallPrivacyWarningShown: true, @@ -134,6 +136,7 @@ export const SENTRY_BACKGROUND_STATE = { gasEstimateType: true, gasFeeEstimates: true, gasFeeEstimatesByChainId: true, + nonRPCGasFeeApisDisabled: false, }, KeyringController: { isUnlocked: true, @@ -219,9 +222,11 @@ export const SENTRY_BACKGROUND_STATE = { showExtensionInFullSizeView: true, showFiatInTestnets: true, showTestNetworks: true, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, petnamesEnabled: true, }, + useExternalServices: false, selectedAddress: false, snapRegistryList: false, theme: true, @@ -237,6 +242,7 @@ export const SENTRY_BACKGROUND_STATE = { useTokenDetection: true, useRequestQueue: true, useTransactionSimulations: true, + enableMV3TimestampSave: true, hasDismissedOpenSeaToBlockaidBanner: true, }, SelectedNetworkController: { domains: false }, @@ -381,6 +387,8 @@ export const SENTRY_UI_STATE = { addSnapAccountEnabled: false, snapsAddSnapAccountModalDismissed: false, ///: END:ONLY_INCLUDE_IF + switchedNetworkDetails: false, + switchedNetworkNeverShowMessage: false, }, unconnectedAccount: true, }; @@ -475,10 +483,18 @@ export default function setupSentry({ release, getState }) { `Missing SENTRY_DSN environment variable in production environment`, ); } + + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) + sentryTarget = process.env.SENTRY_MMI_DSN; + ///: END:ONLY_INCLUDE_IF + + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + sentryTarget = process.env.SENTRY_DSN; + ///: END:ONLY_INCLUDE_IF + console.log( `Setting up Sentry Remote Error Reporting for '${environment}': SENTRY_DSN`, ); - sentryTarget = process.env.SENTRY_DSN; } else { console.log( `Setting up Sentry Remote Error Reporting for '${environment}': SENTRY_DSN_DEV`, @@ -493,6 +509,10 @@ export default function setupSentry({ release, getState }) { * @returns `true` if MetaMetrics is enabled, `false` otherwise. */ async function getMetaMetricsEnabled() { + if (METAMASK_BUILD_TYPE === 'mmi') { + return true; + } + const appState = getState(); if (appState.state || appState.persistedState) { return getMetaMetricsEnabledFromAppState(appState); @@ -508,6 +528,22 @@ export default function setupSentry({ release, getState }) { } } + /** + * Returns whether Sentry should be enabled or not. If the build type is mmi + * it will always be enabled, if it's main it will first check for MetaMetrics + * value before returning true or false + * + * @returns `true` if Sentry should be enabled, depends on the build type and + * whether MetaMetrics is on or off for all build types except mmi + */ + async function getSentryEnabled() { + // For MMI we want Sentry always logging, doesn't depend on MetaMetrics being on or off + if (METAMASK_BUILD_TYPE === 'mmi') { + return true; + } + return getMetaMetricsEnabled(); + } + Sentry.init({ dsn: sentryTarget, debug: METAMASK_DEBUG, @@ -567,7 +603,7 @@ export default function setupSentry({ release, getState }) { const startSession = async () => { const hub = Sentry.getCurrentHub?.(); const options = hub.getClient?.().getOptions?.() ?? {}; - if (hub && (await getMetaMetricsEnabled()) === true) { + if (hub && (await getSentryEnabled()) === true) { options.autoSessionTracking = true; hub.startSession(); } @@ -581,7 +617,7 @@ export default function setupSentry({ release, getState }) { const endSession = async () => { const hub = Sentry.getCurrentHub?.(); const options = hub.getClient?.().getOptions?.() ?? {}; - if (hub && (await getMetaMetricsEnabled()) === false) { + if (hub && (await getSentryEnabled()) === false) { options.autoSessionTracking = false; hub.endSession(); } @@ -597,14 +633,11 @@ export default function setupSentry({ release, getState }) { const options = hub.getClient?.().getOptions?.() ?? { autoSessionTracking: false, }; - const isMetaMetricsEnabled = await getMetaMetricsEnabled(); - if ( - isMetaMetricsEnabled === true && - options.autoSessionTracking === false - ) { + const isSentryEnabled = await getSentryEnabled(); + if (isSentryEnabled === true && options.autoSessionTracking === false) { await startSession(); } else if ( - isMetaMetricsEnabled === false && + isSentryEnabled === false && options.autoSessionTracking === true ) { await endSession(); diff --git a/app/scripts/lib/snap-keyring/metrics.test.ts b/app/scripts/lib/snap-keyring/metrics.test.ts index e0ef57fc2b59..fe49b2be73e4 100644 --- a/app/scripts/lib/snap-keyring/metrics.test.ts +++ b/app/scripts/lib/snap-keyring/metrics.test.ts @@ -3,6 +3,8 @@ import { getSnapAndHardwareInfoForMetrics } from './metrics'; describe('getSnapAndHardwareInfoForMetrics', () => { let getAccountType: jest.Mock; let getDeviceModel: jest.Mock; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any let messenger: any; beforeEach(() => { diff --git a/app/scripts/lib/snap-keyring/snap-keyring.ts b/app/scripts/lib/snap-keyring/snap-keyring.ts index 17683dee0a52..62b61c8ed335 100644 --- a/app/scripts/lib/snap-keyring/snap-keyring.ts +++ b/app/scripts/lib/snap-keyring/snap-keyring.ts @@ -51,14 +51,22 @@ export const snapKeyringBuilder = ( 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, trackEvent: ( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any payload: Record, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any options?: Record, ) => void, getSnapName: (snapId: string) => string, ) => { const builder = (() => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any return new SnapKeyring(getSnapController() as any, { addressExists: async (address) => { const addresses = await controllerMessenger.call( @@ -348,6 +356,8 @@ export const snapKeyringBuilder = ( } }, }); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as any; builder.type = SnapKeyring.type; return builder; diff --git a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts index 839176dca5a1..3a493b764b35 100644 --- a/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts +++ b/app/scripts/lib/snap-keyring/utils/isBlockedUrl.test.ts @@ -8,6 +8,9 @@ describe('isBlockedUrl', () => { name: 'PhishingController', }); const phishingController = new PhishingController({ + // @ts-expect-error The PhishingController uses a newer verison of the package + // `@metamask/base-controller`, which has a different messenger type. This error will be + // resolved shortly when the `@metamask/base-controller` package is updated. messenger: phishingControllerMessenger, state: { phishingLists: [ @@ -36,6 +39,8 @@ describe('isBlockedUrl', () => { [1, true], [0, true], [-1, true], + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any ])('"%s" is blocked: %s', async (url: any, expected: boolean) => { const result = await isBlockedUrl( url, diff --git a/app/scripts/lib/snap-keyring/utils/showResult.ts b/app/scripts/lib/snap-keyring/utils/showResult.ts index c0b4c530ed09..086f9fe07174 100644 --- a/app/scripts/lib/snap-keyring/utils/showResult.ts +++ b/app/scripts/lib/snap-keyring/utils/showResult.ts @@ -42,6 +42,8 @@ export const showError = ( controllerMessenger: SnapKeyringBuilderMessenger, snapId: string, opts: ResultComponentOptions, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any properties: Record, ): Promise => { return controllerMessenger.call('ApprovalController:showError', { @@ -70,6 +72,8 @@ export const showSuccess = ( controllerMessenger: SnapKeyringBuilderMessenger, snapId: string, opts: ResultComponentOptions, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any properties: Record, ): Promise => { return controllerMessenger.call('ApprovalController:showSuccess', { diff --git a/app/scripts/lib/transaction/metrics.test.ts b/app/scripts/lib/transaction/metrics.test.ts index a4eb13918d7d..74d5aaa13ca0 100644 --- a/app/scripts/lib/transaction/metrics.test.ts +++ b/app/scripts/lib/transaction/metrics.test.ts @@ -68,8 +68,12 @@ const mockTransactionMetricsRequest = { getTokenStandardAndDetails: jest.fn(), getTransaction: jest.fn(), provider: provider as Provider, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any snapAndHardwareMessenger: jest.fn() as any, trackEvent: jest.fn(), + getIsSmartTransaction: jest.fn(), + getSmartTransactionByMinedTxHash: jest.fn(), } as TransactionMetricsRequest; describe('Transaction metrics', () => { @@ -77,9 +81,17 @@ describe('Transaction metrics', () => { mockChainId, mockNetworkId, mockTransactionMeta: TransactionMeta, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockTransactionMetaWithBlockaid: any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any expectedProperties: any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any expectedSensitiveProperties: any, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any mockActionId: any; beforeEach(() => { @@ -160,6 +172,8 @@ describe('Transaction metrics', () => { describe('handleTransactionAdded', () => { it('should return if transaction meta is not defined', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any await handleTransactionAdded(mockTransactionMetricsRequest, {} as any); expect( mockTransactionMetricsRequest.createEventFragment, @@ -168,6 +182,8 @@ describe('Transaction metrics', () => { it('should create event fragment', async () => { await handleTransactionAdded(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMeta as any, actionId: mockActionId, }); @@ -195,6 +211,8 @@ describe('Transaction metrics', () => { }; await handleTransactionAdded(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMeta as any, actionId: mockActionId, }); @@ -221,6 +239,8 @@ describe('Transaction metrics', () => { it('should create event fragment with blockaid', async () => { await handleTransactionAdded(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMetaWithBlockaid as any, actionId: mockActionId, }); @@ -251,6 +271,8 @@ describe('Transaction metrics', () => { describe('handleTransactionApproved', () => { it('should return if transaction meta is not defined', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any await handleTransactionApproved(mockTransactionMetricsRequest, {} as any); expect( mockTransactionMetricsRequest.createEventFragment, @@ -265,6 +287,8 @@ describe('Transaction metrics', () => { it('should create, update, finalize event fragment', async () => { await handleTransactionApproved(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMeta as any, actionId: mockActionId, }); @@ -306,6 +330,8 @@ describe('Transaction metrics', () => { it('should create, update, finalize event fragment with blockaid', async () => { await handleTransactionApproved(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMetaWithBlockaid as any, actionId: mockActionId, }); @@ -362,6 +388,8 @@ describe('Transaction metrics', () => { describe('handleTransactionFailed', () => { it('should return if transaction meta is not defined', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any await handleTransactionFailed(mockTransactionMetricsRequest, {} as any); expect( mockTransactionMetricsRequest.createEventFragment, @@ -386,6 +414,8 @@ describe('Transaction metrics', () => { transactionMeta: mockTransactionMeta, actionId: mockActionId, error: mockErrorMessage, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -440,6 +470,8 @@ describe('Transaction metrics', () => { transactionMeta: mockTransactionMetaWithBlockaid, actionId: mockActionId, error: mockErrorMessage, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -503,6 +535,8 @@ describe('Transaction metrics', () => { transactionMeta: mockTransactionMeta, actionId: mockActionId, error: mockErrorMessage, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -550,6 +584,8 @@ describe('Transaction metrics', () => { it('should return if transaction meta is not defined', async () => { await handleTransactionConfirmed( mockTransactionMetricsRequest, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any, ); expect( @@ -571,8 +607,10 @@ describe('Transaction metrics', () => { mockTransactionMeta.submittedTime = 123; await handleTransactionConfirmed(mockTransactionMetricsRequest, { - transactionMeta: mockTransactionMeta, + ...mockTransactionMeta, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -627,8 +665,10 @@ describe('Transaction metrics', () => { mockTransactionMetaWithBlockaid.submittedTime = 123; await handleTransactionConfirmed(mockTransactionMetricsRequest, { - transactionMeta: mockTransactionMetaWithBlockaid, + ...mockTransactionMetaWithBlockaid, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -692,6 +732,8 @@ describe('Transaction metrics', () => { describe('handleTransactionDropped', () => { it('should return if transaction meta is not defined', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any await handleTransactionDropped(mockTransactionMetricsRequest, {} as any); expect( mockTransactionMetricsRequest.createEventFragment, @@ -708,6 +750,8 @@ describe('Transaction metrics', () => { await handleTransactionDropped(mockTransactionMetricsRequest, { transactionMeta: mockTransactionMeta, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -756,6 +800,8 @@ describe('Transaction metrics', () => { await handleTransactionDropped(mockTransactionMetricsRequest, { transactionMeta: mockTransactionMetaWithBlockaid, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-submitted-1'; @@ -817,6 +863,8 @@ describe('Transaction metrics', () => { describe('handleTransactionRejected', () => { it('should return if transaction meta is not defined', async () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any await handleTransactionRejected(mockTransactionMetricsRequest, {} as any); expect( mockTransactionMetricsRequest.createEventFragment, @@ -833,6 +881,8 @@ describe('Transaction metrics', () => { await handleTransactionRejected(mockTransactionMetricsRequest, { transactionMeta: mockTransactionMeta, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-added-1'; @@ -876,6 +926,8 @@ describe('Transaction metrics', () => { await handleTransactionRejected(mockTransactionMetricsRequest, { transactionMeta: mockTransactionMetaWithBlockaid, actionId: mockActionId, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const expectedUniqueId = 'transaction-added-1'; @@ -934,6 +986,8 @@ describe('Transaction metrics', () => { it('should return if transaction meta is not defined', async () => { await handleTransactionSubmitted( mockTransactionMetricsRequest, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any {} as any, ); expect( @@ -943,6 +997,8 @@ describe('Transaction metrics', () => { it('should only create event fragment', async () => { await handleTransactionSubmitted(mockTransactionMetricsRequest, { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transactionMeta: mockTransactionMeta as any, actionId: mockActionId, }); diff --git a/app/scripts/lib/transaction/metrics.ts b/app/scripts/lib/transaction/metrics.ts index 1335878393a9..667643bd8408 100644 --- a/app/scripts/lib/transaction/metrics.ts +++ b/app/scripts/lib/transaction/metrics.ts @@ -7,6 +7,7 @@ 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, @@ -38,6 +39,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(blockaid) import { getBlockaidMetricsProps } from '../../../../ui/helpers/utils/metrics'; ///: END:ONLY_INCLUDE_IF +import { getSmartTransactionMetricsProperties } from '../../../../shared/modules/metametrics'; import { getSnapAndHardwareInfoForMetrics, type SnapAndHardwareMessenger, @@ -69,6 +71,8 @@ export type TransactionMetricsRequest = { // According to the type GasFeeState returned from getEIP1559GasFeeEstimates // doesn't include some properties used in buildEventFragmentProperties, // hence returning any here to avoid type errors. + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getEIP1559GasFeeEstimates(options?: FetchGasFeeEstimateOptions): Promise; getParticipateInMetrics: () => boolean; getSelectedAddress: () => string; @@ -81,7 +85,13 @@ export type TransactionMetricsRequest = { getTransaction: (transactionId: string) => TransactionMeta; provider: Provider; snapAndHardwareMessenger: SnapAndHardwareMessenger; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any trackEvent: (payload: any) => void; + getIsSmartTransaction: () => boolean; + getSmartTransactionByMinedTxHash: ( + txhash: string | undefined, + ) => SmartTransaction; }; export const METRICS_STATUS_FAILED = 'failed on-chain'; @@ -92,6 +102,11 @@ export type TransactionEventPayload = { error?: string; }; +export type TransactionMetaEventPayload = TransactionMeta & { + actionId?: string; + error?: string; +}; + /** * This function is called when a transaction is added to the controller. * @@ -161,6 +176,8 @@ export const handleTransactionFailed = async ( return; } + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const extraParams = {} as Record; if (transactionEventPayload.error) { // This is a failed transaction @@ -185,14 +202,16 @@ export const handleTransactionFailed = async ( */ export const handleTransactionConfirmed = async ( transactionMetricsRequest: TransactionMetricsRequest, - transactionEventPayload: TransactionEventPayload, + transactionEventPayload: TransactionMetaEventPayload, ) => { - if (!transactionEventPayload.transactionMeta) { + if (Object.keys(transactionEventPayload).length === 0) { return; } + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const extraParams = {} as Record; - const { transactionMeta } = transactionEventPayload; + const transactionMeta = { ...transactionEventPayload }; const { txReceipt } = transactionMeta; extraParams.gas_used = txReceipt?.gasUsed; @@ -209,7 +228,10 @@ export const handleTransactionConfirmed = async ( await createUpdateFinalizeTransactionEventFragment({ eventName: TransactionMetaMetricsEvent.finalized, extraParams, - transactionEventPayload, + transactionEventPayload: { + actionId: transactionMeta.actionId, + transactionMeta, + }, transactionMetricsRequest, }); }; @@ -484,6 +506,8 @@ function createTransactionEventFragment({ eventName: TransactionMetaMetricsEvent; transactionEventPayload: TransactionEventPayload; transactionMetricsRequest: TransactionMetricsRequest; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any payload: any; }) { if ( @@ -597,6 +621,8 @@ function updateTransactionEventFragment({ eventName: TransactionMetaMetricsEvent; transactionEventPayload: TransactionEventPayload; transactionMetricsRequest: TransactionMetricsRequest; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any payload: any; }) { const uniqueId = getUniqueId(eventName, transactionMeta.id); @@ -666,6 +692,8 @@ async function createUpdateFinalizeTransactionEventFragment({ eventName: TransactionMetaMetricsEvent; transactionEventPayload: TransactionEventPayload; transactionMetricsRequest: TransactionMetricsRequest; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any extraParams?: Record; }) { const { properties, sensitiveProperties } = @@ -703,6 +731,8 @@ async function createUpdateFinalizeTransactionEventFragment({ } function hasFragment( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getEventFragmentById: (arg0: string) => any, eventName: TransactionMetaMetricsEvent, transactionMeta: TransactionMeta, @@ -731,6 +761,8 @@ async function buildEventFragmentProperties({ transactionMetricsRequest, extraParams = {}, }: { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any extraParams?: Record; transactionEventPayload: TransactionEventPayload; transactionMetricsRequest: TransactionMetricsRequest; @@ -770,6 +802,8 @@ async function buildEventFragmentProperties({ transactionMetricsRequest.getTokenStandardAndDetails, ); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const gasParams = {} as Record; if (isEIP1559Transaction(transactionMeta)) { @@ -849,7 +883,6 @@ async function buildEventFragmentProperties({ TransactionType.tokenMethodSetApprovalForAll, TransactionType.tokenMethodTransfer, TransactionType.tokenMethodTransferFrom, - TransactionType.smart, TransactionType.swap, TransactionType.swapApproval, ].includes(type); @@ -935,6 +968,8 @@ async function buildEventFragmentProperties({ } ///: BEGIN:ONLY_INCLUDE_IF(blockaid) + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const blockaidProperties: any = getBlockaidMetricsProps(transactionMeta); if (blockaidProperties?.ui_customizations?.length > 0) { @@ -946,6 +981,12 @@ async function buildEventFragmentProperties({ uiCustomizations.push(MetaMetricsEventUiCustomization.GasEstimationFailed); } + const smartTransactionMetricsProperties = + getSmartTransactionMetricsProperties( + transactionMetricsRequest, + transactionMeta, + ); + /** The transaction status property is not considered sensitive and is now included in the non-anonymous event */ let properties = { chain_id: chainId, @@ -972,6 +1013,9 @@ async function buildEventFragmentProperties({ ///: END:ONLY_INCLUDE_IF // ui_customizations must come after ...blockaidProperties ui_customizations: uiCustomizations.length > 0 ? uiCustomizations : null, + ...smartTransactionMetricsProperties, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record; const snapAndHardwareInfo = await getSnapAndHardwareInfoForMetrics( @@ -998,6 +1042,8 @@ async function buildEventFragmentProperties({ transaction_replaced: transactionReplaced, ...extraParams, ...gasParamsInGwei, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record; if (transactionContractMethod === contractMethodNames.APPROVE) { @@ -1013,7 +1059,11 @@ async function buildEventFragmentProperties({ return { properties, sensitiveProperties }; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function getGasValuesInGWEI(gasParams: Record) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const gasValuesInGwei = {} as Record; for (const param in gasParams) { if (isHexString(gasParams[param])) { diff --git a/app/scripts/lib/transaction/mmi-hooks.test.ts b/app/scripts/lib/transaction/mmi-hooks.test.ts index 210c3093ada8..fee5dff83de5 100644 --- a/app/scripts/lib/transaction/mmi-hooks.test.ts +++ b/app/scripts/lib/transaction/mmi-hooks.test.ts @@ -13,6 +13,8 @@ describe('MMI hooks', () => { const custodyIdMocked = '123'; describe('afterTransactionSign', () => { it('returns false if txMeta has no custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { to: toMocked } as any; const signedEthTx = {}; const result = afterTransactionSign(txMeta, signedEthTx, jest.fn()); @@ -24,6 +26,8 @@ describe('MMI hooks', () => { custodyStatus: TransactionStatus.approved, custodyId: custodyIdMocked, txParams: { from: fromMocked }, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const signedEthTx = { custodian_transactionId: custodyIdMocked, @@ -47,12 +51,16 @@ describe('MMI hooks', () => { describe('beforeTransactionPublish', () => { it('returns true if txMeta has custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { custodyStatus: TransactionStatus.approved } as any; const result = beforeTransactionPublish(txMeta); expect(result).toBe(false); }); it('returns false if txMeta has no custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { to: toMocked } as any; const result = beforeTransactionPublish(txMeta); expect(result).toBe(true); @@ -61,12 +69,16 @@ describe('MMI hooks', () => { describe('getAdditionalSignArguments', () => { it('returns an array with txMeta when custodyStatus is truthy', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { custodyStatus: TransactionStatus.approved } as any; const result = getAdditionalSignArguments(txMeta); expect(result).toEqual([txMeta]); }); it('returns an empty array when custodyStatus is falsy', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { to: toMocked } as any; const result = getAdditionalSignArguments(txMeta); expect(result).toEqual([]); @@ -75,12 +87,16 @@ describe('MMI hooks', () => { describe('beforeTransactionApproveOnInit', () => { it('returns true if txMeta has custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { custodyStatus: TransactionStatus.approved } as any; const result = beforeTransactionApproveOnInit(txMeta); expect(result).toBe(false); }); it('returns false if txMeta has no custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { to: toMocked } as any; const result = beforeTransactionApproveOnInit(txMeta); expect(result).toBe(true); @@ -92,12 +108,16 @@ describe('MMI hooks', () => { const txMeta = { custodyStatus: TransactionStatus.approved, custodyId: 1, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const result = beforeCheckPendingTransaction(txMeta); expect(result).toBe(false); }); it('returns false if txMeta has no custodyStatus', () => { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const txMeta = { to: toMocked } as any; const result = beforeCheckPendingTransaction(txMeta); expect(result).toBe(true); diff --git a/app/scripts/lib/transaction/mmi-hooks.ts b/app/scripts/lib/transaction/mmi-hooks.ts index 92ea0cf792b9..d4dc3cc4dcda 100644 --- a/app/scripts/lib/transaction/mmi-hooks.ts +++ b/app/scripts/lib/transaction/mmi-hooks.ts @@ -9,6 +9,8 @@ import { TransactionMeta } from '@metamask/transaction-controller'; */ export function afterTransactionSign( txMeta: TransactionMeta, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any signedEthTx: any, addTransactionToWatchList: ( custodianTransactionId: string | undefined, diff --git a/app/scripts/lib/transaction/smart-transactions.test.ts b/app/scripts/lib/transaction/smart-transactions.test.ts new file mode 100644 index 000000000000..507ab83ecdf2 --- /dev/null +++ b/app/scripts/lib/transaction/smart-transactions.test.ts @@ -0,0 +1,365 @@ +import EventEmitter from 'events'; +import { + TransactionType, + TransactionStatus, + TransactionController, +} from '@metamask/transaction-controller'; +import SmartTransactionsController from '@metamask/smart-transactions-controller'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { submitSmartTransactionHook } from './smart-transactions'; +import type { + SubmitSmartTransactionRequest, + SmartTransactionsControllerMessenger, +} from './smart-transactions'; + +const addressFrom = '0xabce7847fd3661a9b7c86aaf1daea08d9da5750e'; +const txHash = + '0x0302b75dfb9fd9eb34056af031efcaee2a8cbd799ea054a85966165cd82a7356'; +const uuid = 'uuid'; +const txId = '1'; + +let addRequestCallback: () => void; + +type SubmitSmartTransactionRequestMocked = SubmitSmartTransactionRequest & { + smartTransactionsController: jest.Mocked; + transactionController: jest.Mocked; +}; + +const createSignedTransaction = () => { + return '0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a02b79f322a625d623a2bb2911e0c6b3e7eaf741a7c7c5d2e8c67ef3ff4acf146ca01ae168fea63dc3391b75b586c8a7c0cb55cdf3b8e2e4d8e097957a3a56c6f2c5'; +}; + +const createTransactionControllerMock = () => { + return { + approveTransactionsWithSameNonce: jest.fn((transactions = []) => { + return transactions.length === 0 ? [] : [createSignedTransaction()]; + }), + state: { transactions: [] }, + } as unknown as jest.Mocked; +}; + +const createSmartTransactionsControllerMessengerMock = () => { + return { + call: jest.fn((type) => { + if (type === 'ApprovalController:addRequest') { + return { + then: (callback: () => void) => { + addRequestCallback = callback; + }, + }; + } + return Promise.resolve({ id: 'approvalId' }); + }), + } as unknown as jest.Mocked; +}; + +const createSmartTransactionsControllerMock = () => { + return { + getFees: jest.fn(async () => { + return { + tradeTxFees: { + cancelFees: [], + feeEstimate: 42000000000000, + fees: [ + { maxFeePerGas: 12843636951, maxPriorityFeePerGas: 2853145236 }, + ], + gasLimit: 21000, + gasUsed: 21000, + }, + }; + }), + submitSignedTransactions: jest.fn(async () => { + return { + uuid, + txHash, + }; + }), + eventEmitter: new EventEmitter(), + } as unknown as jest.Mocked; +}; + +describe('submitSmartTransactionHook', () => { + const createRequest = () => { + return { + transactionMeta: { + hash: txHash, + status: TransactionStatus.signed, + id: '1', + txParams: { + from: addressFrom, + to: '0x1678a085c290ebd122dc42cba69373b5953b831d', + gasPrice: '0x77359400', + gas: '0x7b0d', + nonce: '0x4b', + }, + type: TransactionType.simpleSend, + chainId: CHAIN_IDS.MAINNET, + time: 1624408066355, + defaultGasEstimates: { + gas: '0x7b0d', + gasPrice: '0x77359400', + }, + error: { + name: 'Error', + message: 'Details of the error', + }, + securityProviderResponse: { + flagAsDangerous: 0, + }, + }, + smartTransactionsController: createSmartTransactionsControllerMock(), + transactionController: createTransactionControllerMock(), + isSmartTransaction: true, + controllerMessenger: createSmartTransactionsControllerMessengerMock(), + featureFlags: { + extensionActive: true, + mobileActive: false, + smartTransactions: { + expectedDeadline: 45, + maxDeadline: 150, + returnTxHashAsap: false, + }, + }, + }; + }; + + beforeEach(() => { + addRequestCallback = () => undefined; + }); + + it('does not submit a transaction that is not a smart transaction', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + request.isSmartTransaction = false; + const result = await submitSmartTransactionHook(request); + expect(result).toEqual({ transactionHash: undefined }); + }); + + it('falls back to regular transaction submit if /getFees throws an error', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + jest + .spyOn(request.smartTransactionsController, 'getFees') + .mockImplementation(() => { + throw new Error('Backend call to /getFees failed'); + }); + const result = await submitSmartTransactionHook(request); + expect(request.controllerMessenger.call).toHaveBeenCalledWith( + 'ApprovalController:endFlow', + { + id: 'approvalId', + }, + ); + expect(result).toEqual({ transactionHash: undefined }); + }); + + it('returns a txHash asap if the feature flag requires it', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + request.featureFlags.smartTransactions.returnTxHashAsap = true; + const result = await submitSmartTransactionHook(request); + expect(result).toEqual({ transactionHash: txHash }); + }); + + it('throws an error if there is no uuid', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + request.smartTransactionsController.submitSignedTransactions = jest.fn( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async ({ signedTransactions, signedCanceledTransactions }) => { + return { uuid: undefined }; + }, + ); + await expect(submitSmartTransactionHook(request)).rejects.toThrow( + 'No smart transaction UUID', + ); + }); + + it('throws an error if there is no transaction hash', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + setImmediate(() => { + request.smartTransactionsController.eventEmitter.emit( + `uuid:smartTransaction`, + { + status: 'cancelled', + statusMetadata: { + minedHash: '', + }, + }, + ); + }); + await expect(submitSmartTransactionHook(request)).rejects.toThrow( + 'Transaction does not have a transaction hash, there was a problem', + ); + }); + + it('submits a smart transaction', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + 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, + ).toHaveBeenCalledWith( + [ + { + ...txParams, + maxFeePerGas: '0x2fd8a58d7', + maxPriorityFeePerGas: '0xaa0f8a94', + chainId, + }, + ], + { hasNonce: true }, + ); + expect( + request.smartTransactionsController.submitSignedTransactions, + ).toHaveBeenCalledWith({ + signedTransactions: [createSignedTransaction()], + 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('submits a smart transaction and does not update approval request if approval was already approved or rejected', async () => { + const request: SubmitSmartTransactionRequestMocked = createRequest(); + setImmediate(() => { + request.smartTransactionsController.eventEmitter.emit( + `uuid:smartTransaction`, + { + status: 'pending', + uuid, + statusMetadata: { + minedHash: '', + }, + }, + ); + addRequestCallback(); + request.smartTransactionsController.eventEmitter.emit( + `uuid:smartTransaction`, + { + status: 'success', + uuid, + statusMetadata: { + minedHash: txHash, + }, + }, + ); + }); + const result = await submitSmartTransactionHook(request); + expect(result).toEqual({ transactionHash: txHash }); + const { txParams, chainId } = request.transactionMeta; + expect( + request.transactionController.approveTransactionsWithSameNonce, + ).toHaveBeenCalledWith( + [ + { + ...txParams, + maxFeePerGas: '0x2fd8a58d7', + maxPriorityFeePerGas: '0xaa0f8a94', + chainId, + }, + ], + { hasNonce: true }, + ); + expect( + request.smartTransactionsController.submitSignedTransactions, + ).toHaveBeenCalledWith({ + signedTransactions: [createSignedTransaction()], + signedCanceledTransactions: [], + txParams, + transactionMeta: request.transactionMeta, + }); + expect(request.controllerMessenger.call).toHaveBeenCalledTimes(3); + 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:endFlow', + { + id: 'approvalId', + }, + ); + }); +}); diff --git a/app/scripts/lib/transaction/smart-transactions.ts b/app/scripts/lib/transaction/smart-transactions.ts new file mode 100644 index 000000000000..38f57f597947 --- /dev/null +++ b/app/scripts/lib/transaction/smart-transactions.ts @@ -0,0 +1,338 @@ +import SmartTransactionsController from '@metamask/smart-transactions-controller'; +import { + Fee, + Fees, + SmartTransactionStatuses, + SmartTransaction, +} from '@metamask/smart-transactions-controller/dist/types'; +import type { Hex } from '@metamask/utils'; +import { + TransactionController, + TransactionMeta, + TransactionParams, +} from '@metamask/transaction-controller'; +import log from 'loglevel'; +import { + RestrictedControllerMessenger, + EventConstraint, +} from '@metamask/base-controller'; +import { + AddApprovalRequest, + UpdateRequestState, + StartFlow, + EndFlow, +} from '@metamask/approval-controller'; + +import { decimalToHex } from '../../../../shared/modules/conversion.utils'; +import { CANCEL_GAS_LIMIT_DEC } from '../../../../shared/constants/smartTransactions'; +import { + SMART_TRANSACTION_CONFIRMATION_TYPES, + ORIGIN_METAMASK, +} from '../../../../shared/constants/app'; + +const namespace = 'SmartTransactions'; + +type AllowedActions = + | AddApprovalRequest + | UpdateRequestState + | StartFlow + | EndFlow; + +export type SmartTransactionsControllerMessenger = + RestrictedControllerMessenger< + typeof namespace, + AllowedActions, + EventConstraint, + AllowedActions['type'], + never + >; + +export type FeatureFlags = { + extensionActive: boolean; + mobileActive: boolean; + smartTransactions: { + expectedDeadline?: number; + maxDeadline?: number; + returnTxHashAsap?: boolean; + }; +}; + +export type SubmitSmartTransactionRequest = { + transactionMeta: TransactionMeta; + smartTransactionsController: SmartTransactionsController; + transactionController: TransactionController; + isSmartTransaction: boolean; + controllerMessenger: SmartTransactionsControllerMessenger; + featureFlags: FeatureFlags; +}; + +class SmartTransactionHook { + #approvalFlowEnded: boolean; + + #approvalFlowId: string; + + #chainId: Hex; + + #controllerMessenger: SmartTransactionsControllerMessenger; + + #featureFlags: { + extensionActive: boolean; + mobileActive: boolean; + smartTransactions: { + expectedDeadline?: number; + maxDeadline?: number; + returnTxHashAsap?: boolean; + }; + }; + + #isDapp: boolean; + + #isSmartTransaction: boolean; + + #smartTransactionsController: SmartTransactionsController; + + #transactionController: TransactionController; + + #transactionMeta: TransactionMeta; + + #txParams: TransactionParams; + + constructor(request: SubmitSmartTransactionRequest) { + const { + transactionMeta, + smartTransactionsController, + transactionController, + isSmartTransaction, + controllerMessenger, + featureFlags, + } = request; + this.#approvalFlowId = ''; + this.#approvalFlowEnded = false; + this.#transactionMeta = transactionMeta; + this.#smartTransactionsController = smartTransactionsController; + this.#transactionController = transactionController; + this.#isSmartTransaction = isSmartTransaction; + this.#controllerMessenger = controllerMessenger; + this.#featureFlags = featureFlags; + this.#isDapp = transactionMeta.origin !== ORIGIN_METAMASK; + this.#chainId = transactionMeta.chainId; + this.#txParams = transactionMeta.txParams; + } + + async submit() { + // Will cause TransactionController to publish to the RPC provider as normal. + const useRegularTransactionSubmit = { transactionHash: undefined }; + if (!this.#isSmartTransaction) { + return useRegularTransactionSubmit; + } + const { id: approvalFlowId } = await this.#controllerMessenger.call( + 'ApprovalController:startFlow', + ); + this.#approvalFlowId = approvalFlowId; + let getFeesResponse; + try { + getFeesResponse = await this.#smartTransactionsController.getFees( + { ...this.#txParams, chainId: this.#chainId }, + undefined, + ); + } catch (error) { + log.error( + 'Error in smart transaction publish hook, falling back to regular transaction submission', + error, + ); + this.#onApproveOrReject(); + return useRegularTransactionSubmit; // Fallback to regular transaction submission. + } + try { + const submitTransactionResponse = await this.#signAndSubmitTransactions({ + getFeesResponse, + }); + const uuid = submitTransactionResponse?.uuid; + if (!uuid) { + throw new Error('No smart transaction UUID'); + } + const returnTxHashAsap = + this.#featureFlags?.smartTransactions?.returnTxHashAsap; + this.#addApprovalRequest({ + uuid, + }); + this.#addListenerToUpdateStatusPage({ + uuid, + }); + let transactionHash: string | undefined | null; + if (returnTxHashAsap && submitTransactionResponse?.txHash) { + transactionHash = submitTransactionResponse.txHash; + } else { + transactionHash = await this.#waitForTransactionHash({ + uuid, + }); + } + if (transactionHash === null) { + throw new Error( + 'Transaction does not have a transaction hash, there was a problem', + ); + } + return { transactionHash }; + } catch (error) { + log.error('Error in smart transaction publish hook', error); + this.#onApproveOrReject(); + throw error; + } + } + + #onApproveOrReject() { + if (this.#approvalFlowEnded) { + return; + } + this.#approvalFlowEnded = true; + this.#controllerMessenger.call('ApprovalController:endFlow', { + id: this.#approvalFlowId, + }); + } + + #addApprovalRequest({ uuid }: { uuid: string }) { + const onApproveOrRejectWrapper = () => { + this.#onApproveOrReject(); + }; + this.#controllerMessenger + .call( + 'ApprovalController:addRequest', + { + id: this.#approvalFlowId, + origin, + type: SMART_TRANSACTION_CONFIRMATION_TYPES.showSmartTransactionStatusPage, + requestState: { + smartTransaction: { + status: SmartTransactionStatuses.PENDING, + creationTime: Date.now(), + uuid, + }, + isDapp: this.#isDapp, + txId: this.#transactionMeta.id, + }, + }, + true, + ) + .then(onApproveOrRejectWrapper, onApproveOrRejectWrapper); + } + + async #updateApprovalRequest({ + smartTransaction, + }: { + smartTransaction: SmartTransaction; + }) { + return await this.#controllerMessenger.call( + 'ApprovalController:updateRequestState', + { + id: this.#approvalFlowId, + requestState: { + smartTransaction, + isDapp: this.#isDapp, + txId: this.#transactionMeta.id, + }, + }, + ); + } + + async #addListenerToUpdateStatusPage({ uuid }: { uuid: string }) { + this.#smartTransactionsController.eventEmitter.on( + `${uuid}:smartTransaction`, + async (smartTransaction: SmartTransaction) => { + const { status } = smartTransaction; + if (!status || status === SmartTransactionStatuses.PENDING) { + return; + } + if (!this.#approvalFlowEnded) { + await this.#updateApprovalRequest({ + smartTransaction, + }); + } + }, + ); + } + + #waitForTransactionHash({ uuid }: { uuid: string }): Promise { + return new Promise((resolve) => { + this.#smartTransactionsController.eventEmitter.on( + `${uuid}:smartTransaction`, + async (smartTransaction: SmartTransaction) => { + const { status, statusMetadata } = smartTransaction; + if (!status || status === SmartTransactionStatuses.PENDING) { + return; + } + log.debug('Smart Transaction: ', smartTransaction); + if (statusMetadata?.minedHash) { + log.debug( + 'Smart Transaction - Received tx hash: ', + statusMetadata?.minedHash, + ); + resolve(statusMetadata.minedHash); + } else { + resolve(null); + } + }, + ); + }); + } + + async #signAndSubmitTransactions({ + getFeesResponse, + }: { + getFeesResponse: Fees; + }) { + const signedTransactions = await this.#createSignedTransactions( + getFeesResponse.tradeTxFees?.fees ?? [], + false, + ); + const signedCanceledTransactions = await this.#createSignedTransactions( + getFeesResponse.tradeTxFees?.cancelFees || [], + true, + ); + return await this.#smartTransactionsController.submitSignedTransactions({ + signedTransactions, + signedCanceledTransactions, + txParams: this.#txParams, + transactionMeta: this.#transactionMeta, + }); + } + + #applyFeeToTransaction(fee: Fee, isCancel: boolean): TransactionParams { + const unsignedTransaction = { + ...this.#txParams, + maxFeePerGas: `0x${decimalToHex(fee.maxFeePerGas)}`, + maxPriorityFeePerGas: `0x${decimalToHex(fee.maxPriorityFeePerGas)}`, + gas: isCancel + ? `0x${decimalToHex(CANCEL_GAS_LIMIT_DEC)}` // It has to be 21000 for cancel transactions, otherwise the API would reject it. + : this.#txParams.gas, + }; + if (isCancel) { + unsignedTransaction.to = unsignedTransaction.from; + unsignedTransaction.data = '0x'; + } + return unsignedTransaction; + } + + async #createSignedTransactions( + fees: Fee[], + isCancel: boolean, + ): Promise { + const unsignedTransactions = fees.map((fee) => { + return this.#applyFeeToTransaction(fee, isCancel); + }); + const transactionsWithChainId = unsignedTransactions.map((tx) => ({ + ...tx, + chainId: tx.chainId || this.#chainId, + })); + return (await this.#transactionController.approveTransactionsWithSameNonce( + transactionsWithChainId, + { hasNonce: true }, + )) as string[]; + } +} + +export const submitSmartTransactionHook = ( + request: SubmitSmartTransactionRequest, +) => { + const smartTransactionHook = new SmartTransactionHook(request); + return smartTransactionHook.submit(); +}; diff --git a/app/scripts/lib/transaction/util.test.ts b/app/scripts/lib/transaction/util.test.ts index b69fec9ae959..9b31caff37e0 100644 --- a/app/scripts/lib/transaction/util.test.ts +++ b/app/scripts/lib/transaction/util.test.ts @@ -515,6 +515,8 @@ describe('Transaction Utils', () => { request.securityAlertsEnabled = true; request.chainId = '0x1'; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any request.ppomController.usePPOM = (callback: any) => callback(ppomMock); await addTransaction(request); diff --git a/app/scripts/lib/transaction/util.ts b/app/scripts/lib/transaction/util.ts index f625dcbfb709..1781dd19a416 100644 --- a/app/scripts/lib/transaction/util.ts +++ b/app/scripts/lib/transaction/util.ts @@ -12,6 +12,7 @@ import { AddUserOperationOptions, UserOperationController, } from '@metamask/user-operation-controller'; +import type { Hex } from '@metamask/utils'; ///: BEGIN:ONLY_INCLUDE_IF(blockaid) import { PPOMController } from '@metamask/ppom-validator'; import { captureException } from '@sentry/browser'; @@ -34,6 +35,7 @@ export type SecurityAlertResponse = { result_type: string; providerRequestsCount?: Record; securityAlertId?: string; + description?: string; }; export type AddTransactionOptions = NonNullable< @@ -59,7 +61,7 @@ export type AddTransactionOptions = NonNullable< >; type BaseAddTransactionRequest = { - chainId: string; + chainId: Hex; networkClientId: string; ppomController: PPOMController; securityAlertsEnabled: boolean; @@ -78,6 +80,8 @@ export type AddTransactionRequest = FinalAddTransactionRequest & { }; export type AddDappTransactionRequest = BaseAddTransactionRequest & { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any dappRequest: Record; }; @@ -270,6 +274,8 @@ async function addUserOperationWithController( } = request; const { maxFeePerGas, maxPriorityFeePerGas } = transactionParams; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { origin, requireApproval, type } = transactionOptions as any; const normalisedTransaction: TransactionParams = { diff --git a/app/scripts/lib/util.test.js b/app/scripts/lib/util.test.js index 2ac5970493ce..1868007f7684 100644 --- a/app/scripts/lib/util.test.js +++ b/app/scripts/lib/util.test.js @@ -15,6 +15,7 @@ import { } from '../../../shared/constants/app'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { + shouldEmitDappViewedEvent, addUrlProtocolPrefix, deferredPromise, formatTxMetaForRpcResult, @@ -255,6 +256,19 @@ describe('app utils', () => { }); }); + describe('shouldEmitDappViewedEvent', () => { + it('should return true for valid metrics IDs', () => { + expect(shouldEmitDappViewedEvent('fake-metrics-id-fd20')).toStrictEqual( + true, + ); + }); + it('should return false for invalid metrics IDs', () => { + expect( + shouldEmitDappViewedEvent('fake-metrics-id-invalid'), + ).toStrictEqual(false); + }); + }); + describe('formatTxMetaForRpcResult', () => { it('should correctly format the tx meta object (EIP-1559)', () => { const txMeta = { diff --git a/app/scripts/lib/util.ts b/app/scripts/lib/util.ts index 77c2e8543b64..91aa9058b8a0 100644 --- a/app/scripts/lib/util.ts +++ b/app/scripts/lib/util.ts @@ -181,6 +181,8 @@ export const isValidDate = (d: Date | number) => { */ type DeferredPromise = { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any promise: Promise; resolve?: () => void; reject?: () => void; @@ -274,6 +276,25 @@ export function isWebUrl(urlString: string): boolean { ); } +/** + * Determines whether to emit a MetaMetrics event for a given metaMetricsId. + * Relies on the last 4 characters of the metametricsId. Assumes the IDs are evenly distributed. + * If metaMetricsIds are distributed evenly, this should be a 1% sample rate + * + * @param metaMetricsId - The metametricsId to use for the event. + * @returns Whether to emit the event or not. + */ +export function shouldEmitDappViewedEvent(metaMetricsId: string): boolean { + if (metaMetricsId === null) { + return false; + } + + const lastFourCharacters = metaMetricsId.slice(-4); + const lastFourCharactersAsNumber = parseInt(lastFourCharacters, 16); + + return lastFourCharactersAsNumber % 100 === 0; +} + type FormattedTransactionMeta = { blockHash: string | null; blockNumber: string | null; diff --git a/app/scripts/metamask-controller.actions.test.js b/app/scripts/metamask-controller.actions.test.js index 31dcd6d5bf36..b260d23dbbce 100644 --- a/app/scripts/metamask-controller.actions.test.js +++ b/app/scripts/metamask-controller.actions.test.js @@ -14,7 +14,7 @@ import { PermissionsRequestNotFoundError } from '@metamask/permission-controller import nock from 'nock'; import mockEncryptor from '../../test/lib/mock-encryptor'; -const { Ganache } = require('../../test/e2e/ganache'); +const { Ganache } = require('../../test/e2e/seeder/ganache'); const ganacheServer = new Ganache(); diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2153c5c61558..2aed7de9f559 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -24,8 +24,10 @@ import { wrap, ///: END:ONLY_INCLUDE_IF } from 'lodash'; -import { keyringBuilderFactory } from '@metamask/eth-keyring-controller'; -import { KeyringController } from '@metamask/keyring-controller'; +import { + KeyringController, + keyringBuilderFactory, +} from '@metamask/keyring-controller'; import createFilterMiddleware from '@metamask/eth-json-rpc-filters'; import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import { @@ -151,6 +153,11 @@ import { } from '@metamask/snaps-utils'; ///: END:ONLY_INCLUDE_IF +import { + methodsRequiringNetworkSwitch, + methodsWithConfirmation, +} from '../../shared/constants/methods-tags'; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; ///: END:ONLY_INCLUDE_IF @@ -160,17 +167,15 @@ import { TokenStandard, SIGNING_METHODS, } from '../../shared/constants/transaction'; -import { - GAS_API_BASE_URL, - GAS_DEV_API_BASE_URL, - SWAPS_CLIENT_ID, -} from '../../shared/constants/swaps'; +import { SWAPS_CLIENT_ID } from '../../shared/constants/swaps'; import { CHAIN_IDS, NETWORK_TYPES, TEST_NETWORK_TICKER_MAP, NetworkStatus, } from '../../shared/constants/network'; +import { getAllowedSmartTransactionsChainIds } from '../../shared/constants/smartTransactions'; + import { HardwareDeviceNames, LedgerTransportTypes, @@ -211,6 +216,12 @@ import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens'; import { getTokenValueParam } from '../../shared/lib/metamask-controller-utils'; import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { convertNetworkId } from '../../shared/modules/network.utils'; +import { + getIsSmartTransaction, + getFeatureFlagsByChainId, + getSmartTransactionsOptInStatus, + getCurrentChainSupportsSmartTransactions, +} from '../../shared/modules/selectors'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) handleMMITransactionUpdate, @@ -234,6 +245,7 @@ import { getAdditionalSignArguments as getAdditionalSignArgumentsMMI, } from './lib/transaction/mmi-hooks'; ///: END:ONLY_INCLUDE_IF +import { submitSmartTransactionHook } from './lib/transaction/smart-transactions'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { keyringSnapPermissionsBuilder } from './lib/keyring-snaps-permissions'; ///: END:ONLY_INCLUDE_IF @@ -304,6 +316,14 @@ import { snapKeyringBuilder, getAccountsBySnapId } from './lib/snap-keyring'; import { encryptorFactory } from './lib/encryptor-factory'; import { addDappTransaction, addTransaction } from './lib/transaction/util'; import { LatticeKeyringOffscreen } from './lib/offscreen-bridge/lattice-offscreen-keyring'; +///: BEGIN:ONLY_INCLUDE_IF(snaps) +import PREINSTALLED_SNAPS from './snaps/preinstalled-snaps'; +///: END:ONLY_INCLUDE_IF +import AuthenticationController from './controllers/authentication/authentication-controller'; +import UserStorageController from './controllers/user-storage/user-storage-controller'; +import { WeakRefObjectMap } from './lib/WeakRefObjectMap'; + +import { PushPlatformNotificationsController } from './controllers/push-platform-notifications/push-platform-notifications'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -400,6 +420,12 @@ export default class MetamaskController extends EventEmitter { // next, we will initialize the controllers // controller initialization order matters + const clearPendingConfirmations = () => { + this.encryptionPublicKeyController.clearUnapproved(); + this.decryptMessageController.clearUnapproved(); + this.signatureController.clearUnapproved(); + this.approvalController.clear(ethErrors.provider.userRejectedRequest()); + }; this.queuedRequestController = new QueuedRequestController({ messenger: this.controllerMessenger.getRestricted({ @@ -409,7 +435,10 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:setActiveNetwork', 'SelectedNetworkController:getNetworkClientIdForDomain', ], + allowedEvents: ['SelectedNetworkController:stateChange'], }), + methodsRequiringNetworkSwitch, + clearPendingConfirmations, }); this.approvalController = new ApprovalController({ @@ -486,28 +515,13 @@ export default class MetamaskController extends EventEmitter { this.networkController.getProviderAndBlockTracker().provider; this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker; - - // TODO: Delete when ready to remove `networkVersion` from provider object - this.deprecatedNetworkId = null; - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - () => this.updateDeprecatedNetworkId(), - ); + this.deprecatedNetworkVersions = {}; const tokenListMessenger = this.controllerMessenger.getRestricted({ name: 'TokenListController', allowedEvents: ['NetworkController:stateChange'], }); - this.tokenListController = new TokenListController({ - chainId: this.networkController.state.providerConfig.chainId, - preventPollingOnNetworkRestart: initState.TokenListController - ? initState.TokenListController.preventPollingOnNetworkRestart - : true, - messenger: tokenListMessenger, - state: initState.TokenListController, - }); - const preferencesMessenger = this.controllerMessenger.getRestricted({ name: 'PreferencesController', allowedActions: [], @@ -527,6 +541,15 @@ export default class MetamaskController extends EventEmitter { ), }); + this.tokenListController = new TokenListController({ + chainId: this.networkController.state.providerConfig.chainId, + preventPollingOnNetworkRestart: !this.#isTokenListPollingRequired( + this.preferencesController.store.getState(), + ), + messenger: tokenListMessenger, + state: initState.TokenListController, + }); + this.assetsContractController = new AssetsContractController( { chainId: this.networkController.state.providerConfig.chainId, @@ -684,6 +707,14 @@ export default class MetamaskController extends EventEmitter { 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: + this.preferencesController.store.getState().useNftDetection === + undefined + ? true + : !this.preferencesController.store.getState().useNftDetection, + selectedAddress: + this.preferencesController.store.getState().selectedAddress, }); this.metaMetricsController = new MetaMetricsController({ @@ -720,10 +751,6 @@ export default class MetamaskController extends EventEmitter { allowedEvents: ['NetworkController:stateChange'], }); - const gasApiBaseUrl = process.env.SWAPS_USE_DEV_APIS - ? GAS_DEV_API_BASE_URL - : GAS_API_BASE_URL; - this.gasFeeController = new GasFeeController({ state: initState.GasFeeController, interval: 10000, @@ -743,13 +770,12 @@ export default class MetamaskController extends EventEmitter { ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(this), - legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, - EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, getCurrentNetworkLegacyGasAPICompatibility: () => { const { chainId } = this.networkController.state.providerConfig; return chainId === CHAIN_IDS.BSC; }, getChainId: () => this.networkController.state.providerConfig.chainId, + infuraAPIKey: opts.infuraProjectId, }); this.appStateController = new AppStateController({ @@ -898,21 +924,14 @@ export default class MetamaskController extends EventEmitter { }, initState.TokenRatesController, ); - if (this.preferencesController.store.getState().useCurrencyRateCheck) { - this.tokenRatesController.start(); - } this.preferencesController.store.subscribe( previousValueComparator((prevState, currState) => { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; if (currUseCurrencyRateCheck && !prevUseCurrencyRateCheck) { - this.currencyRateController.startPollingByNetworkClientId( - this.networkController.state.selectedNetworkClientId, - ); this.tokenRatesController.start(); } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { - this.currencyRateController.stopAllPolling(); this.tokenRatesController.stop(); } }, this.preferencesController.store.getState()), @@ -979,6 +998,7 @@ export default class MetamaskController extends EventEmitter { additionalKeyrings.push( mmiKeyringBuilderFactory(CUSTODIAN_TYPES[custodianType].keyringClass, { mmiConfigurationController: this.mmiConfigurationController, + captureException, }), ); } @@ -1060,35 +1080,6 @@ export default class MetamaskController extends EventEmitter { state: initState.KeyringController, encryptor: opts.encryptor || encryptorFactory(600_000), messenger: keyringControllerMessenger, - removeIdentity: this.preferencesController.removeAddress.bind( - this.preferencesController, - ), - setAccountLabel: (address, label) => { - const accountToBeNamed = - this.accountsController.getAccountByAddress(address); - if (accountToBeNamed === undefined) { - throw new Error(`No account found for address: ${address}`); - } - this.accountsController.setAccountName(accountToBeNamed.id, label); - - this.preferencesController.setAccountLabel(address, label); - }, - setSelectedAddress: (address) => { - const accountToBeSet = - this.accountsController.getAccountByAddress(address); - if (accountToBeSet === undefined) { - throw new Error(`No account found for address: ${address}`); - } - - this.accountsController.setSelectedAccount(accountToBeSet.id); - this.preferencesController.setSelectedAddress(address); - }, - syncIdentities: (identities) => { - this.preferencesController.syncAddresses(identities); - }, - updateIdentities: this.preferencesController.setAddresses.bind( - this.preferencesController, - ), }); this.controllerMessenger.subscribe('KeyringController:unlock', () => @@ -1097,6 +1088,7 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.subscribe('KeyringController:lock', () => this._onLock(), ); + this.controllerMessenger.subscribe( 'KeyringController:stateChange', (state) => { @@ -1173,6 +1165,7 @@ export default class MetamaskController extends EventEmitter { allowedActions: [ 'NetworkController:getNetworkClientById', 'NetworkController:getState', + 'NetworkController:getSelectedNetworkClient', 'PermissionController:hasPermissions', 'PermissionController:getSubjectNames', ], @@ -1182,9 +1175,11 @@ export default class MetamaskController extends EventEmitter { ], }), state: initState.SelectedNetworkController, - getUseRequestQueue: this.preferencesController.getUseRequestQueue.bind( - this.preferencesController, - ), + useRequestQueuePreference: + this.preferencesController.store.getState().useRequestQueue, + onPreferencesStateChange: (listener) => + this.preferencesController.store.subscribe(listener), + domainProxyMap: new WeakRefObjectMap(), }); this.permissionLogController = new PermissionLogController({ @@ -1284,6 +1279,9 @@ export default class MetamaskController extends EventEmitter { allowLocalSnaps, requireAllowlist, }, + encryptor: encryptorFactory(600_000), + getMnemonic: this.getPrimaryKeyringMnemonic.bind(this), + preinstalledSnaps: PREINSTALLED_SNAPS, }); this.notificationController = new NotificationController({ @@ -1390,6 +1388,36 @@ export default class MetamaskController extends EventEmitter { ///: END:ONLY_INCLUDE_IF + // Notification Controllers + this.authenticationController = new AuthenticationController({ + state: initState.AuthenticationController, + messenger: this.controllerMessenger.getRestricted({ + name: 'AuthenticationController', + allowedActions: ['SnapController:handleRequest'], + }), + }); + this.userStorageController = new UserStorageController({ + state: initState.UserStorageController, + messenger: this.controllerMessenger.getRestricted({ + name: 'UserStorageController', + allowedActions: [ + 'SnapController:handleRequest', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:getSessionProfile', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignIn', + ], + }), + }); + this.pushPlatformNotificationsController = + new PushPlatformNotificationsController({ + messenger: this.controllerMessenger.getRestricted({ + name: 'PushPlatformNotificationsController', + allowedActions: ['AuthenticationController:getBearerToken'], + }), + state: initState.PushPlatformNotificationsController, + }); + // account tracker watches balances, nonces, and any code at their address this.accountTracker = new AccountTracker({ provider: this.provider, @@ -1431,8 +1459,14 @@ export default class MetamaskController extends EventEmitter { const { completedOnboarding: prevCompletedOnboarding } = prevState; const { completedOnboarding: currCompletedOnboarding } = currState; if (!prevCompletedOnboarding && currCompletedOnboarding) { + const { address } = this.accountsController.getSelectedAccount(); + this.postOnboardingInitialization(); this.triggerNetworkrequests(); + // execute once the token detection on the post-onboarding + await this.tokenDetectionController.detectTokens({ + selectedAddress: address, + }); } }, this.onboardingController.store.getState()), ); @@ -1594,7 +1628,17 @@ export default class MetamaskController extends EventEmitter { () => listener(), ); }, + pendingTransactions: { + isResubmitEnabled: () => { + const state = this._getMetaMaskState(); + return !( + getSmartTransactionsOptInStatus(state) && + getCurrentChainSupportsSmartTransactions(state) + ); + }, + }, provider: this.provider, + testGasFeeFlows: process.env.TEST_GAS_FEE_FLOWS, hooks: { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) afterSign: (txMeta, signedEthTx) => @@ -1611,6 +1655,7 @@ export default class MetamaskController extends EventEmitter { beforePublish: beforeTransactionPublishMMI.bind(this), getAdditionalSignArguments: getAdditionalSignArgumentsMMI.bind(this), ///: END:ONLY_INCLUDE_IF + publish: this._publishSmartTransactionHook.bind(this), }, sign: (...args) => this.keyringController.signTransaction(...args), state: initState.TransactionController, @@ -1618,25 +1663,6 @@ export default class MetamaskController extends EventEmitter { this._addTransactionControllerListeners(); - networkControllerMessenger.subscribe( - 'NetworkController:networkDidChange', - async () => { - try { - if ( - this.preferencesController.store.getState().useCurrencyRateCheck - ) { - await this.currencyRateController.stopAllPolling(); - this.currencyRateController.startPollingByNetworkClientId( - this.networkController.state.selectedNetworkClientId, - ); - } - } catch (error) { - // TODO: Handle failure to get conversion rate more gracefully - console.error(error); - } - }, - ); - this.decryptMessageController = new DecryptMessageController({ getState: this.getState.bind(this), messenger: this.controllerMessenger.getRestricted({ @@ -1761,6 +1787,10 @@ export default class MetamaskController extends EventEmitter { this.txController.updateTransaction(txMeta, note), updateTransactionHash: (id, hash) => this.txController.updateCustodialTransaction(id, { hash }), + setChannelId: (channelId) => + this.institutionalFeaturesController.setChannelId(channelId), + setConnectionRequest: (payload) => + this.institutionalFeaturesController.setConnectionRequest(payload), }); ///: END:ONLY_INCLUDE_IF @@ -1784,6 +1814,11 @@ export default class MetamaskController extends EventEmitter { this.gasFeeController.fetchGasFeeEstimates.bind( this.gasFeeController, ), + getLayer1GasFee: this.txController.getLayer1GasFee.bind( + this.txController, + ), + getNetworkClientId: () => + this.networkController.state.selectedNetworkClientId, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -1799,18 +1834,19 @@ export default class MetamaskController extends EventEmitter { networkControllerMessenger, 'NetworkController:stateChange', ), - getNonceLock: this.txController.nonceTracker.getNonceLock.bind( - this.txController.nonceTracker, - ), + getNonceLock: this.txController.getNonceLock.bind(this.txController), confirmExternalTransaction: this.txController.confirmExternalTransaction.bind(this.txController), + getTransactions: this.txController.getTransactions.bind( + this.txController, + ), provider: this.provider, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }, { - supportedChainIds: [CHAIN_IDS.MAINNET, CHAIN_IDS.GOERLI], + supportedChainIds: getAllowedSmartTransactionsChainIds(), }, initState.SmartTransactionsController, ); @@ -1906,12 +1942,7 @@ export default class MetamaskController extends EventEmitter { // clear unapproved transactions and messages when the network will change networkControllerMessenger.subscribe( 'NetworkController:networkWillChange', - () => { - this.encryptionPublicKeyController.clearUnapproved(); - this.decryptMessageController.clearUnapproved(); - this.signatureController.clearUnapproved(); - this.approvalController.clear(ethErrors.provider.userRejectedRequest()); - }, + clearPendingConfirmations.bind(this), ); this.metamaskMiddleware = createMetamaskMiddleware({ @@ -2201,7 +2232,6 @@ export default class MetamaskController extends EventEmitter { } postOnboardingInitialization() { - this.updateDeprecatedNetworkId(); this.networkController.lookupNetwork(); } @@ -2213,12 +2243,15 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.store.getState(); - const { useCurrencyRateCheck } = preferencesControllerState; + const { useCurrencyRateCheck, useNftDetection } = + preferencesControllerState; + + if (useNftDetection) { + this.nftDetectionController.start(); + } if (useCurrencyRateCheck) { - this.currencyRateController.startPollingByNetworkClientId( - this.networkController.state.selectedNetworkClientId, - ); + this.tokenRatesController.start(); } if (this.#isTokenListPollingRequired(preferencesControllerState)) { @@ -2230,6 +2263,7 @@ export default class MetamaskController extends EventEmitter { this.accountTracker.stop(); this.txController.stopIncomingTransactionPolling(); this.tokenDetectionController.disable(); + this.nftDetectionController.stop(); const preferencesControllerState = this.preferencesController.store.getState(); @@ -2237,12 +2271,11 @@ export default class MetamaskController extends EventEmitter { const { useCurrencyRateCheck } = preferencesControllerState; if (useCurrencyRateCheck) { - this.currencyRateController.stopAllPolling(); + this.tokenRatesController.stop(); } if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.stop(); - this.tokenRatesController.stop(); } } @@ -2370,15 +2403,11 @@ export default class MetamaskController extends EventEmitter { * Constructor helper for getting Snap permission specifications. */ getSnapPermissionSpecifications() { - const snapEncryptor = encryptorFactory(600_000); - return { ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), ...buildSnapRestrictedMethodSpecifications( Object.keys(ExcludedSnapPermissions), { - encrypt: snapEncryptor.encrypt, - decrypt: snapEncryptor.decrypt, getLocale: this.getLocale.bind(this), clearSnapState: this.controllerMessenger.call.bind( this.controllerMessenger, @@ -2678,20 +2707,21 @@ export default class MetamaskController extends EventEmitter { // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore(); - const selectPublicState = (chainId, { isUnlocked }) => { + const selectPublicState = async ({ isUnlocked }) => { + const { chainId, networkVersion } = await this.getProviderNetworkState(); + return { isUnlocked, chainId, - networkVersion: this.deprecatedNetworkId ?? 'loading', + networkVersion: networkVersion ?? 'loading', }; }; - const updatePublicConfigStore = (memState) => { + const updatePublicConfigStore = async (memState) => { const networkStatus = memState.networksMetadata[memState.selectedNetworkClientId]?.status; - const { chainId } = this.networkController.state.providerConfig; if (networkStatus === NetworkStatus.Available) { - publicConfigStore.putState(selectPublicState(chainId, memState)); + publicConfigStore.putState(await selectPublicState(memState)); } }; @@ -2709,12 +2739,14 @@ export default class MetamaskController extends EventEmitter { * @returns {Promise<{ isUnlocked: boolean, networkVersion: string, chainId: string, accounts: string[] }>} An object with relevant state properties. */ async getProviderState(origin) { + const providerNetworkState = await this.getProviderNetworkState( + this.preferencesController.getUseRequestQueue() ? origin : undefined, + ); + return { isUnlocked: this.isUnlocked(), accounts: await this.getPermittedAccounts(origin), - ...this.getProviderNetworkState( - this.preferencesController.getUseRequestQueue() ? origin : undefined, - ), + ...providerNetworkState, }; } @@ -2724,71 +2756,41 @@ export default class MetamaskController extends EventEmitter { * @param {string} origin - The origin identifier for which network state is requested (default: 'metamask'). * @returns {object} An object containing important network state properties, including chainId and networkVersion. */ - getProviderNetworkState(origin = METAMASK_DOMAIN) { - let chainId; - if ( - this.preferencesController.getUseRequestQueue() && - origin !== METAMASK_DOMAIN - ) { - const networkClientId = this.controllerMessenger.call( - 'SelectedNetworkController:getNetworkClientIdForDomain', - origin, - ); + async getProviderNetworkState(origin = METAMASK_DOMAIN) { + const networkClientId = this.controllerMessenger.call( + 'SelectedNetworkController:getNetworkClientIdForDomain', + origin, + ); - const networkClient = this.controllerMessenger.call( - 'NetworkController:getNetworkClientById', - networkClientId, - ); - chainId = networkClient.configuration.chainId; - } else { - chainId = this.networkController.state.providerConfig.chainId; - } + const networkClient = this.controllerMessenger.call( + 'NetworkController:getNetworkClientById', + networkClientId, + ); - return { - chainId, - networkVersion: this.deprecatedNetworkId ?? 'loading', - }; - } + const { chainId } = networkClient.configuration; - /** - * TODO: Delete when ready to remove `networkVersion` from provider object - * Updates the `deprecatedNetworkId` value - */ - async updateDeprecatedNetworkId() { - try { - this.deprecatedNetworkId = await this.deprecatedGetNetworkId(); - } catch (error) { - console.error(error); - this.deprecatedNetworkId = null; - } - this._notifyChainChange(); - } + const { completedOnboarding } = this.onboardingController.store.getState(); - /** - * TODO: Delete when ready to remove `networkVersion` from provider object - * Gets current networkId as returned by `net_version` - * - * @returns {string} The networkId for the current network or null on failure - * @throws Will throw if there is a problem getting the network version - */ - async deprecatedGetNetworkId() { - const ethQuery = this.controllerMessenger.call( - 'NetworkController:getEthQuery', - ); - - if (!ethQuery) { - throw new Error('Provider has not been initialized'); + let networkVersion = this.deprecatedNetworkVersions[networkClientId]; + if (!networkVersion && completedOnboarding) { + const ethQuery = new EthQuery(networkClient.provider); + networkVersion = await new Promise((resolve) => { + ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { + if (error) { + console.error(error); + resolve(null); + } else { + resolve(convertNetworkId(result)); + } + }); + }); + this.deprecatedNetworkVersions[networkClientId] = networkVersion; } - return new Promise((resolve, reject) => { - ethQuery.sendAsync({ method: 'net_version' }, (error, result) => { - if (error) { - reject(error); - } else { - resolve(convertNetworkId(result)); - } - }); - }); + return { + chainId, + networkVersion: networkVersion ?? 'loading', + }; } //============================================================================= @@ -2953,6 +2955,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setIncomingTransactionsPreferences.bind( preferencesController, ), + setServiceWorkerKeepAlivePreference: + preferencesController.setServiceWorkerKeepAlivePreference.bind( + preferencesController, + ), markPasswordForgotten: this.markPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), getRequestAccountTabIds: this.getRequestAccountTabIds, @@ -3005,6 +3011,12 @@ export default class MetamaskController extends EventEmitter { setActiveNetwork: (networkConfigurationId) => { return this.networkController.setActiveNetwork(networkConfigurationId); }, + setNetworkClientIdForDomain: (origin, networkClientId) => { + return this.selectedNetworkController.setNetworkClientIdForDomain( + origin, + networkClientId, + ); + }, rollbackToPreviousProvider: networkController.rollbackToPreviousProvider.bind(networkController), removeNetworkConfiguration: @@ -3031,6 +3043,7 @@ export default class MetamaskController extends EventEmitter { throw new Error(`No account found for address: ${address}`); } }, + toggleExternalServices: this.toggleExternalServices.bind(this), addToken: tokensController.addToken.bind(tokensController), updateTokenType: tokensController.updateTokenType.bind(tokensController), setFeatureFlag: preferencesController.setFeatureFlag.bind( @@ -3120,6 +3133,8 @@ export default class MetamaskController extends EventEmitter { // AppStateController setLastActiveTime: appStateController.setLastActiveTime.bind(appStateController), + setCurrentExtensionPopupId: + appStateController.setCurrentExtensionPopupId.bind(appStateController), setDefaultHomeActiveTabName: appStateController.setDefaultHomeActiveTabName.bind(appStateController), setConnectedStatusPopoverHasBeenShown: @@ -3158,8 +3173,6 @@ export default class MetamaskController extends EventEmitter { appStateController.setShowBetaHeader.bind(appStateController), setShowPermissionsTour: appStateController.setShowPermissionsTour.bind(appStateController), - setShowProductTour: - appStateController.setShowProductTour.bind(appStateController), setShowAccountBanner: appStateController.setShowAccountBanner.bind(appStateController), setShowNetworkBanner: @@ -3168,6 +3181,14 @@ export default class MetamaskController extends EventEmitter { appStateController.updateNftDropDownState.bind(appStateController), setFirstTimeUsedNetwork: appStateController.setFirstTimeUsedNetwork.bind(appStateController), + setSwitchedNetworkDetails: + appStateController.setSwitchedNetworkDetails.bind(appStateController), + clearSwitchedNetworkDetails: + appStateController.clearSwitchedNetworkDetails.bind(appStateController), + setSwitchedNetworkNeverShowMessage: + appStateController.setSwitchedNetworkNeverShowMessage.bind( + appStateController, + ), // EnsController tryReverseResolveAddress: @@ -3227,6 +3248,7 @@ export default class MetamaskController extends EventEmitter { txController.updatePreviousGasParams.bind(txController), abortTransactionSigning: txController.abortTransactionSigning.bind(txController), + getLayer1GasFee: txController.getLayer1GasFee.bind(txController), // decryptMessageController decryptMessage: this.decryptMessageController.decryptMessage.bind( @@ -3318,6 +3340,10 @@ export default class MetamaskController extends EventEmitter { this.institutionalFeaturesController.removeAddTokenConnectRequest.bind( this.institutionalFeaturesController, ), + setConnectionRequest: + this.institutionalFeaturesController.setConnectionRequest.bind( + this.institutionalFeaturesController, + ), showInteractiveReplacementTokenBanner: appStateController.showInteractiveReplacementTokenBanner.bind( appStateController, @@ -3437,10 +3463,6 @@ export default class MetamaskController extends EventEmitter { swapsController.setSwapsQuotesPollingLimitEnabled.bind(swapsController), // Smart Transactions - setSmartTransactionsOptInStatus: - smartTransactionsController.setOptInState.bind( - smartTransactionsController, - ), fetchSmartTransactionFees: smartTransactionsController.getFees.bind( smartTransactionsController, ), @@ -3492,10 +3514,23 @@ export default class MetamaskController extends EventEmitter { rejectPendingApproval: this.rejectPendingApproval, // Notifications + resetViewedNotifications: announcementController.resetViewed.bind( + announcementController, + ), updateViewedNotifications: announcementController.updateViewed.bind( announcementController, ), + // CurrencyRateController + currencyRateStartPollingByNetworkClientId: + currencyRateController.startPollingByNetworkClientId.bind( + currencyRateController, + ), + currencyRateStopPollingByPollingToken: + currencyRateController.stopPollingByPollingToken.bind( + currencyRateController, + ), + // GasFeeController gasFeeStartPollingByNetworkClientId: gasFeeController.startPollingByNetworkClientId.bind(gasFeeController), @@ -3683,15 +3718,7 @@ export default class MetamaskController extends EventEmitter { async createNewVaultAndKeychain(password) { const releaseLock = await this.createVaultMutex.acquire(); try { - const vault = await this.keyringController.createNewVaultAndKeychain( - password, - ); - - const accounts = await this.keyringController.getAccounts(); - this.preferencesController.setAddresses(accounts); - this.selectFirstAccount(); - - return vault; + return await this.keyringController.createNewVaultAndKeychain(password); } finally { releaseLock(); } @@ -3709,9 +3736,6 @@ export default class MetamaskController extends EventEmitter { try { const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase); - // clear known identities - this.preferencesController.setAddresses([]); - // clear permissions this.permissionController.clearState(); @@ -3778,8 +3802,6 @@ export default class MetamaskController extends EventEmitter { // Ledger Keyring GitHub downtime this.setLedgerTransportPreference(); - this.selectFirstAccount(); - return vault; } finally { releaseLock(); @@ -3940,18 +3962,6 @@ export default class MetamaskController extends EventEmitter { * receiving funds from our automatic Ropsten faucet. */ - /** - * Sets the first account in the state to the selected address - */ - selectFirstAccount() { - const { identities } = this.preferencesController.store.getState(); - const [address] = Object.keys(identities); - this.preferencesController.setSelectedAddress(address); - - const [account] = this.accountsController.listAccounts(); - this.accountsController.setSelectedAccount(account.id); - } - /** * Gets the mnemonic of the user's primary keyring. */ @@ -4177,7 +4187,6 @@ export default class MetamaskController extends EventEmitter { keyring, ); const newAccounts = await this.keyringController.getAccounts(); - this.preferencesController.setAddresses(newAccounts); newAccounts.forEach((address) => { if (!oldAccounts.includes(address)) { const label = this.getAccountLabel( @@ -4256,7 +4265,11 @@ export default class MetamaskController extends EventEmitter { async resetAccount() { const selectedAddress = this.accountsController.getSelectedAccount().address; - this.txController.wipeTransactions(true, selectedAddress); + this.txController.wipeTransactions(false, selectedAddress); + this.smartTransactionsController.wipeSmartTransactions({ + address: selectedAddress, + ignoreNetwork: false, + }); this.networkController.resetConnection(); return selectedAddress; @@ -4829,23 +4842,13 @@ export default class MetamaskController extends EventEmitter { // append selectedNetworkClientId to each request engine.push(createSelectedNetworkMiddleware(this.controllerMessenger)); - let proxyClient; - if ( - this.preferencesController.getUseRequestQueue() && - this.selectedNetworkController.state.domains[origin] - ) { - proxyClient = - this.selectedNetworkController.getProviderAndBlockTracker(origin); - } else { - // if useRequestQueue is false we want to use the globally selected network provider/blockTracker - // since this means the per domain network feature is disabled - - // if the origin is not in the selectedNetworkController's `domains` state, - // this means that origin does not have permissions (is not connected to the wallet) - // and will therefore not have its own selected network even if useRequestQueue is true - // and so in this case too we want to use the globally selected network provider/blockTracker - proxyClient = this.networkController.getProviderAndBlockTracker(); - } + // if the origin is not in the selectedNetworkController's `domains` state + // when the provider engine is created, the selectedNetworkController will + // fetch the globally selected networkClient from the networkController and wrap + // it in a proxy which can be switched to use its own state if/when the origin + // is added to the `domains` state + const proxyClient = + this.selectedNetworkController.getProviderAndBlockTracker(origin); const requestQueueMiddleware = createQueuedRequestMiddleware({ enqueueRequest: this.queuedRequestController.enqueueRequest.bind( @@ -4854,6 +4857,7 @@ export default class MetamaskController extends EventEmitter { useRequestQueue: this.preferencesController.getUseRequestQueue.bind( this.preferencesController, ), + methodsWithConfirmation, }); // add some middleware that will switch chain on each request (as needed) engine.push(requestQueueMiddleware); @@ -4892,6 +4896,11 @@ export default class MetamaskController extends EventEmitter { ); ///: END:ONLY_INCLUDE_IF + const isConfirmationRedesignEnabled = () => { + return this.preferencesController.store.getState().preferences + .redesignedConfirmationsEnabled; + }; + engine.push( createRPCMethodTrackingMiddleware({ trackEvent: this.metaMetricsController.trackEvent.bind( @@ -4902,6 +4911,7 @@ export default class MetamaskController extends EventEmitter { ), getAccountType: this.getAccountType.bind(this), getDeviceModel: this.getDeviceModel.bind(this), + isConfirmationRedesignEnabled, snapAndHardwareMessenger: this.controllerMessenger.getRestricted({ name: 'SnapAndHardwareMessenger', allowedActions: [ @@ -4954,16 +4964,6 @@ export default class MetamaskController extends EventEmitter { endApprovalFlow: this.approvalController.endFlow.bind( this.approvalController, ), - setApprovalFlowLoadingText: - this.approvalController.setFlowLoadingText.bind( - this.approvalController, - ), - showApprovalSuccess: this.approvalController.success.bind( - this.approvalController, - ), - showApprovalError: this.approvalController.error.bind( - this.approvalController, - ), sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), @@ -5025,18 +5025,11 @@ export default class MetamaskController extends EventEmitter { this.networkController, ), findNetworkConfigurationBy: this.findNetworkConfigurationBy.bind(this), - getNetworkClientIdForDomain: - this.selectedNetworkController.getNetworkClientIdForDomain.bind( - this.selectedNetworkController, - ), setNetworkClientIdForDomain: this.selectedNetworkController.setNetworkClientIdForDomain.bind( this.selectedNetworkController, ), - getUseRequestQueue: this.preferencesController.getUseRequestQueue.bind( - this.preferencesController, - ), getProviderConfig: () => this.networkController.state.providerConfig, setProviderType: (type) => { return this.networkController.setProviderType(type); @@ -5299,8 +5292,12 @@ export default class MetamaskController extends EventEmitter { Object.keys(this.connections).forEach((origin) => { Object.values(this.connections[origin]).forEach(async (conn) => { - if (conn.engine) { - conn.engine.emit('notification', await getPayload(origin)); + try { + if (conn.engine) { + conn.engine.emit('notification', await getPayload(origin)); + } + } catch (err) { + console.error(err); } }); }); @@ -5326,8 +5323,6 @@ export default class MetamaskController extends EventEmitter { return; } - // Ensure preferences + identities controller know about all addresses - this.preferencesController.syncAddresses(addresses); this.accountTracker.syncWithAddresses(addresses); } @@ -5564,6 +5559,14 @@ export default class MetamaskController extends EventEmitter { getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), getTransaction: (id) => this.txController.state.transactions.find((tx) => tx.id === id), + getIsSmartTransaction: () => { + return getIsSmartTransaction(this._getMetaMaskState()); + }, + getSmartTransactionByMinedTxHash: (txHash) => { + return this.smartTransactionsController.getSmartTransactionByMinedTxHash( + txHash, + ); + }, }; return { ...controllerActions, @@ -5579,6 +5582,17 @@ export default class MetamaskController extends EventEmitter { }; } + toggleExternalServices(useExternal) { + this.preferencesController.toggleExternalServices(useExternal); + if (useExternal) { + this.tokenDetectionController.enable(); + this.gasFeeController.enableNonRPCGasFeeApis(); + } else { + this.tokenDetectionController.disable(); + this.gasFeeController.disableNonRPCGasFeeApis(); + } + } + //============================================================================= // CONFIG //============================================================================= @@ -5658,6 +5672,7 @@ export default class MetamaskController extends EventEmitter { onClientClosed() { try { this.gasFeeController.stopAllPolling(); + this.currencyRateController.stopAllPolling(); this.appStateController.clearPollingTokens(); } catch (error) { console.error(error); @@ -5677,6 +5692,7 @@ export default class MetamaskController extends EventEmitter { this.appStateController.store.getState()[appStatePollingTokenType]; pollingTokensToDisconnect.forEach((pollingToken) => { this.gasFeeController.stopPollingByPollingToken(pollingToken); + this.currencyRateController.stopPollingByPollingToken(pollingToken); this.appStateController.removePollingToken( pollingToken, appStatePollingTokenType, @@ -5837,16 +5853,16 @@ export default class MetamaskController extends EventEmitter { this.permissionLogController.updateAccountsHistory(origin, newAccounts); } - _notifyChainChange() { + async _notifyChainChange() { if (this.preferencesController.getUseRequestQueue()) { - this.notifyAllConnections((origin) => ({ + this.notifyAllConnections(async (origin) => ({ method: NOTIFICATION_NAMES.chainChanged, - params: this.getProviderNetworkState(origin), + params: await this.getProviderNetworkState(origin), })); } else { this.notifyAllConnections({ method: NOTIFICATION_NAMES.chainChanged, - params: this.getProviderNetworkState(), + params: await this.getProviderNetworkState(), }); } } @@ -6006,10 +6022,34 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger.publish( 'TransactionController:transactionStatusUpdated', - { updatedTransactionMeta }, + { transactionMeta: updatedTransactionMeta }, ); } + _publishSmartTransactionHook(transactionMeta) { + const state = this._getMetaMaskState(); + const isSmartTransaction = getIsSmartTransaction(state); + if (!isSmartTransaction) { + // Will cause TransactionController to publish to the RPC provider as normal. + return { transactionHash: undefined }; + } + const featureFlags = getFeatureFlagsByChainId(state); + return submitSmartTransactionHook({ + transactionMeta, + transactionController: this.txController, + smartTransactionsController: this.smartTransactionsController, + controllerMessenger: this.controllerMessenger, + isSmartTransaction, + featureFlags, + }); + } + + _getMetaMaskState() { + return { + metamask: this.getState(), + }; + } + async #onPreferencesControllerStateChange(currentState, previousState) { const { currentLocale } = currentState; const { chainId } = this.networkController.state.providerConfig; diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 749d3ce66e70..a879f3f61add 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -31,7 +31,7 @@ import { flushPromises } from '../../test/lib/timer-helpers'; import { deferredPromise } from './lib/util'; import MetaMaskController from './metamask-controller'; -const { Ganache } = require('../../test/e2e/ganache'); +const { Ganache } = require('../../test/e2e/seeder/ganache'); const ganacheServer = new Ganache(); @@ -83,8 +83,15 @@ const createLoggerMiddlewareMock = () => (req, res, next) => { } next(); }; - jest.mock('./lib/createLoggerMiddleware', () => createLoggerMiddlewareMock); + +const rpcMethodMiddlewareMock = { + createMethodMiddleware: () => (_req, _res, next, _end) => { + next(); + }, +}; +jest.mock('./lib/rpc-method-middleware', () => rpcMethodMiddlewareMock); + jest.mock( './controllers/preferences', () => @@ -519,7 +526,6 @@ describe('MetaMaskController', () => { describe('#createNewVaultAndKeychain', () => { it('can only create new vault on keyringController once', async () => { - jest.spyOn(metamaskController, 'selectFirstAccount').mockReturnValue(); const password = 'a-fake-password'; const vault1 = await metamaskController.createNewVaultAndKeychain( @@ -695,47 +701,6 @@ describe('MetaMaskController', () => { }); }); - describe('#selectFirstAccount', () => { - let identities; - - beforeEach(async () => { - await metamaskController.keyringController.createNewVaultAndRestore( - 'password', - TEST_SEED, - ); - await metamaskController.addNewAccount(1); - await metamaskController.addNewAccount(2); - - identities = { - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { - TEST_ADDRESS, - name: 'Account 1', - }, - '0xc42edfcc21ed14dda456aa0756c153f7985d8813': { - TEST_ADDRESS_2, - name: 'Account 2', - }, - }; - metamaskController.preferencesController.store.updateState({ - identities, - }); - metamaskController.selectFirstAccount(); - }); - - it('changes preferences controller select address', () => { - const preferenceControllerState = - metamaskController.preferencesController.store.getState(); - expect(preferenceControllerState.selectedAddress).toStrictEqual( - TEST_ADDRESS, - ); - }); - - it('changes metamask controller selected address', () => { - const metamaskState = metamaskController.getState(); - expect(metamaskState.selectedAddress).toStrictEqual(TEST_ADDRESS); - }); - }); - describe('connectHardware', () => { it('should throw if it receives an unknown device name', async () => { const result = metamaskController.connectHardware( @@ -920,9 +885,6 @@ describe('MetaMaskController', () => { .mockResolvedValueOnce(['0x1']) .mockResolvedValueOnce(['0x2']) .mockResolvedValueOnce(['0x3']); - jest - .spyOn(metamaskController.preferencesController, 'setAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.preferencesController, 'setSelectedAddress') .mockReturnValue(); @@ -968,12 +930,6 @@ describe('MetaMaskController', () => { ).toHaveBeenCalledTimes(3); }); - it('should call preferencesController.setAddresses', async () => { - expect( - metamaskController.preferencesController.setAddresses, - ).toHaveBeenCalledTimes(1); - }); - it('should call preferencesController.setSelectedAddress', async () => { expect( metamaskController.preferencesController.setSelectedAddress, @@ -1033,15 +989,28 @@ describe('MetaMaskController', () => { .mockReturnValue({ address: selectedAddressMock }); jest.spyOn(metamaskController.txController, 'wipeTransactions'); + jest.spyOn( + metamaskController.smartTransactionsController, + 'wipeSmartTransactions', + ); await metamaskController.resetAccount(); expect( metamaskController.txController.wipeTransactions, ).toHaveBeenCalledTimes(1); + expect( + metamaskController.smartTransactionsController.wipeSmartTransactions, + ).toHaveBeenCalledTimes(1); expect( metamaskController.txController.wipeTransactions, - ).toHaveBeenCalledWith(true, selectedAddressMock); + ).toHaveBeenCalledWith(false, selectedAddressMock); + expect( + metamaskController.smartTransactionsController.wipeSmartTransactions, + ).toHaveBeenCalledWith({ + address: selectedAddressMock, + ignoreNetwork: false, + }); }); }); @@ -1269,9 +1238,6 @@ describe('MetaMaskController', () => { describe('#_onKeyringControllerUpdate', () => { it('should do nothing if there are no keyrings in state', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1279,9 +1245,6 @@ describe('MetaMaskController', () => { const oldState = metamaskController.getState(); await metamaskController._onKeyringControllerUpdate({ keyrings: [] }); - expect( - metamaskController.preferencesController.syncAddresses, - ).not.toHaveBeenCalled(); expect( metamaskController.accountTracker.syncWithAddresses, ).not.toHaveBeenCalled(); @@ -1289,9 +1252,6 @@ describe('MetaMaskController', () => { }); it('should sync addresses if there are keyrings in state', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1305,9 +1265,6 @@ describe('MetaMaskController', () => { ], }); - expect( - metamaskController.preferencesController.syncAddresses, - ).toHaveBeenCalledWith(['0x1', '0x2']); expect( metamaskController.accountTracker.syncWithAddresses, ).toHaveBeenCalledWith(['0x1', '0x2']); @@ -1315,9 +1272,6 @@ describe('MetaMaskController', () => { }); it('should NOT update selected address if already unlocked', async () => { - jest - .spyOn(metamaskController.preferencesController, 'syncAddresses') - .mockReturnValue(); jest .spyOn(metamaskController.accountTracker, 'syncWithAddresses') .mockReturnValue(); @@ -1332,9 +1286,6 @@ describe('MetaMaskController', () => { ], }); - expect( - metamaskController.preferencesController.syncAddresses, - ).toHaveBeenCalledWith(['0x1', '0x2']); expect( metamaskController.accountTracker.syncWithAddresses, ).toHaveBeenCalledWith(['0x1', '0x2']); diff --git a/app/scripts/migrations/081.ts b/app/scripts/migrations/081.ts index 0162a43b6e49..aa7e48a8c9fa 100644 --- a/app/scripts/migrations/081.ts +++ b/app/scripts/migrations/081.ts @@ -91,7 +91,7 @@ function transformState(state: Record) { // Adding the snap name to the wallet_snap permission's caveat value const snapId = permissionName.slice(snapPrefix.length); const caveat = ( - (updatedPermissions.wallet_snap as Record) + (updatedPermissions.wallet_snap as Record) .caveats as unknown[] )[0]; diff --git a/app/scripts/migrations/095.ts b/app/scripts/migrations/095.ts index 39128284e03f..6361bba388fc 100644 --- a/app/scripts/migrations/095.ts +++ b/app/scripts/migrations/095.ts @@ -35,7 +35,11 @@ function migrateData(state: Record): void { removeIncomingTransactionsControllerState(state); } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function moveIncomingTransactions(state: Record) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const incomingTransactions: Record = state.IncomingTransactionsController?.incomingTransactions || {}; @@ -46,6 +50,8 @@ function moveIncomingTransactions(state: Record) { const transactions = state.TransactionController?.transactions || {}; const updatedTransactions = Object.values(incomingTransactions).reduce( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (result: Record, tx: any) => { result[tx.id] = tx; return result; @@ -59,7 +65,11 @@ function moveIncomingTransactions(state: Record) { }; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function generateLastFetchedBlockNumbers(state: Record) { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const incomingTransactions: Record = state.IncomingTransactionsController?.incomingTransactions || {}; diff --git a/app/scripts/migrations/096.ts b/app/scripts/migrations/096.ts index 749956a726b2..b04b7613e008 100644 --- a/app/scripts/migrations/096.ts +++ b/app/scripts/migrations/096.ts @@ -4,6 +4,8 @@ import { CHAIN_IDS } from '../../../shared/constants/network'; type VersionedData = { meta: { version: number }; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any data: Record; }; @@ -34,6 +36,8 @@ export async function migrate( } type NetworkConfiguration = { + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any chainId: Record; }; @@ -48,10 +52,18 @@ function transformState(state: Record) { return state; } const { PreferencesController, NetworkController } = state; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { featureFlags }: Record = PreferencesController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { showIncomingTransactions }: any = featureFlags; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const { networkConfigurations }: Record = NetworkController; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const addedNetwork: Record[] = Object.values(networkConfigurations).map( (network) => network.chainId, @@ -63,6 +75,8 @@ function transformState(state: Record) { CHAIN_IDS.SEPOLIA, CHAIN_IDS.LINEA_GOERLI, ]; + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const allSavedNetworks: Record = [ ...mainNetworks, ...addedNetwork, diff --git a/app/scripts/migrations/097.ts b/app/scripts/migrations/097.ts index 9e99cf61b40a..56a11679449d 100644 --- a/app/scripts/migrations/097.ts +++ b/app/scripts/migrations/097.ts @@ -21,6 +21,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const transactionControllerState = state?.TransactionController || {}; const transactions = transactionControllerState?.transactions || {}; diff --git a/app/scripts/migrations/098.ts b/app/scripts/migrations/098.ts index 3085827b4c6a..2fe77148e052 100644 --- a/app/scripts/migrations/098.ts +++ b/app/scripts/migrations/098.ts @@ -25,6 +25,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const transactionControllerState = state?.TransactionController || {}; const transactions = transactionControllerState?.transactions || {}; diff --git a/app/scripts/migrations/099.test.ts b/app/scripts/migrations/099.test.ts index 1feba98e6fa3..3b98f4f72a26 100644 --- a/app/scripts/migrations/099.test.ts +++ b/app/scripts/migrations/099.test.ts @@ -72,6 +72,8 @@ describe('migration #99', () => { const newStorage = await migrate(oldStorage); + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any const migratedTransactions = (newStorage.data.TransactionController as any) .transactions; diff --git a/app/scripts/migrations/099.ts b/app/scripts/migrations/099.ts index 9464d19a64df..38018c97919d 100644 --- a/app/scripts/migrations/099.ts +++ b/app/scripts/migrations/099.ts @@ -24,6 +24,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const transactionControllerState = state?.TransactionController || {}; const transactions = transactionControllerState?.transactions || {}; @@ -33,6 +35,8 @@ function transformState(state: Record) { } const newTxs = Object.keys(transactions).reduce( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (txs: { [key: string]: any }, oldTransactionId) => { // Clone the transaction const transaction = cloneDeep(transactions[oldTransactionId]); diff --git a/app/scripts/migrations/100.ts b/app/scripts/migrations/100.ts index c9b4a99afceb..89dbe0d5670d 100644 --- a/app/scripts/migrations/100.ts +++ b/app/scripts/migrations/100.ts @@ -25,6 +25,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const addressBook = state?.AddressBookController?.addressBook ?? {}; const names = state?.NameController?.names?.ethereumAddress ?? {}; diff --git a/app/scripts/migrations/102.ts b/app/scripts/migrations/102.ts index 820e67605251..a1fde4f27f7f 100644 --- a/app/scripts/migrations/102.ts +++ b/app/scripts/migrations/102.ts @@ -23,6 +23,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const transactionControllerState = state?.TransactionController || {}; const transactions = transactionControllerState?.transactions || {}; @@ -32,6 +34,8 @@ function transformState(state: Record) { } const newTxs = Object.keys(transactions).reduce( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (txs: { [key: string]: any }, txId) => { // Clone the transaction const transaction = cloneDeep(transactions[txId]); diff --git a/app/scripts/migrations/104.ts b/app/scripts/migrations/104.ts index 340d167ccd5f..38ab3c0f57c8 100644 --- a/app/scripts/migrations/104.ts +++ b/app/scripts/migrations/104.ts @@ -22,6 +22,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const transactionControllerState = state?.TransactionController; @@ -32,6 +34,8 @@ function transformState(state: Record) { const transactionsObject = transactionControllerState?.transactions || {}; const transactionsArray = Object.values(transactionsObject).sort( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any (a: any, b: any) => (a.time > b.time ? -1 : 1), // Descending ); diff --git a/app/scripts/migrations/105.test.ts b/app/scripts/migrations/105.test.ts index c25659b641c9..a57c422c8992 100644 --- a/app/scripts/migrations/105.test.ts +++ b/app/scripts/migrations/105.test.ts @@ -73,7 +73,14 @@ function expectedInternalAccount( lastSelected: lastSelected ? expect.any(Number) : undefined, }, options: {}, - methods: [...Object.values(EthMethod)], + methods: [ + EthMethod.Sign, + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ], type: 'eip155:eoa', }; } @@ -94,7 +101,7 @@ function createMockState( describe('migration #105', () => { it('updates the version metadata', async () => { const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: createMockState(), }; @@ -108,7 +115,7 @@ describe('migration #105', () => { const oldData = createMockState(); const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; @@ -139,7 +146,7 @@ describe('migration #105', () => { const oldData = createMockState(); const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; @@ -151,7 +158,7 @@ describe('migration #105', () => { accounts: { [expectedUUID]: expectedInternalAccount( MOCK_ADDRESS, - `Account 1`, + 'Account 1', ), }, selectedAccount: expectedUUID, @@ -168,7 +175,7 @@ describe('migration #105', () => { ]), ); const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; const newStorage = await migrate(oldStorage); @@ -179,7 +186,7 @@ describe('migration #105', () => { accounts: { [expectedUUID]: expectedInternalAccount( MOCK_ADDRESS, - `a random name`, + 'a random name', ), }, selectedAccount: expectedUUID, @@ -198,7 +205,7 @@ describe('migration #105', () => { }); const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; @@ -210,11 +217,11 @@ describe('migration #105', () => { accounts: { [expectedUUID]: expectedInternalAccount( MOCK_ADDRESS, - `Account 1`, + 'Account 1', ), [expectedUUID2]: expectedInternalAccount( MOCK_ADDRESS_2, - `Account 2`, + 'Account 2', ), }, selectedAccount: expectedUUID, @@ -226,10 +233,14 @@ describe('migration #105', () => { }); describe('createSelectedAccountForAccountsController', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('should select the same account as the selected address', async () => { const oldData = createMockState(); const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; const newStorage = await migrate(oldStorage); @@ -252,7 +263,7 @@ describe('migration #105', () => { }, }; const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; const newStorage = await migrate(oldStorage); @@ -275,7 +286,7 @@ describe('migration #105', () => { }, }; const oldStorage = { - meta: { version: 103 }, + meta: { version: 104 }, data: oldData, }; await migrate(oldStorage); @@ -285,5 +296,41 @@ describe('migration #105', () => { new Error(`state.PreferencesController?.selectedAddress is undefined`), ); }); + + it('recovers from invalid selectedAddress state', async () => { + const expectedUUID = addressToUUID(MOCK_ADDRESS); + + const oldData = { + PreferencesController: { + identities: { + [MOCK_ADDRESS]: { name: 'Account 1', address: MOCK_ADDRESS }, + }, + selectedAddress: undefined, + }, + }; + const oldStorage = { + meta: { version: 104 }, + data: oldData, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toStrictEqual({ + PreferencesController: expect.objectContaining({ + selectedAddress: MOCK_ADDRESS, + }), + AccountsController: { + internalAccounts: { + accounts: { + [expectedUUID]: expectedInternalAccount( + MOCK_ADDRESS, + 'Account 1', + ), + }, + selectedAccount: expectedUUID, + }, + }, + }); + }); }); }); diff --git a/app/scripts/migrations/105.ts b/app/scripts/migrations/105.ts index 10be55e69e0a..8640d9c6c645 100644 --- a/app/scripts/migrations/105.ts +++ b/app/scripts/migrations/105.ts @@ -48,6 +48,22 @@ function migrateData(state: Record): void { createSelectedAccountForAccountsController(state); } +function findInternalAccountByAddress( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: Record, + address: string, +): InternalAccount | undefined { + return Object.values( + state.AccountsController.internalAccounts.accounts, + ).find( + (account: InternalAccount) => + account.address.toLowerCase() === address.toLowerCase(), + ); +} + +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function createDefaultAccountsController(state: Record) { state.AccountsController = { internalAccounts: { @@ -58,6 +74,8 @@ function createDefaultAccountsController(state: Record) { } function createInternalAccountsForAccountsController( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record, ) { const identities: { @@ -89,7 +107,14 @@ function createInternalAccountsForAccountsController( type: 'HD Key Tree', }, }, - methods: [...Object.values(EthMethod)], + methods: [ + EthMethod.Sign, + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ], type: EthAccountType.Eoa, }; }); @@ -97,10 +122,23 @@ function createInternalAccountsForAccountsController( state.AccountsController.internalAccounts.accounts = accounts; } +function getFirstAddress( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: Record, +) { + const [firstAddress] = Object.keys( + state.PreferencesController?.identities || {}, + ); + return firstAddress; +} + function createSelectedAccountForAccountsController( + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record, ) { - const selectedAddress = state.PreferencesController?.selectedAddress; + let selectedAddress = state.PreferencesController?.selectedAddress; if (typeof selectedAddress !== 'string') { global.sentry?.captureException?.( @@ -108,18 +146,18 @@ function createSelectedAccountForAccountsController( `state.PreferencesController?.selectedAddress is ${selectedAddress}`, ), ); - } - const selectedAccount = Object.values( - state.AccountsController.internalAccounts.accounts, - ).find((account: InternalAccount) => { - return account.address.toLowerCase() === selectedAddress.toLowerCase(); - }) as InternalAccount; + // Get the first account if selectedAddress is not a string + selectedAddress = getFirstAddress(state); + } + const selectedAccount = findInternalAccountByAddress(state, selectedAddress); if (selectedAccount) { + // Required in case there was no address selected + state.PreferencesController.selectedAddress = selectedAccount.address; state.AccountsController.internalAccounts = { ...state.AccountsController.internalAccounts, - selectedAccount: selectedAccount.id ?? '', + selectedAccount: selectedAccount.id, }; } } diff --git a/app/scripts/migrations/108.ts b/app/scripts/migrations/108.ts index 1ea75957b73a..4b12de5f9004 100644 --- a/app/scripts/migrations/108.ts +++ b/app/scripts/migrations/108.ts @@ -27,6 +27,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const addressBook = state?.AddressBookController?.addressBook ?? {}; const names = state?.NameController?.names?.ethereumAddress ?? {}; diff --git a/app/scripts/migrations/109.ts b/app/scripts/migrations/109.ts index 5c40b07d4b09..13e268b8e061 100644 --- a/app/scripts/migrations/109.ts +++ b/app/scripts/migrations/109.ts @@ -27,6 +27,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const identities: PreferencesControllerState['identities'] = state?.PreferencesController?.identities ?? {}; diff --git a/app/scripts/migrations/110.ts b/app/scripts/migrations/110.ts index bc941ac87695..7a677ed8b9cd 100644 --- a/app/scripts/migrations/110.ts +++ b/app/scripts/migrations/110.ts @@ -35,6 +35,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { const NetworkController = state?.NetworkController || {}; const provider = NetworkController?.providerConfig || {}; diff --git a/app/scripts/migrations/111.ts b/app/scripts/migrations/111.ts index a45c24fa168f..1a06e655cabb 100644 --- a/app/scripts/migrations/111.ts +++ b/app/scripts/migrations/111.ts @@ -28,6 +28,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { if (!hasProperty(state, 'SelectedNetworkController')) { return state; diff --git a/app/scripts/migrations/112.ts b/app/scripts/migrations/112.ts index 6313462be199..519be68c9ca3 100644 --- a/app/scripts/migrations/112.ts +++ b/app/scripts/migrations/112.ts @@ -26,6 +26,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { if (!hasProperty(state, 'SelectedNetworkController')) { return state; diff --git a/app/scripts/migrations/114.ts b/app/scripts/migrations/114.ts index 2288dc84d612..bed9c70a8850 100644 --- a/app/scripts/migrations/114.ts +++ b/app/scripts/migrations/114.ts @@ -26,6 +26,8 @@ export async function migrate( return versionedData; } +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function transformState(state: Record) { if (!hasProperty(state, 'PreferencesController')) { return state; diff --git a/app/scripts/migrations/115.test.ts b/app/scripts/migrations/115.test.ts new file mode 100644 index 000000000000..19599c42320d --- /dev/null +++ b/app/scripts/migrations/115.test.ts @@ -0,0 +1,194 @@ +import { NetworkType } from '@metamask/controller-utils'; +import { + CHAIN_IDS, + CHAIN_ID_TO_RPC_URL_MAP, + LINEA_SEPOLIA_DISPLAY_NAME, + NETWORK_TYPES, + TEST_NETWORK_TICKER_MAP, +} from '../../../shared/constants/network'; +import { migrate, version } from './115'; + +const oldVersion = 114; + +const ethereumProviderConfig = { + chainId: '0x1', + rpcPrefs: { + blockExplorerUrl: 'https://etherscan.io', + }, + ticker: 'ETH', + type: 'mainnet', +}; + +const ethereumNetworksMetadata = { + mainnet: { + EIPS: { + '1559': true, + }, + status: 'available', + }, +}; +const ethereumOldState = { + CurrencyController: { + currencyRates: { + ETH: { + conversionDate: 1708532473.416, + conversionRate: 2918.02, + usdConversionRate: 2918.02, + }, + GoerliETH: { + conversionDate: 1708532466.732, + conversionRate: 2918.02, + usdConversionRate: 2918.02, + }, + }, + currentCurrency: 'usd', + }, + NetworkController: { + networkConfigurations: {}, + networksMetadata: ethereumNetworksMetadata, + providerConfig: ethereumProviderConfig, + selectedNetworkClientId: 'mainnet', + }, +}; + +const lineaGoerliState = { + NetworkController: { + networkConfigurations: {}, + networksMetadata: { + 'linea-goerli': { + EIPS: { + '1559': true, + }, + status: 'available', + }, + }, + providerConfig: { + chainId: CHAIN_IDS.LINEA_GOERLI, + rpcPrefs: {}, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_GOERLI], + type: NETWORK_TYPES.LINEA_GOERLI, + }, + selectedNetworkClientId: NETWORK_TYPES.LINEA_GOERLI, + }, +}; + +describe('migration #115', () => { + 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 state', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('Should return state if chainId is not linea-goerli', async () => { + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: ethereumOldState, + }); + + expect(transformedState.data).toEqual(ethereumOldState); + }); + + it('Should return state if there is no NetworkController in state', async () => { + const { NetworkController, ...state } = ethereumOldState; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: state, + }); + + expect(transformedState.data).toEqual(state); + }); + + it('Should return state if there is no provider in NetworkController', async () => { + const state = { + ...ethereumOldState, + NetworkController: {}, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: state, + }); + + expect(transformedState.data).toEqual(state); + }); + + it('Should return state if there is no chainId in provider in NetworkController', async () => { + const state = { + ...ethereumOldState, + NetworkController: { + providerConfig: {}, + }, + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: state, + }); + + expect(transformedState.data).toEqual(state); + }); + + it('Should return state if chainId is not linea-goerli', async () => { + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: ethereumOldState, + }); + + expect(transformedState.data).toEqual(ethereumOldState); + }); + + it('Should update NetworkController to Linea Sepolia if chainId is on Linea Goerli', async () => { + const expectedNetworkControllerState = { + networkConfigurations: {}, + networksMetadata: { + 'linea-sepolia': { + EIPS: { + '1559': true, + }, + status: 'available', + }, + 'linea-goerli': { + EIPS: { + '1559': true, + }, + status: 'available', + }, + }, + providerConfig: { + type: NetworkType['linea-sepolia'], + rpcPrefs: {}, + chainId: CHAIN_IDS.LINEA_SEPOLIA, + nickname: LINEA_SEPOLIA_DISPLAY_NAME, + rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.LINEA_SEPOLIA], + providerType: NETWORK_TYPES.LINEA_SEPOLIA, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_SEPOLIA], + id: NETWORK_TYPES.LINEA_SEPOLIA, + }, + selectedNetworkClientId: 'linea-sepolia', + }; + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: lineaGoerliState, + }); + + expect(transformedState.data).toEqual({ + NetworkController: expectedNetworkControllerState, + }); + }); +}); diff --git a/app/scripts/migrations/115.ts b/app/scripts/migrations/115.ts new file mode 100644 index 000000000000..d0d582ab6d04 --- /dev/null +++ b/app/scripts/migrations/115.ts @@ -0,0 +1,81 @@ +import { cloneDeep, isObject } from 'lodash'; +import { NetworkType } from '@metamask/controller-utils'; +import { hasProperty } from '@metamask/utils'; +import { NetworkStatus } from '@metamask/network-controller'; +import { + CHAIN_IDS, + CHAIN_ID_TO_RPC_URL_MAP, + NETWORK_TYPES, + TEST_NETWORK_TICKER_MAP, + LINEA_SEPOLIA_DISPLAY_NAME, +} from '../../../shared/constants/network'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 115; + +/** + * Migrates the user network to Linea Sepolia if the user is on Linea Goerli network. + * + * @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 NetworkController = state?.NetworkController || {}; + const provider = NetworkController?.providerConfig || {}; + + if (provider?.chainId !== CHAIN_IDS.LINEA_GOERLI) { + return state; + } + const networkControllerState = state.NetworkController; + + if ( + hasProperty(state, 'NetworkController') && + isObject(state.NetworkController) && + hasProperty(state.NetworkController, 'providerConfig') && + isObject(state.NetworkController.providerConfig) && + hasProperty(state.NetworkController.providerConfig, 'chainId') && + state.NetworkController.providerConfig.chainId === CHAIN_IDS.LINEA_GOERLI + ) { + networkControllerState.providerConfig = { + type: NetworkType['linea-sepolia'], + rpcPrefs: {}, + chainId: CHAIN_IDS.LINEA_SEPOLIA, + nickname: LINEA_SEPOLIA_DISPLAY_NAME, + rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.LINEA_SEPOLIA], + providerType: NETWORK_TYPES.LINEA_SEPOLIA, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.LINEA_SEPOLIA], + id: NETWORK_TYPES.LINEA_SEPOLIA, + }; + networkControllerState.selectedNetworkClientId = + NETWORK_TYPES.LINEA_SEPOLIA; + networkControllerState.networksMetadata = { + ...networkControllerState.networksMetadata, + 'linea-sepolia': { + EIPS: { + '1559': true, + }, + status: NetworkStatus.Available, + }, + }; + } + return { + ...state, + NetworkController: networkControllerState, + }; +} diff --git a/app/scripts/migrations/116.test.ts b/app/scripts/migrations/116.test.ts new file mode 100644 index 000000000000..6b3d8095dbd8 --- /dev/null +++ b/app/scripts/migrations/116.test.ts @@ -0,0 +1,66 @@ +import { migrate, version } from './116'; + +const oldVersion = 115; + +describe('migration #79', () => { + it('should update the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('should remove the "showProductTour"', async () => { + const oldStorage = { + meta: { + version: oldVersion, + }, + data: { + AppStateController: { + showProductTour: false, + bar: 'baz', + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 116, + }, + data: { + AppStateController: { + bar: 'baz', + }, + }, + }); + }); + + it('should make no changes if "showProductTour" never existed', async () => { + const oldStorage = { + meta: { + version: oldVersion, + }, + data: { + AppStateController: { + bar: 'baz', + }, + }, + }; + + const newStorage = await migrate(oldStorage); + expect(newStorage).toStrictEqual({ + meta: { + version: 116, + }, + data: { + AppStateController: { + bar: 'baz', + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/116.ts b/app/scripts/migrations/116.ts new file mode 100644 index 000000000000..69dc8ba3bf1c --- /dev/null +++ b/app/scripts/migrations/116.ts @@ -0,0 +1,41 @@ +import { cloneDeep, isObject } from 'lodash'; +import { hasProperty } from '@metamask/utils'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 116; + +/** + * As we have removed Product tour from Home Page so this migration is to remove showProductTour from AppState + * + * @param originalVersionedData + */ +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 AppStateController = state?.AppStateController || {}; + + if ( + hasProperty(state, 'AppStateController') && + isObject(state.AppStateController) && + hasProperty(state.AppStateController, 'showProductTour') && + state.AppStateController.showProductTour !== undefined + ) { + delete AppStateController.showProductTour; + } + + return { + ...state, + AppStateController, + }; +} diff --git a/app/scripts/migrations/117.test.ts b/app/scripts/migrations/117.test.ts new file mode 100644 index 000000000000..234dd60f90bc --- /dev/null +++ b/app/scripts/migrations/117.test.ts @@ -0,0 +1,104 @@ +import { TransactionStatus } from '@metamask/transaction-controller'; +import { migrate, version, StuckTransactionError, TARGET_DATE } from './117'; + +const oldVersion = 116; + +const TRANSACTIONS_MOCK = [ + { id: 'tx1', time: TARGET_DATE - 1000, status: 'approved' }, // Before target date, should be marked as failed + { id: 'tx2', time: TARGET_DATE + 1000, status: 'approved' }, // After target date, should remain unchanged + { id: 'tx3', time: TARGET_DATE - 1000, status: 'signed' }, // Before target date, should be marked as failed + { id: 'tx4', time: TARGET_DATE - 1000, status: 'confirmed' }, // Before target date but not approved/signed, should remain unchanged +]; + +describe('migration #117', () => { + 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('handles missing TransactionController', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('handles empty transactions', async () => { + const oldState = { + TransactionController: { + transactions: [], + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('handles missing state', async () => { + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: {}, + }); + + expect(transformedState.data).toEqual({}); + }); + + it('marks the transactions as failed before December 8, 2023, if they are approved or signed', async () => { + const oldState = { + TransactionController: { + transactions: TRANSACTIONS_MOCK, + }, + }; + const oldStorage = { + meta: { version: oldVersion }, + data: oldState, + }; + + const newStorage = await migrate(oldStorage); + + // Expected modifications to the transactions based on the migration logic + const expectedTransactions = [ + { + ...TRANSACTIONS_MOCK[0], // Assuming tx1 is the first element + status: TransactionStatus.failed, + error: StuckTransactionError, + }, + TRANSACTIONS_MOCK[1], // Assuming tx2 remains unchanged + { + ...TRANSACTIONS_MOCK[2], // Assuming tx3 is the third element + status: TransactionStatus.failed, + error: StuckTransactionError, + }, + TRANSACTIONS_MOCK[3], // Assuming tx4 and any others remain unchanged + // Add more transactions if there are more than four in TRANSACTIONS_MOCK + ]; + + expect(newStorage.data).toEqual({ + TransactionController: { + transactions: expectedTransactions, + }, + }); + }); +}); diff --git a/app/scripts/migrations/117.ts b/app/scripts/migrations/117.ts new file mode 100644 index 000000000000..a84cdbbb32b3 --- /dev/null +++ b/app/scripts/migrations/117.ts @@ -0,0 +1,62 @@ +import { cloneDeep } from 'lodash'; +import { + TransactionMeta, + TransactionStatus, + TransactionError, +} from '@metamask/transaction-controller'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 117; + +// Target date is December 8, 2023 - 00:00:00 UTC +export const TARGET_DATE = new Date('2023-12-08T00:00:00Z').getTime(); + +const STUCK_STATES = [TransactionStatus.approved, TransactionStatus.signed]; + +type FailedTransactionMeta = TransactionMeta & { + status: TransactionStatus.failed; + error: TransactionError; +}; + +export const StuckTransactionError = { + name: 'StuckTransactionDueToStatus', + message: 'Transaction is stuck due to status - migration 116', +}; + +/** + * This migration sets the `status` to `failed` for all transactions created before December 8, 2023 that are still `approved` or `signed`. + * + * @param originalVersionedData + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +// TODO: Replace `any` with type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function transformState(state: Record) { + const transactions: TransactionMeta[] = + state?.TransactionController?.transactions ?? []; + + for (const transaction of transactions) { + if ( + transaction.time < TARGET_DATE && + STUCK_STATES.includes(transaction.status) + ) { + transaction.status = TransactionStatus.failed; + + const failedTransaction = transaction as FailedTransactionMeta; + + failedTransaction.error = StuckTransactionError; + } + } +} diff --git a/app/scripts/migrations/118.test.ts b/app/scripts/migrations/118.test.ts new file mode 100644 index 000000000000..5c8db790961f --- /dev/null +++ b/app/scripts/migrations/118.test.ts @@ -0,0 +1,131 @@ +import { migrate, version } from './118'; + +const sentryCaptureExceptionMock = jest.fn(); + +global.sentry = { + captureException: sentryCaptureExceptionMock, +}; + +describe('migration #118', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: 117 }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('does nothing if SelectedNetworkController is not present', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: 117 }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('removes domains with npm: or local: prefixes and preserves other domains', async () => { + const oldState = { + SelectedNetworkController: { + domains: { + 'npm:package': 'network1', + 'local:development': 'network2', + otherDomain: 'network3', + }, + }, + }; + + const expectedState = { + SelectedNetworkController: { + domains: { + otherDomain: 'network3', + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: 117 }, + data: oldState, + }); + + expect(transformedState.data).toEqual(expectedState); + }); + + it('keeps the domains unchanged if there are no npm: or local: prefixes', async () => { + const oldState = { + SelectedNetworkController: { + domains: { + someDomain: 'network1', + anotherDomain: 'network2', + }, + }, + }; + + const expectedState = { + SelectedNetworkController: { + domains: { + someDomain: 'network1', + anotherDomain: 'network2', + }, + }, + }; + + const transformedState = await migrate({ + meta: { version: 117 }, + data: oldState, + }); + + expect(transformedState.data).toEqual(expectedState); + }); + + it('should capture an exception if SelectedNetworkController is in state but is not an object', async () => { + const oldData = { + SelectedNetworkController: 'not an object', + }; + const oldStorage = { + meta: { + version: 117, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error('SelectedNetworkController is not an object.'), + ); + }); + + it('should capture an exception if SelectedNetworkController has domains but it is not an object', async () => { + const oldData = { + SelectedNetworkController: { + domains: 'not an object', + }, + }; + const oldStorage = { + meta: { + version: 117, + }, + data: oldData, + }; + + await migrate(oldStorage); + + expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1); + expect(sentryCaptureExceptionMock).toHaveBeenCalledWith( + new Error('Domains state is not an object.'), + ); + }); +}); diff --git a/app/scripts/migrations/118.ts b/app/scripts/migrations/118.ts new file mode 100644 index 000000000000..518b6f661e1a --- /dev/null +++ b/app/scripts/migrations/118.ts @@ -0,0 +1,75 @@ +import { cloneDeep } from 'lodash'; +import log from 'loglevel'; +import { hasProperty, isObject } from '@metamask/utils'; + +export const version = 118; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +/** + * Removes all Snaps domains (identified as starting with 'npm:' or 'local:') from the SelectedNetworkController's domains 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; +} + +/** + * Removes all domains starting with 'npm:' or 'local:' from the SelectedNetworkController's domains state. + * + * @param state - The entire state object of the MetaMask extension. + */ +function transformState(state: Record) { + const selectedNetworkControllerState = state.SelectedNetworkController; + if (!selectedNetworkControllerState) { + log.warn('Skipping migration. SelectedNetworkController state not found.'); + return; + } + + if (!isObject(selectedNetworkControllerState)) { + global.sentry?.captureException?.( + new Error('SelectedNetworkController is not an object.'), + ); + return; + } + + if (!hasProperty(selectedNetworkControllerState, 'domains')) { + global.sentry?.captureException?.( + new Error('Domains key is missing in SelectedNetworkController state.'), + ); + return; + } + + if (!isObject(selectedNetworkControllerState.domains)) { + global.sentry?.captureException?.( + new Error('Domains state is not an object.'), + ); + return; + } + + const { domains } = selectedNetworkControllerState; + const filteredDomains = Object.keys(domains).reduce>( + (acc, domain) => { + if (!domain.startsWith('npm:') && !domain.startsWith('local:')) { + acc[domain] = domains[domain]; + } + return acc; + }, + {}, + ); + + selectedNetworkControllerState.domains = filteredDomains; +} diff --git a/app/scripts/migrations/119.test.ts b/app/scripts/migrations/119.test.ts new file mode 100644 index 000000000000..40d374c8001a --- /dev/null +++ b/app/scripts/migrations/119.test.ts @@ -0,0 +1,66 @@ +import { migrate, version } from './119'; + +const oldVersion = 118; + +describe('migration #119', () => { + 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 useRequestQueue to true in PreferencesController', () => { + it('sets useRequestQueue to true', async () => { + const oldStorage = { + PreferencesController: { + useRequestQueue: false, + }, + }; + + const expectedState = { + PreferencesController: { + useRequestQueue: true, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldStorage, + }); + + expect(transformedState.data).toEqual(expectedState); + }); + + it('should not update useRequestQueue value if it was set true in initial state', async () => { + const oldStorage = { + PreferencesController: { + useRequestQueue: true, + }, + }; + + const expectedState = { + PreferencesController: { + useRequestQueue: true, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldStorage, + }); + + expect(transformedState.data).toEqual(expectedState); + }); + }); +}); diff --git a/app/scripts/migrations/119.ts b/app/scripts/migrations/119.ts new file mode 100644 index 000000000000..cacd97f28059 --- /dev/null +++ b/app/scripts/migrations/119.ts @@ -0,0 +1,52 @@ +import { cloneDeep } from 'lodash'; +import { hasProperty, isObject } from '@metamask/utils'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 119; + +/** + * This migration sets preference useRequestQueue 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 state; + } + + if (!isObject(state.PreferencesController)) { + const controllerType = typeof state.PreferencesController; + global.sentry?.captureException?.( + new Error(`state.PreferencesController is type: ${controllerType}`), + ); + state.PreferencesController = {}; + } + + if ( + state.PreferencesController.useRequestQueue === false || + state.PreferencesController.useRequestQueue === undefined + ) { + state.PreferencesController.useRequestQueue = true; + } + + return state; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 85c95bbb1695..bcd4fef72698 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -125,6 +125,11 @@ const migrations = [ require('./112'), require('./113'), require('./114'), + require('./115'), + require('./116'), + require('./117'), + require('./118'), + require('./119'), ]; export default migrations; diff --git a/app/scripts/snaps/preinstalled-snaps.ts b/app/scripts/snaps/preinstalled-snaps.ts new file mode 100644 index 000000000000..0a014c350c21 --- /dev/null +++ b/app/scripts/snaps/preinstalled-snaps.ts @@ -0,0 +1,8 @@ +import type { PreinstalledSnap } from '@metamask/snaps-controllers'; +import MessageSigningSnap from '@metamask/message-signing-snap/dist/preinstalled-snap.json'; + +const PREINSTALLED_SNAPS: readonly PreinstalledSnap[] = Object.freeze([ + MessageSigningSnap as PreinstalledSnap, +]); + +export default PREINSTALLED_SNAPS; diff --git a/app/trezor-usb-permissions.html b/app/trezor-usb-permissions.html index b3c6ce72b11c..8c92552cfa02 100644 --- a/app/trezor-usb-permissions.html +++ b/app/trezor-usb-permissions.html @@ -1,9 +1,11 @@ - - + <% if (it.shouldIncludeSnow) { %> + + + <% } %> TrezorConnect | Trezor @@ -31,5 +33,5 @@ - + diff --git a/builds.yml b/builds.yml index bf949e39bc78..c70cdd6be3fa 100644 --- a/builds.yml +++ b/builds.yml @@ -27,7 +27,7 @@ buildTypes: - SEGMENT_WRITE_KEY_REF: SEGMENT_PROD_WRITE_KEY - ALLOW_LOCAL_SNAPS: false - REQUIRE_SNAPS_ALLOWLIST: true - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/5.0.3/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.0.2/index.html - ACCOUNT_SNAPS_DIRECTORY_URL: https://snaps.metamask.io/account-management # Main build uses the default browser manifest manifestOverrides: false @@ -64,12 +64,13 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/5.0.3/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.0.2/index.html - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - 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' isPrerelease: true manifestOverrides: ./app/build-types/flask/manifest/ buildNameOverride: MetaMask Flask @@ -85,7 +86,7 @@ buildTypes: - SEGMENT_FLASK_WRITE_KEY - ALLOW_LOCAL_SNAPS: true - REQUIRE_SNAPS_ALLOWLIST: false - - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/5.0.3/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.0.2/index.html - SUPPORT_LINK: https://metamask-flask.zendesk.com/hc - SUPPORT_REQUEST_LINK: https://metamask-flask.zendesk.com/hc/en-us/requests/new - INFURA_ENV_KEY_REF: INFURA_FLASK_PROJECT_ID @@ -108,7 +109,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/5.0.3/index.html + - IFRAME_EXECUTION_ENVIRONMENT_URL: https://execution.metamask.io/iframe/6.0.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 @@ -144,7 +145,6 @@ features: env: - BLOCKAID_FILE_CDN: static.cx.metamask.io/api/v1/confirmations/ppom - BLOCKAID_PUBLIC_KEY: 066ad3e8af5583385e312c156d238055215d5f25247c1e91055afa756cb98a88 - conf-redesign: ### # Build Type code extensions. Things like different support links, warning pages, banners ### @@ -192,12 +192,38 @@ env: # TODO(ritave): Move ManifestV3 into a feature? - ENABLE_MV3: false # These are exclusively used for MV3 + - USE_SNOW - APPLY_LAVAMOAT - FILE_NAMES # This variable is read by Trezor's source and breaks build if not included - ASSET_PREFIX: null + - SENTRY_MMI_DSN: '' + + ### + # Notifications Feature + ### + - AUTH_API: https://authentication.api.cx.metamask.io + - OIDC_API: https://oidc.api.cx.metamask.io + - OIDC_CLIENT_ID: 1132f10a-b4e5-4390-a5f2-d9c6022db564 + - OIDC_GRANT_TYPE: urn:ietf:params:oauth:grant-type:jwt-bearer + - USER_STORAGE_API: https://user-storage.api.cx.metamask.io + - CONTENTFUL_ACCESS_SPACE_ID: null + - CONTENTFUL_ACCESS_TOKEN: null + - NOTIFICATIONS_SERVICE_URL: https://notification.api.cx.metamask.io + - TRIGGERS_SERVICE_URL: https://trigger.api.cx.metamask.io + - PUSH_NOTIFICATIONS_SERVICE_URL: https://push.api.cx.metamask.io + - VAPID_KEY: null + - FIREBASE_API_KEY: null + - FIREBASE_AUTH_DOMAIN: null + - FIREBASE_STORAGE_BUCKET: null + - FIREBASE_PROJECT_ID: null + - FIREBASE_MESSAGING_SENDER_ID: null + - FIREBASE_APP_ID: null + - FIREBASE_MEASUREMENT_ID: null + - __FIREBASE_DEFAULTS__: null + ### # API keys to 3rd party services ### @@ -222,6 +248,10 @@ env: # Variables that are modified with hardcoded code ### + # Used to enable confirmation redesigned pages + - ENABLE_CONFIRMATION_REDESIGN: '' + # Determines if feature flagged Settings Page - Developer Options should be used + - ENABLE_SETTINGS_PAGE_DEV_OPTIONS: false # Used for debugging changes to the phishing warning page. # Modified in /development/build/scripts.js:@getPhishingWarningPageUrl - PHISHING_WARNING_PAGE_URL: null @@ -263,6 +293,11 @@ env: - MULTICHAIN: '' # Determines if feature flagged Multichain Transactions should be used - TRANSACTION_MULTICHAIN: '' + # Enables use of test gas fee flow to debug gas fee estimation + - TEST_GAS_FEE_FLOWS: false + + # Enables the notifications feature within the build: + - NOTIFICATIONS: '' ### # Meta variables @@ -275,4 +310,4 @@ env: # Account Abstraction (EIP-4337) ### - - EIP_4337_ENTRYPOINT: null + - EIP_4337_ENTRYPOINT: null \ No newline at end of file diff --git a/development/build/index.js b/development/build/index.js index cc89cb6ad1e6..fbd828993c82 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -26,7 +26,12 @@ const createScriptTasks = require('./scripts'); const createStyleTasks = require('./styles'); const createStaticAssetTasks = require('./static'); const createEtcTasks = require('./etc'); -const { getBrowserVersionMap, getEnvironment } = require('./utils'); +const { + getBrowserVersionMap, + getEnvironment, + isDevBuild, + isTestBuild, +} = require('./utils'); const { getConfig } = require('./config'); /* eslint-disable no-constant-condition, node/global-require */ @@ -190,6 +195,7 @@ async function defineAndRunBuildTasks() { const styleTasks = createStyleTasks({ livereload }); const scriptTasks = createScriptTasks({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildType, @@ -380,8 +386,9 @@ testDev: Create an unoptimized, live-reloading build for debugging e2e tests.`, platform, } = argv; - // Manually default this to `false` for dev builds only. - const shouldLintFenceFiles = lintFenceFiles ?? !/dev/iu.test(task); + // Manually default this to `false` for dev and test builds. + const shouldLintFenceFiles = + lintFenceFiles ?? (!isDevBuild(task) && !isTestBuild(task)); const version = getVersion(buildType, buildVersion); diff --git a/development/build/scripts.js b/development/build/scripts.js index 9e0ebd53715c..17c57807073d 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -4,7 +4,6 @@ const { callbackify } = require('util'); const path = require('path'); const { writeFileSync, readFileSync, unlinkSync } = require('fs'); const EventEmitter = require('events'); -const assert = require('assert'); const gulp = require('gulp'); const watch = require('gulp-watch'); const Vinyl = require('vinyl'); @@ -14,7 +13,7 @@ const log = require('fancy-log'); const browserify = require('browserify'); const watchify = require('watchify'); const babelify = require('babelify'); -const brfs = require('brfs'); + const envify = require('loose-envify/custom'); const sourcemaps = require('gulp-sourcemaps'); const applySourceMap = require('vinyl-sourcemaps-apply'); @@ -30,9 +29,9 @@ const terser = require('terser'); const bifyModuleGroups = require('bify-module-groups'); -const phishingWarningManifest = require('@metamask/phishing-warning/package.json'); const { streamFlatMap } = require('../stream-flat-map'); -const { BUILD_TARGETS, ENVIRONMENT } = require('./constants'); +const { setEnvironmentVariables } = require('./set-environment-variables'); +const { BUILD_TARGETS } = require('./constants'); const { getConfig } = require('./config'); const { isDevBuild, @@ -41,8 +40,6 @@ const { logError, wrapAgainstScuttling, getBuildName, - getBuildAppId, - getBuildIcon, makeSelfInjecting, } = require('./utils'); @@ -58,7 +55,7 @@ const { // map dist files to bag of needed native APIs against LM scuttling const scuttlingConfigBase = { - 'sentry-install.js': { + 'scripts/sentry-install.js': { // globals sentry need to function window: '', navigator: '', @@ -96,128 +93,12 @@ const mv3ScuttlingConfig = { ...scuttlingConfigBase }; const standardScuttlingConfig = { ...scuttlingConfigBase, - 'sentry-install.js': { - ...scuttlingConfigBase['sentry-install.js'], + 'scripts/sentry-install.js': { + ...scuttlingConfigBase['scripts/sentry-install.js'], document: '', }, }; -/** - * Get the appropriate Infura project ID. - * - * @param {object} options - The Infura project ID options. - * @param {string} options.buildType - The current build type. - * @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment. - * @param {boolean} options.testing - Whether this is a test build or not. - * @param options.variables - * @returns {string} The Infura project ID. - */ -function getInfuraProjectId({ buildType, variables, environment, testing }) { - const EMPTY_PROJECT_ID = '00000000000000000000000000000000'; - if (testing) { - return EMPTY_PROJECT_ID; - } else if (environment !== ENVIRONMENT.PRODUCTION) { - // Skip validation because this is unset on PRs from forks. - // For forks, return empty project ID if we don't have one. - if ( - !variables.isDefined('INFURA_PROJECT_ID') && - environment === ENVIRONMENT.PULL_REQUEST - ) { - return EMPTY_PROJECT_ID; - } - return variables.get('INFURA_PROJECT_ID'); - } - /** @type {string|undefined} */ - const infuraKeyReference = variables.get('INFURA_ENV_KEY_REF'); - assert( - typeof infuraKeyReference === 'string' && infuraKeyReference.length > 0, - `Build type "${buildType}" has improperly set INFURA_ENV_KEY_REF in builds.yml. Current value: "${infuraKeyReference}"`, - ); - /** @type {string|undefined} */ - const infuraProjectId = variables.get(infuraKeyReference); - assert( - typeof infuraProjectId === 'string' && infuraProjectId.length > 0, - `Infura Project ID environmental variable "${infuraKeyReference}" is set improperly.`, - ); - return infuraProjectId; -} - -/** - * Get the appropriate Segment write key. - * - * @param {object} options - The Segment write key options. - * @param {string} options.buildType - The current build type. - * @param {keyof ENVIRONMENT} options.environment - The current build environment. - * @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline - * @returns {string} The Segment write key. - */ -function getSegmentWriteKey({ buildType, variables, environment }) { - if (environment !== ENVIRONMENT.PRODUCTION) { - // Skip validation because this is unset on PRs from forks, and isn't necessary for development builds. - return variables.get('SEGMENT_WRITE_KEY'); - } - - const segmentKeyReference = variables.get('SEGMENT_WRITE_KEY_REF'); - assert( - typeof segmentKeyReference === 'string' && segmentKeyReference.length > 0, - `Build type "${buildType}" has improperly set SEGMENT_WRITE_KEY_REF in builds.yml. Current value: "${segmentKeyReference}"`, - ); - - const segmentWriteKey = variables.get(segmentKeyReference); - assert( - typeof segmentWriteKey === 'string' && segmentWriteKey.length > 0, - `Segment Write Key environmental variable "${segmentKeyReference}" is set improperly.`, - ); - return segmentWriteKey; -} - -/** - * Get the URL for the phishing warning page, if it has been set. - * - * @param {object} options - The phishing warning page options. - * @param {boolean} options.testing - Whether this is a test build or not. - * @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline - * @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set. - */ -function getPhishingWarningPageUrl({ variables, testing }) { - let phishingWarningPageUrl = variables.get('PHISHING_WARNING_PAGE_URL'); - - assert( - phishingWarningPageUrl === null || - typeof phishingWarningPageUrl === 'string', - ); - if (phishingWarningPageUrl === null) { - phishingWarningPageUrl = testing - ? 'http://localhost:9999/' - : `https://metamask.github.io/phishing-warning/v${phishingWarningManifest.version}/`; - } - - // 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); - } catch (error) { - throw new Error( - `Invalid phishing warning page URL: '${normalizedUrl}'`, - error, - ); - } - if (phishingWarningPageUrlObject.hash) { - // The URL fragment must be set dynamically - throw new Error( - `URL fragment not allowed in phishing warning page URL: '${normalizedUrl}'`, - ); - } - - return normalizedUrl; -} - const noopWriteStream = through.obj((_file, _fileEncoding, callback) => callback(), ); @@ -247,9 +128,12 @@ module.exports = createScriptTasks; * @param {boolean} options.shouldLintFenceFiles - Whether files with code * fences should be linted after fences have been removed. * @param {string} options.version - The current version of the extension. + * @param options.shouldIncludeSnow - Whether the build should use + * Snow at runtime or not. * @returns {object} A set of tasks, one for each build target. */ function createScriptTasks({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildType, @@ -312,6 +196,7 @@ function createScriptTasks({ const standardSubtask = createTask( `${taskPrefix}:standardEntryPoints`, createFactoredBuild({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildTarget, @@ -376,6 +261,7 @@ function createScriptTasks({ installSentrySubtask, ].map((subtask) => runInChildProcess(subtask, { + shouldIncludeSnow, applyLavaMoat, buildType, isLavaMoat, @@ -400,7 +286,7 @@ function createScriptTasks({ browserPlatforms, buildTarget, buildType, - destFilepath: `${label}.js`, + destFilepath: `scripts/${label}.js`, entryFilepath: `./app/scripts/${label}.js`, ignoredFiles, label, @@ -424,7 +310,7 @@ function createScriptTasks({ browserPlatforms, buildTarget, buildType, - destFilepath: `${label}.js`, + destFilepath: `scripts/${label}.js`, entryFilepath: `./app/scripts/${label}.js`, ignoredFiles, label, @@ -452,7 +338,7 @@ function createScriptTasks({ buildTarget, buildType, browserPlatforms, - destFilepath: `${inpage}.js`, + destFilepath: `scripts/${inpage}.js`, entryFilepath: `./app/scripts/${inpage}.js`, label: inpage, ignoredFiles, @@ -467,25 +353,29 @@ function createScriptTasks({ if (process.env.ENABLE_MV3) { return; } - // stringify inpage.js into itself, and then make it inject itself into the page + // stringify scripts/inpage.js into itself, and then make it inject itself into the page browserPlatforms.forEach((browser) => { makeSelfInjecting( - path.join(__dirname, `../../dist/${browser}/${inpage}.js`), + path.join(__dirname, `../../dist/${browser}/scripts/${inpage}.js`), ); }); - // delete the inpage.js source map, as it no longer represents inpage.js - // and so `yarn source-map-explorer` can't handle it. It's also not - // useful anyway, as inpage.js is injected as a `script.textContent`, - // and not tracked in Sentry or browsers devtools anyway. + // delete the scripts/inpage.js source map, as it no longer represents + // scripts/inpage.js and so `yarn source-map-explorer` can't handle it. + // It's also not useful anyway, as scripts/inpage.js is injected as a + // `script.textContent`, and not tracked in Sentry or browsers devtools + // anyway. unlinkSync( - path.join(__dirname, `../../dist/sourcemaps/${inpage}.js.map`), + path.join( + __dirname, + `../../dist/sourcemaps/scripts/${inpage}.js.map`, + ), ); }, createNormalBundle({ buildTarget, buildType, browserPlatforms, - destFilepath: `${contentscript}.js`, + destFilepath: `scripts/${contentscript}.js`, entryFilepath: `./app/scripts/${contentscript}.js`, label: contentscript, ignoredFiles, @@ -523,9 +413,12 @@ function createScriptTasks({ * @param {boolean} options.shouldLintFenceFiles - Whether files with code * fences should be linted after fences have been removed. * @param {string} options.version - The current version of the extension. + * @param options.shouldIncludeSnow - Whether the build should use + * Snow at runtime or not. * @returns {Function} A function that creates the set of bundles. */ async function createManifestV3AppInitializationBundle({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildTarget, @@ -551,6 +444,7 @@ async function createManifestV3AppInitializationBundle({ } const extraEnvironmentVariables = { + USE_SNOW: shouldIncludeSnow, APPLY_LAVAMOAT: applyLavaMoat, FILE_NAMES: jsBundles.join(','), }; @@ -559,7 +453,7 @@ async function createManifestV3AppInitializationBundle({ browserPlatforms: mv3BrowserPlatforms, buildTarget, buildType, - destFilepath: 'app-init.js', + destFilepath: 'scripts/app-init.js', entryFilepath: './app/scripts/app-init.js', extraEnvironmentVariables, ignoredFiles, @@ -573,9 +467,12 @@ async function createManifestV3AppInitializationBundle({ // Code below is used to set statsMode to true when testing in MV3 // This is used to capture module initialisation stats using lavamoat. if (isTestBuild(buildTarget)) { - const content = readFileSync('./dist/chrome/runtime-lavamoat.js', 'utf8'); + const content = readFileSync( + './dist/chrome/scripts/runtime-lavamoat.js', + 'utf8', + ); const fileOutput = content.replace('statsMode = false', 'statsMode = true'); - writeFileSync('./dist/chrome/runtime-lavamoat.js', fileOutput); + writeFileSync('./dist/chrome/scripts/runtime-lavamoat.js', fileOutput); } console.log(`Bundle end: service worker app-init.js`); @@ -609,9 +506,12 @@ async function createManifestV3AppInitializationBundle({ * @param {boolean} options.shouldLintFenceFiles - Whether files with code * fences should be linted after fences have been removed. * @param {string} options.version - The current version of the extension. + * @param options.shouldIncludeSnow - Whether the build should use + * Snow at runtime or not. * @returns {Function} A function that creates the set of bundles. */ function createFactoredBuild({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildTarget, @@ -635,12 +535,16 @@ function createFactoredBuild({ const environment = getEnvironment({ buildTarget }); const config = await getConfig(buildType, environment); const { variables, activeBuild } = config; - await setEnvironmentVariables({ - buildTarget, + setEnvironmentVariables({ + isDevBuild: reloadOnChange, + isTestBuild: isTestBuild(buildTarget), + buildName: getBuildName({ + environment, + buildType, + }), buildType, environment, variables, - activeBuild, version, }); const features = { @@ -670,12 +574,17 @@ function createFactoredBuild({ __dirname, `../../lavamoat/browserify/${buildType}/policy.json`, ), + policyDebug: path.resolve( + __dirname, + `../../lavamoat/browserify/${buildType}/policy-debug.json`, + ), policyName: buildType, policyOverride: path.resolve( __dirname, `../../lavamoat/browserify/policy-override.json`, ), writeAutoPolicy: process.env.WRITE_AUTO_POLICY, + writeAutoPolicyDebug: process.env.WRITE_AUTO_POLICY_DEBUG, }; Object.assign(bundlerOpts, lavamoatBrowserify.args); bundlerOpts.plugin.push([lavamoatBrowserify, lavamoatOpts]); @@ -718,7 +627,7 @@ function createFactoredBuild({ // add lavamoat policy loader file to packer output moduleGroupPackerStream.push( new Vinyl({ - path: 'policy-load.js', + path: 'scripts/policy-load.js', contents: lavapack.makePolicyLoaderStream(lavamoatOpts), }), ); @@ -749,14 +658,23 @@ function createFactoredBuild({ buildTarget === BUILD_TARGETS.TEST_DEV; switch (groupLabel) { case 'ui': { + renderHtmlFile({ + htmlName: 'loading', + browserPlatforms, + shouldIncludeSnow, + applyLavaMoat, + isMMI: buildType === 'mmi', + }); renderHtmlFile({ htmlName: 'popup', browserPlatforms, + shouldIncludeSnow, applyLavaMoat, }); renderHtmlFile({ htmlName: 'notification', browserPlatforms, + shouldIncludeSnow, applyLavaMoat, isMMI: buildType === 'mmi', isTest, @@ -764,6 +682,7 @@ function createFactoredBuild({ renderHtmlFile({ htmlName: 'home', browserPlatforms, + shouldIncludeSnow, applyLavaMoat, isMMI: buildType === 'mmi', isTest, @@ -772,6 +691,7 @@ function createFactoredBuild({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, destinationFileName: 'load-app.js', }); @@ -783,12 +703,14 @@ function createFactoredBuild({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, }); renderJavaScriptLoader({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, destinationFileName: 'load-background.js', }); @@ -796,8 +718,9 @@ function createFactoredBuild({ const jsBundles = [ ...commonSet.values(), ...groupSet.values(), - ].map((label) => `./${label}.js`); + ].map((label) => `../${label}.js`); await createManifestV3AppInitializationBundle({ + shouldIncludeSnow, applyLavaMoat, browserPlatforms, buildTarget, @@ -817,6 +740,7 @@ function createFactoredBuild({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat: false, }); break; @@ -826,6 +750,7 @@ function createFactoredBuild({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, destinationFileName: 'load-offscreen.js', }); @@ -900,12 +825,16 @@ function createNormalBundle({ const environment = getEnvironment({ buildTarget }); const config = await getConfig(buildType, environment); const { activeBuild, variables } = config; - await setEnvironmentVariables({ - buildTarget, + setEnvironmentVariables({ + buildName: getBuildName({ + environment, + buildType, + }), + isDevBuild: devMode, + isTestBuild: isTestBuild(buildTarget), buildType, variables, environment, - activeBuild, version, }); Object.entries(extraEnvironmentVariables ?? {}).forEach(([key, value]) => @@ -993,8 +922,14 @@ function setupBundlerDefaults( extensions, }, ], - // Inline `fs.readFileSync` files - brfs, + // We are transpelling the firebase package to be compatible with the lavaMoat restrictions + [ + babelify, + { + only: ['./**/node_modules/firebase', './**/node_modules/@firebase'], + global: true, + }, + ], ], // Look for TypeScript files when walking the dependency tree extensions, @@ -1198,74 +1133,11 @@ async function createBundle(buildConfiguration, { reloadOnChange }) { } } -/** - * Sets environment variables to inject in the current build. - * - * @param {object} options - Build options. - * @param {BUILD_TARGETS} options.buildTarget - The current build target. - * @param {string} options.buildType - The current build type (e.g. "main", - * "flask", etc.). - * @param {string} options.version - The current version of the extension. - * @param options.activeBuild - * @param options.variables - * @param options.environment - */ -async function setEnvironmentVariables({ - buildTarget, - buildType, - activeBuild, - environment, - variables, - version, -}) { - const devMode = isDevBuild(buildTarget); - const testing = isTestBuild(buildTarget); - - variables.set({ - DEBUG: devMode || testing ? variables.getMaybe('DEBUG') : undefined, - EIP_4337_ENTRYPOINT: - variables.getMaybe('EIP_4337_ENTRYPOINT') || - (testing ? '0x18b06605539dc02ecD3f7AB314e38eB7c1dA5c9b' : undefined), - IN_TEST: testing, - INFURA_PROJECT_ID: getInfuraProjectId({ - buildType, - activeBuild, - variables, - environment, - testing, - }), - METAMASK_DEBUG: devMode || variables.getMaybe('METAMASK_DEBUG') === true, - METAMASK_BUILD_NAME: getBuildName({ - environment, - buildType, - }), - METAMASK_BUILD_APP_ID: getBuildAppId({ - buildType, - }), - METAMASK_BUILD_ICON: getBuildIcon({ - buildType, - }), - METAMASK_ENVIRONMENT: environment, - METAMASK_VERSION: version, - METAMASK_BUILD_TYPE: buildType, - NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION, - PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ - variables, - testing, - }), - SEGMENT_WRITE_KEY: getSegmentWriteKey({ - buildType, - activeBuild, - variables, - environment, - }), - }); -} - function renderJavaScriptLoader({ groupSet, commonSet, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, destinationFileName, }) { @@ -1280,18 +1152,23 @@ function renderJavaScriptLoader({ ); const securityScripts = applyLavaMoat - ? ['./runtime-lavamoat.js', './lockdown-more.js', './policy-load.js'] + ? [ + './scripts/runtime-lavamoat.js', + './scripts/lockdown-more.js', + './scripts/policy-load.js', + ] : [ - './lockdown-install.js', - './lockdown-run.js', - './lockdown-more.js', - './runtime-cjs.js', + './scripts/lockdown-install.js', + './scripts/lockdown-run.js', + './scripts/lockdown-more.js', + './scripts/runtime-cjs.js', ]; const requiredScripts = [ - './snow.js', - './use-snow.js', - './sentry-install.js', + ...(shouldIncludeSnow + ? ['./scripts/snow.js', './scripts/use-snow.js'] + : []), + './scripts/sentry-install.js', ...securityScripts, ...jsBundles, ]; @@ -1313,6 +1190,7 @@ function renderJavaScriptLoader({ function renderHtmlFile({ htmlName, browserPlatforms, + shouldIncludeSnow, applyLavaMoat, isMMI, isTest, @@ -1322,11 +1200,21 @@ function renderHtmlFile({ 'build/scripts/renderHtmlFile - must specify "applyLavaMoat" option', ); } + if (shouldIncludeSnow === undefined) { + throw new Error( + 'build/scripts/renderHtmlFile - must specify "shouldIncludeSnow" option', + ); + } + const htmlFilePath = `./app/${htmlName}.html`; const htmlTemplate = readFileSync(htmlFilePath, 'utf8'); const eta = new Eta(); - const htmlOutput = eta.renderString(htmlTemplate, { isMMI, isTest }); + const htmlOutput = eta.renderString(htmlTemplate, { + isMMI, + isTest, + shouldIncludeSnow, + }); 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 new file mode 100644 index 000000000000..13e7e96f1f24 --- /dev/null +++ b/development/build/set-environment-variables.js @@ -0,0 +1,217 @@ +const { readFileSync } = require('node:fs'); +const assert = require('node:assert'); +const { ENVIRONMENT } = require('./constants'); + +/** + * Sets environment variables to inject in the current build. + * + * @param {object} options - Build options. + * @param {string} options.buildName - The name of the build. + * @param {boolean} options.isDevBuild - Whether the build is a development build. + * @param {boolean} options.isTestBuild - Whether the build is a test build. + * @param {string} options.buildType - The current build type (e.g. "main", + * "flask", etc.). + * @param {string} options.version - The current version of the extension. + * @param {import('../lib/variables').Variables} options.variables + * @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment. + */ +module.exports.setEnvironmentVariables = function setEnvironmentVariables({ + buildName, + isDevBuild, + isTestBuild, + buildType, + environment, + variables, + version, +}) { + variables.set({ + DEBUG: isDevBuild || isTestBuild ? variables.getMaybe('DEBUG') : undefined, + EIP_4337_ENTRYPOINT: isTestBuild + ? '0x18b06605539dc02ecD3f7AB314e38eB7c1dA5c9b' + : variables.getMaybe('EIP_4337_ENTRYPOINT'), + IN_TEST: isTestBuild, + INFURA_PROJECT_ID: getInfuraProjectId({ + buildType, + variables, + environment, + testing: isTestBuild, + }), + METAMASK_DEBUG: isDevBuild || variables.getMaybe('METAMASK_DEBUG') === true, + METAMASK_BUILD_NAME: buildName, + METAMASK_BUILD_APP_ID: getBuildAppId({ + buildType, + }), + METAMASK_BUILD_ICON: getBuildIcon({ + buildType, + }), + METAMASK_ENVIRONMENT: environment, + METAMASK_VERSION: version, + METAMASK_BUILD_TYPE: buildType, + NODE_ENV: isDevBuild ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION, + PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ + variables, + testing: isTestBuild, + }), + SEGMENT_WRITE_KEY: getSegmentWriteKey({ + buildType, + variables, + environment, + }), + TEST_GAS_FEE_FLOWS: + isDevBuild && variables.getMaybe('TEST_GAS_FEE_FLOWS') === true, + }); +}; + +const BUILD_TYPES_TO_SVG_LOGO_PATH = { + main: './app/images/logo/metamask-fox.svg', + beta: './app/build-types/beta/images/logo/metamask-fox.svg', + flask: './app/build-types/flask/images/logo/metamask-fox.svg', + mmi: './app/build-types/mmi/images/logo/mmi-logo.svg', + desktop: './app/build-types/desktop/images/logo/metamask-fox.svg', +}; + +/** + * Get the image data uri for the svg icon for the current build. + * + * @param {object} options - The build options. + * @param {string} options.buildType - The build type of the current build. + * @returns {string} The image data uri for the icon. + */ +function getBuildIcon({ buildType }) { + const svgLogoPath = + BUILD_TYPES_TO_SVG_LOGO_PATH[buildType] || + BUILD_TYPES_TO_SVG_LOGO_PATH.main; + // encode as base64 as its more space-efficient for most SVGs than a data uri + return `data:image/svg+xml;base64,${readFileSync(svgLogoPath, 'base64')}`; +} + +/** + * Get the app ID for the current build. Should be valid reverse FQDN. + * + * @param {object} options - The build options. + * @param {string} options.buildType - The build type of the current build. + * @returns {string} The build app ID. + */ +function getBuildAppId({ buildType }) { + const baseDomain = 'io.metamask'; + return buildType === 'main' ? baseDomain : `${baseDomain}.${buildType}`; +} + +/** + * Get the appropriate Infura project ID. + * + * @param {object} options - The Infura project ID options. + * @param {string} options.buildType - The current build type. + * @param {ENVIRONMENT[keyof ENVIRONMENT]} options.environment - The build environment. + * @param {boolean} options.testing - Whether this is a test build or not. + * @param options.variables + * @returns {string} The Infura project ID. + */ +function getInfuraProjectId({ buildType, variables, environment, testing }) { + const EMPTY_PROJECT_ID = '00000000000000000000000000000000'; + if (testing) { + return EMPTY_PROJECT_ID; + } else if (environment !== ENVIRONMENT.PRODUCTION) { + // Skip validation because this is unset on PRs from forks. + // For forks, return empty project ID if we don't have one. + if ( + !variables.isDefined('INFURA_PROJECT_ID') && + environment === ENVIRONMENT.PULL_REQUEST + ) { + return EMPTY_PROJECT_ID; + } + return variables.get('INFURA_PROJECT_ID'); + } + /** @type {string|undefined} */ + const infuraKeyReference = variables.get('INFURA_ENV_KEY_REF'); + assert( + typeof infuraKeyReference === 'string' && infuraKeyReference.length > 0, + `Build type "${buildType}" has improperly set INFURA_ENV_KEY_REF in builds.yml. Current value: "${infuraKeyReference}"`, + ); + /** @type {string|undefined} */ + const infuraProjectId = variables.get(infuraKeyReference); + assert( + typeof infuraProjectId === 'string' && infuraProjectId.length > 0, + `Infura Project ID environmental variable "${infuraKeyReference}" is set improperly.`, + ); + return infuraProjectId; +} + +/** + * Get the appropriate Segment write key. + * + * @param {object} options - The Segment write key options. + * @param {string} options.buildType - The current build type. + * @param {keyof ENVIRONMENT} options.environment - The current build environment. + * @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline + * @returns {string} The Segment write key. + */ +function getSegmentWriteKey({ buildType, variables, environment }) { + if (environment !== ENVIRONMENT.PRODUCTION) { + // Skip validation because this is unset on PRs from forks, and isn't necessary for development builds. + return variables.get('SEGMENT_WRITE_KEY'); + } + + const segmentKeyReference = variables.get('SEGMENT_WRITE_KEY_REF'); + assert( + typeof segmentKeyReference === 'string' && segmentKeyReference.length > 0, + `Build type "${buildType}" has improperly set SEGMENT_WRITE_KEY_REF in builds.yml. Current value: "${segmentKeyReference}"`, + ); + + const segmentWriteKey = variables.get(segmentKeyReference); + assert( + typeof segmentWriteKey === 'string' && segmentWriteKey.length > 0, + `Segment Write Key environmental variable "${segmentKeyReference}" is set improperly.`, + ); + return segmentWriteKey; +} + +/** + * Get the URL for the phishing warning page, if it has been set. + * + * @param {object} options - The phishing warning page options. + * @param {boolean} options.testing - Whether this is a test build or not. + * @param {import('../lib/variables').Variables} options.variables - Object containing all variables that modify the build pipeline + * @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set. + */ +function getPhishingWarningPageUrl({ variables, testing }) { + let phishingWarningPageUrl = variables.get('PHISHING_WARNING_PAGE_URL'); + + assert( + phishingWarningPageUrl === null || + typeof phishingWarningPageUrl === 'string', + ); + if (phishingWarningPageUrl === null) { + phishingWarningPageUrl = testing + ? 'http://localhost:9999/' + : `https://metamask.github.io/phishing-warning/v${ + // eslint-disable-next-line node/global-require + require('@metamask/phishing-warning/package.json').version + }/`; + } + + // 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); + } catch (error) { + throw new Error( + `Invalid phishing warning page URL: '${normalizedUrl}'`, + error, + ); + } + if (phishingWarningPageUrlObject.hash) { + // The URL fragment must be set dynamically + throw new Error( + `URL fragment not allowed in phishing warning page URL: '${normalizedUrl}'`, + ); + } + + return normalizedUrl; +} diff --git a/development/build/static.js b/development/build/static.js index c8eb7d6d5ef2..20e22eeddcca 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -147,54 +147,54 @@ function getCopyTargets( pattern: `*.css`, dest: ``, }, - { - src: `./app/loading.html`, - dest: `loading.html`, - }, - { - src: shouldIncludeSnow - ? `./node_modules/@lavamoat/snow/snow.prod.js` - : EMPTY_JS_FILE, - dest: `snow.js`, - }, - { - src: shouldIncludeSnow ? `./app/scripts/use-snow.js` : EMPTY_JS_FILE, - dest: `use-snow.js`, - }, + ...(shouldIncludeSnow + ? [ + { + src: shouldIncludeSnow + ? `./node_modules/@lavamoat/snow/snow.prod.js` + : EMPTY_JS_FILE, + dest: `scripts/snow.js`, + }, + { + src: `./app/scripts/use-snow.js`, + dest: `scripts/use-snow.js`, + }, + ] + : []), { src: shouldIncludeLockdown ? getPathInsideNodeModules('ses', 'dist/lockdown.umd.min.js') : EMPTY_JS_FILE, - dest: `lockdown-install.js`, + dest: `scripts/lockdown-install.js`, }, { src: './app/scripts/init-globals.js', - dest: 'init-globals.js', + dest: 'scripts/init-globals.js', }, { src: './app/scripts/load-app.js', - dest: 'load-app.js', + dest: 'scripts/load-app.js', }, { src: shouldIncludeLockdown ? `./app/scripts/lockdown-run.js` : EMPTY_JS_FILE, - dest: `lockdown-run.js`, + dest: `scripts/lockdown-run.js`, }, { src: shouldIncludeLockdown ? `./app/scripts/lockdown-more.js` : EMPTY_JS_FILE, - dest: `lockdown-more.js`, + dest: `scripts/lockdown-more.js`, }, { src: getPathInsideNodeModules('@lavamoat/lavapack', 'src/runtime-cjs.js'), - dest: `runtime-cjs.js`, + dest: `scripts/runtime-cjs.js`, pattern: '', }, { src: getPathInsideNodeModules('@lavamoat/lavapack', 'src/runtime.js'), - dest: `runtime-lavamoat.js`, + dest: `scripts/runtime-lavamoat.js`, pattern: '', }, { @@ -208,7 +208,7 @@ function getCopyTargets( allCopyTargets.push({ src: getPathInsideNodeModules('@blockaid/ppom_release', '/'), pattern: '*.wasm', - dest: '', + dest: process.env.ENABLE_MV3 ? 'scripts/' : '', }); } diff --git a/development/build/task.js b/development/build/task.js index 1b28a15f6783..cbbc1ea22578 100644 --- a/development/build/task.js +++ b/development/build/task.js @@ -51,7 +51,14 @@ function createTask(taskName, taskFn) { function runInChildProcess( task, - { applyLavaMoat, buildType, isLavaMoat, policyOnly, shouldLintFenceFiles }, + { + shouldIncludeSnow, + applyLavaMoat, + buildType, + isLavaMoat, + policyOnly, + shouldLintFenceFiles, + }, ) { const taskName = typeof task === 'string' ? task : task.taskName; if (!taskName) { @@ -68,6 +75,7 @@ function runInChildProcess( // LavaMoat if the parent process also ran in LavaMoat. isLavaMoat ? 'build' : 'build:dev', taskName, + `--snow=${shouldIncludeSnow ? 'true' : 'false'}`, `--apply-lavamoat=${applyLavaMoat ? 'true' : 'false'}`, `--build-type=${buildType}`, `--lint-fence-files=${shouldLintFenceFiles ? 'true' : 'false'}`, diff --git a/development/build/utils.js b/development/build/utils.js index 07349c88db28..746290fb8e2b 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -5,14 +5,6 @@ const { capitalize } = require('lodash'); const { loadBuildTypesConfig } = require('../lib/build-type'); const { BUILD_TARGETS, ENVIRONMENT } = require('./constants'); -const BUILD_TYPES_TO_SVG_LOGO_PATH = { - main: './app/images/logo/metamask-fox.svg', - beta: './app/build-types/beta/images/logo/metamask-fox.svg', - flask: './app/build-types/flask/images/logo/metamask-fox.svg', - mmi: './app/build-types/mmi/images/logo/mmi-logo.svg', - desktop: './app/build-types/desktop/images/logo/metamask-fox.svg', -}; - /** * Returns whether the current build is a development build or not. * @@ -261,32 +253,6 @@ function getBuildName({ return name; } -/** - * Get the app ID for the current build. Should be valid reverse FQDN. - * - * @param {object} options - The build options. - * @param {string} options.buildType - The build type of the current build. - * @returns {string} The build app ID. - */ -function getBuildAppId({ buildType }) { - const baseDomain = 'io.metamask'; - return buildType === 'main' ? baseDomain : `${baseDomain}.${buildType}`; -} - -/** - * Get the image data uri for the svg icon for the current build. - * - * @param {object} options - The build options. - * @param {string} options.buildType - The build type of the current build. - * @returns {string} The image data uri for the icon. - */ -function getBuildIcon({ buildType }) { - const svgLogoPath = - BUILD_TYPES_TO_SVG_LOGO_PATH[buildType] || - BUILD_TYPES_TO_SVG_LOGO_PATH.main; - const svg = readFileSync(svgLogoPath, 'utf8'); - return `data:image/svg+xml,${encodeURIComponent(svg)}`; -} /** * Takes the given JavaScript file at `filePath` and replaces its contents with * a script that injects the original file contents into the document in which @@ -306,8 +272,6 @@ function makeSelfInjecting(filePath) { module.exports = { getBrowserVersionMap, getBuildName, - getBuildAppId, - getBuildIcon, getEnvironment, isDevBuild, isTestBuild, diff --git a/development/charts/flamegraph/chart/index.html b/development/charts/flamegraph/chart/index.html index 7afe9f9d0dcb..ce53076ad9e4 100644 --- a/development/charts/flamegraph/chart/index.html +++ b/development/charts/flamegraph/chart/index.html @@ -9,7 +9,7 @@ rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" /> - +