diff --git a/.circleci/config.yml b/.circleci/config.yml index 823da67ad..e26983022 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,6 +30,12 @@ rc_branch_only: &rc_branch_only only: - /^Version-v(\d+)[.](\d+)[.](\d+)/ +rc_or_master_branch_only: &rc_or_master_branch_only + filters: + branches: + only: + - /^Version-v(\d+)[.](\d+)[.](\d+)|master/ + workflows: test_and_release: jobs: @@ -50,11 +56,19 @@ workflows: - test-yarn-dedupe: requires: - prep-deps - - validate-lavamoat-config: - filters: - branches: - only: - - /^Version-v(\d+)[.](\d+)[.](\d+)|master/ + - validate-lavamoat-allow-scripts: + <<: *rc_or_master_branch_only + requires: + - prep-deps + - validate-lavamoat-policy-build: + <<: *rc_or_master_branch_only + requires: + - prep-deps + - validate-lavamoat-policy-webapp: + <<: *rc_or_master_branch_only + matrix: + parameters: + build-type: [main, beta, flask, mmi, desktop] requires: - prep-deps - prep-build: @@ -162,7 +176,9 @@ workflows: - prep-build-flask - all-tests-pass: requires: - - validate-lavamoat-config + - validate-lavamoat-allow-scripts + - validate-lavamoat-policy-build + - validate-lavamoat-policy-webapp - test-lint - test-lint-shellcheck - test-lint-lockfile @@ -329,7 +345,7 @@ jobs: - node_modules - build-artifacts - validate-lavamoat-config: + validate-lavamoat-allow-scripts: executor: node-browsers-medium-plus steps: - checkout @@ -337,12 +353,39 @@ jobs: at: . - run: name: Validate allow-scripts config - command: | - .circleci/scripts/validate-allow-scripts.sh + command: yarn allow-scripts auto - run: - name: Validate LavaMoat policy - command: | - .circleci/scripts/validate-lavamoat-policy.sh + name: Check working tree + command: .circleci/scripts/check-working-tree.sh + + validate-lavamoat-policy-build: + executor: node-browsers-medium-plus + steps: + - checkout + - attach_workspace: + at: . + - run: + name: Validate LavaMoat build policy + command: yarn lavamoat:build:auto + - run: + name: Check working tree + command: .circleci/scripts/check-working-tree.sh + + validate-lavamoat-policy-webapp: + executor: node-browsers-medium-plus + parameters: + build-type: + type: string + steps: + - checkout + - attach_workspace: + at: . + - run: + name: Validate LavaMoat << parameters.build-type >> policy + command: yarn lavamoat:webapp:auto:ci '--build-types=<< parameters.build-type >>' + - run: + name: Check working tree + command: .circleci/scripts/check-working-tree.sh prep-build: executor: node-browsers-medium-plus diff --git a/.circleci/scripts/check-working-tree.sh b/.circleci/scripts/check-working-tree.sh new file mode 100755 index 000000000..5de67431a --- /dev/null +++ b/.circleci/scripts/check-working-tree.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +if ! git diff --exit-code +then + echo "Working tree dirty" + exit 1 +fi diff --git a/.circleci/scripts/chrome-install.sh b/.circleci/scripts/chrome-install.sh index 450293197..cba84785f 100755 --- a/.circleci/scripts/chrome-install.sh +++ b/.circleci/scripts/chrome-install.sh @@ -7,12 +7,12 @@ set -o pipefail sudo apt-get update # To get the latest version, see -CHROME_VERSION='111.0.5563.64-1' +CHROME_VERSION='114.0.5735.133-1' CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb" CHROME_BINARY_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}" # To retrieve this checksum, run the `wget` and `shasum` commands below -CHROME_BINARY_SHA512SUM='bbfd436c17d6f0554b91211ecf1324aeeac012f1d000d610f93956dbfb8387c0adb56f921c5b7bcc1833c49ab2abbd3bbc250001f650b3ca4f79cebe708c29ae' +CHROME_BINARY_SHA512SUM='0b1a18c44efb72ed3e69a5f78419ff5fa973df42b18a8becfcc3d4f6825957c637e9396d07756f910f2d9c7c85a3e2b64cc30cca18182ae8811feadd609f159d' wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}" diff --git a/.circleci/scripts/validate-allow-scripts.sh b/.circleci/scripts/validate-allow-scripts.sh deleted file mode 100755 index de45520ad..000000000 --- a/.circleci/scripts/validate-allow-scripts.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -yarn allow-scripts auto - -if git diff --exit-code -then - echo "allow-scripts configuration is up-to-date" -else - echo "allow-scripts configuration requires updates" - exit 1 -fi diff --git a/.circleci/scripts/validate-lavamoat-policy.sh b/.circleci/scripts/validate-lavamoat-policy.sh deleted file mode 100755 index 177fabe0b..000000000 --- a/.circleci/scripts/validate-lavamoat-policy.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -yarn lavamoat:auto:ci - -if git diff --exit-code -then - echo "LavaMoat policy is up-to-date" -else - echo "LavaMoat policy requires updates" - exit 1 -fi diff --git a/.eslintrc.js b/.eslintrc.js index 1236df167..4ef8de9bf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -238,7 +238,6 @@ module.exports = { excludedFiles: [ 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/mmi-controller.test.js', - 'app/scripts/controllers/network/**/*.test.js', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/lib/**/*.test.js', 'app/scripts/migrations/*.test.js', @@ -267,9 +266,6 @@ module.exports = { '**/__snapshots__/*.snap', 'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/mmi-controller.test.js', - 'app/scripts/controllers/network/**/*.test.js', - 'app/scripts/controllers/network/**/*.test.ts', - 'app/scripts/controllers/network/provider-api-tests/*.ts', 'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/lib/**/*.test.js', 'app/scripts/migrations/*.test.js', diff --git a/.github/scripts/add-release-label-to-pr-and-linked-issues.ts b/.github/scripts/add-release-label-to-pr-and-linked-issues.ts new file mode 100644 index 000000000..0bf34182e --- /dev/null +++ b/.github/scripts/add-release-label-to-pr-and-linked-issues.ts @@ -0,0 +1,328 @@ +import * as core from '@actions/core'; +import { context, getOctokit } from '@actions/github'; +import { GitHub } from '@actions/github/lib/utils'; + +// A labelable object can be a pull request or an issue +interface Labelable { + id: string; + number: number; + repoOwner: string; + repoName: string; + createdAt: string; +} + +main().catch((error: Error): void => { + console.error(error); + process.exit(1); +}); + +async function main(): Promise { + // "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions. + // We can't use "GITHUB_TOKEN" here, as its permissions are scoped to the repository where the action is running. + // "GITHUB_TOKEN" does not have access to other repositories, even when they belong to the same organization. + // As we want to update linked issues which are not necessarily located in the same repository, + // we need to create our own "RELEASE_LABEL_TOKEN" with "repo" permissions. + // Such a token allows to access other repositories of the MetaMask organisation. + const personalAccessToken = process.env.RELEASE_LABEL_TOKEN; + if (!personalAccessToken) { + core.setFailed('RELEASE_LABEL_TOKEN not found'); + process.exit(1); + } + + const nextReleaseVersionNumber = process.env.NEXT_SEMVER_VERSION; + if (!nextReleaseVersionNumber) { + // NEXT_SEMVER_VERSION is automatically deduced as minor version bump on top of the latest version + // found, either in repo's list of branches, or in repo's list of tags or in repo's "package.json" version. + // For edge cases (e.g. major version bumps, etc.), where the value can not be deduced automatically, + // NEXT_SEMVER_VERSION can be defined manually set by defining FORCE_NEXT_SEMVER_VERSION variable in + // section "Secrets and variables">"Actions">"Variables">"New repository variable" in the settings of this repo. + // Example value: 6.5.0 + core.setFailed('NEXT_SEMVER_VERSION not found'); + process.exit(1); + } + + if (!isValidVersionFormat(nextReleaseVersionNumber)) { + core.setFailed(`NEXT_SEMVER_VERSION (${nextReleaseVersionNumber}) is not a valid version format. The expected format is "x.y.z", where "x", "y" and "z" are numbers.`); + process.exit(1); + } + + // Release label indicates the next release version number + // Example release label: "release-6.5.0" + const releaseLabelName = `release-${nextReleaseVersionNumber}`; + const releaseLabelColor = "ededed"; + const releaseLabelDescription = `Issue or pull request that will be included in release ${nextReleaseVersionNumber}`; + + // Initialise octokit, required to call Github GraphQL API + const octokit: InstanceType = getOctokit( + personalAccessToken, + { + previews: ["bane"], // The "bane" preview is required for adding, updating, creating and deleting labels. + }, + ); + + // Retrieve pull request info from context + const prRepoOwner = context.repo.owner; + const prRepoName = context.repo.repo; + const prNumber = context.payload.pull_request?.number; + if (!prNumber) { + core.setFailed('Pull request number not found'); + process.exit(1); + } + + // Retrieve pull request + const pullRequest: Labelable = await retrievePullRequest(octokit, prRepoOwner, prRepoName, prNumber); + + // Add the release label to the pull request + await addLabelToLabelable(octokit, pullRequest, releaseLabelName, releaseLabelColor, releaseLabelDescription); + + // Retrieve linked issues for the pull request + const linkedIssues: Labelable[] = await retrieveLinkedIssues(octokit, prRepoOwner, prRepoName, prNumber); + + // Add the release label to the linked issues + for (const linkedIssue of linkedIssues) { + await addLabelToLabelable(octokit, linkedIssue, releaseLabelName, releaseLabelColor, releaseLabelDescription); + } +} + +// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers. +function isValidVersionFormat(str: string): boolean { + const regex = /^\d+\.\d+\.\d+$/; + return regex.test(str); +} + +// This function retrieves the repo +async function retrieveRepo(octokit: InstanceType, repoOwner: string, repoName: string): Promise { + + const retrieveRepoQuery = ` + query RetrieveRepo($repoOwner: String!, $repoName: String!) { + repository(owner: $repoOwner, name: $repoName) { + id + } + } +`; + + const retrieveRepoResult: { + repository: { + id: string; + }; + } = await octokit.graphql(retrieveRepoQuery, { + repoOwner, + repoName, + }); + + const repoId = retrieveRepoResult?.repository?.id; + + return repoId; +} + +// This function retrieves the label on a specific repo +async function retrieveLabel(octokit: InstanceType, repoOwner: string, repoName: string, labelName: string): Promise { + + const retrieveLabelQuery = ` + query RetrieveLabel($repoOwner: String!, $repoName: String!, $labelName: String!) { + repository(owner: $repoOwner, name: $repoName) { + label(name: $labelName) { + id + } + } + } + `; + + const retrieveLabelResult: { + repository: { + label: { + id: string; + }; + }; + } = await octokit.graphql(retrieveLabelQuery, { + repoOwner, + repoName, + labelName, + }); + + const labelId = retrieveLabelResult?.repository?.label?.id; + + return labelId; +} + +// This function creates the label on a specific repo +async function createLabel(octokit: InstanceType, repoId: string, labelName: string, labelColor: string, labelDescription: string): Promise { + + const createLabelMutation = ` + mutation CreateLabel($repoId: ID!, $labelName: String!, $labelColor: String!, $labelDescription: String) { + createLabel(input: {repositoryId: $repoId, name: $labelName, color: $labelColor, description: $labelDescription}) { + label { + id + } + } + } + `; + + const createLabelResult: { + createLabel: { + label: { + id: string; + }; + }; + } = await octokit.graphql(createLabelMutation, { + repoId, + labelName, + labelColor, + labelDescription, + }); + + const labelId = createLabelResult?.createLabel?.label?.id; + + return labelId; +} + +// This function creates or retrieves the label on a specific repo +async function createOrRetrieveLabel(octokit: InstanceType, repoOwner: string, repoName: string, labelName: string, labelColor: string, labelDescription: string): Promise { + + // Check if label already exists on the repo + let labelId = await retrieveLabel(octokit, repoOwner, repoName, labelName); + + // If label doesn't exist on the repo, create it + if (!labelId) { + // Retrieve PR's repo + const repoId = await retrieveRepo(octokit, repoOwner, repoName); + + // Create label on repo + labelId = await createLabel(octokit, repoId, labelName, labelColor, labelDescription); + } + + return labelId; +} + +// This function retrieves the pull request on a specific repo +async function retrievePullRequest(octokit: InstanceType, repoOwner: string, repoName: string, prNumber: number): Promise { + + const retrievePullRequestQuery = ` + query GetPullRequest($repoOwner: String!, $repoName: String!, $prNumber: Int!) { + repository(owner: $repoOwner, name: $repoName) { + pullRequest(number: $prNumber) { + id + createdAt + } + } + } + `; + + const retrievePullRequestResult: { + repository: { + pullRequest: { + id: string; + createdAt: string; + }; + }; + } = await octokit.graphql(retrievePullRequestQuery, { + repoOwner, + repoName, + prNumber, + }); + + const pullRequest: Labelable = { + id: retrievePullRequestResult?.repository?.pullRequest?.id, + number: prNumber, + repoOwner: repoOwner, + repoName: repoName, + createdAt: retrievePullRequestResult?.repository?.pullRequest?.createdAt, + } + + return pullRequest; +} + + +// This function retrieves the list of linked issues for a pull request +async function retrieveLinkedIssues(octokit: InstanceType, repoOwner: string, repoName: string, prNumber: number): Promise { + + // We assume there won't be more than 100 linked issues + const retrieveLinkedIssuesQuery = ` + query ($repoOwner: String!, $repoName: String!, $prNumber: Int!) { + repository(owner: $repoOwner, name: $repoName) { + pullRequest(number: $prNumber) { + closingIssuesReferences(first: 100) { + nodes { + id + number + createdAt + repository { + name + owner { + login + } + } + } + } + } + } + } + `; + + const retrieveLinkedIssuesResult: { + repository: { + pullRequest: { + closingIssuesReferences: { + nodes: Array<{ + id: string; + number: number; + createdAt: string; + repository: { + name: string; + owner: { + login: string; + }; + }; + }>; + }; + }; + }; + } = await octokit.graphql(retrieveLinkedIssuesQuery, { + repoOwner, + repoName, + prNumber + }); + + const linkedIssues = retrieveLinkedIssuesResult?.repository?.pullRequest?.closingIssuesReferences?.nodes?.map((issue: { + id: string; + number: number; + createdAt: string; + repository: { + name: string; + owner: { + login: string; + }; + }; + }) => { + return { + id: issue?.id, + number: issue?.number, + repoOwner: issue?.repository?.owner?.login, + repoName: issue?.repository?.name, + createdAt: issue?.createdAt + }; + }) || []; + + return linkedIssues; +} + +// This function adds label to a labelable object (i.e. a pull request or an issue) +async function addLabelToLabelable(octokit: InstanceType, labelable: Labelable, labelName: string, labelColor: string, labelDescription: string): Promise { + + // Retrieve label from the labelable's repo, or create label if required + const labelId = await createOrRetrieveLabel(octokit, labelable?.repoOwner, labelable?.repoName, labelName, labelColor, labelDescription); + + const addLabelsToLabelableMutation = ` + mutation AddLabelsToLabelable($labelableId: ID!, $labelIds: [ID!]!) { + addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) { + clientMutationId + } + } + `; + + await octokit.graphql(addLabelsToLabelableMutation, { + labelableId: labelable?.id, + labelIds: [labelId], + }); + +} diff --git a/.github/workflows/add-release-label.yml b/.github/workflows/add-release-label.yml new file mode 100644 index 000000000..24d8f717a --- /dev/null +++ b/.github/workflows/add-release-label.yml @@ -0,0 +1,39 @@ +name: Add release label to PR and linked issues when PR gets merged + +on: + pull_request: + branches: + - develop + types: + - closed + +jobs: + add-release-label: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 # This is needed to checkout all branches + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: yarn --immutable + + - name: Get the next semver version + id: get-next-semver-version + env: + FORCE_NEXT_SEMVER_VERSION: ${{ vars.FORCE_NEXT_SEMVER_VERSION }} + run: ./development/get-next-semver-version.sh "$FORCE_NEXT_SEMVER_VERSION" + + - name: Add release label to PR and linked issues + id: add-release-label-to-pr-and-linked-issues + env: + RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} + NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }} + run: npm run add-release-label-to-pr-and-linked-issues diff --git a/.github/workflows/crowdin_action.yml b/.github/workflows/crowdin-action.yml similarity index 100% rename from .github/workflows/crowdin_action.yml rename to .github/workflows/crowdin-action.yml diff --git a/.github/workflows/fitness-functions.yml b/.github/workflows/fitness-functions.yml index 20efbf90e..6ca53a53a 100644 --- a/.github/workflows/fitness-functions.yml +++ b/.github/workflows/fitness-functions.yml @@ -17,10 +17,10 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version-file: '.nvmrc' - name: Install dependencies - run: yarn + run: yarn --immutable - name: Run fitness functions env: @@ -31,4 +31,4 @@ jobs: # files in the current directory and its subdirectories. The output is # then saved to a file called "diff". git diff "$(git merge-base "origin/$BASE_REF" HEAD)" HEAD -- . > ./diff - npm run fitness-functions -- "ci" "./diff" \ No newline at end of file + npm run fitness-functions -- "ci" "./diff" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..3d28f7886 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +name: Main + +on: + push: + branches: [develop, master] + pull_request: + +jobs: + check-workflows: + name: Check workflows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Download actionlint + id: download-actionlint + run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.23 + shell: bash + - name: Check workflow files + run: ${{ steps.download-actionlint.outputs.executable }} -color + shell: bash + + all-jobs-completed: + name: All jobs completed + runs-on: ubuntu-latest + needs: + - check-workflows + outputs: + PASSED: ${{ steps.set-output.outputs.PASSED }} + steps: + - name: Set PASSED output + id: set-output + run: echo "PASSED=true" >> "$GITHUB_OUTPUT" + + all-jobs-pass: + name: All jobs pass + if: ${{ always() }} + runs-on: ubuntu-latest + needs: all-jobs-completed + steps: + - name: Check that all jobs have passed + run: | + passed="${{ needs.all-jobs-completed.outputs.PASSED }}" + if [[ $passed != "true" ]]; then + exit 1 + fi diff --git a/.github/workflows/remove-labels-after-pr-closed.yml b/.github/workflows/remove-labels-after-pr-closed.yml new file mode 100644 index 000000000..17845f6fe --- /dev/null +++ b/.github/workflows/remove-labels-after-pr-closed.yml @@ -0,0 +1,40 @@ +name: Remove labels after issue (or PR) closed + +on: + issues: + types: [closed] + pull_request: + types: [closed] + +jobs: + cleanup: + runs-on: ubuntu-latest + + steps: + - name: Remove labels + env: + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + run: | + LABELS=( + "product-backlog" + "needs-design" + "design-in-progress" + "ready-for-dev" + "sprint-backlog" + "in-progress" + "blocked" + "needs-dev-review" + "needs-qa" + "issues-found" + "ready-for-release" + ) + for LABEL in "${LABELS[@]}"; do + curl \ + -X DELETE \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels/$LABEL" + done diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 000000000..afe3fe367 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,22 @@ +name: Sonar +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + 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 + with: + args: > + -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/update-lavamoat-policies.yml b/.github/workflows/update-lavamoat-policies.yml new file mode 100644 index 000000000..ad43fb486 --- /dev/null +++ b/.github/workflows/update-lavamoat-policies.yml @@ -0,0 +1,172 @@ +name: Update LavaMoat policies + +on: + issue_comment: + types: created + +jobs: + is-fork-pull-request: + name: Determine whether this issue comment was on a pull request from a fork + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '@metamaskbot update-policies') }} + runs-on: ubuntu-latest + outputs: + IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} + steps: + - uses: actions/checkout@v3 + - 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" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + + prepare: + name: Prepare dependencies + runs-on: ubuntu-latest + needs: is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install Yarn dependencies + run: yarn --immutable + + update-lavamoat-build-policy: + name: Update LavaMoat build policy + runs-on: ubuntu-latest + needs: + - prepare + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Update LavaMoat build policy + run: yarn lavamoat:build:auto + - name: Cache build policy + uses: actions/cache/save@v3 + with: + path: lavamoat/build-system + key: cache-build-${{ github.run_id }}-${{ github.run_attempt }} + + update-lavamoat-webapp-policy: + strategy: + matrix: + # Ensure this is synchronized with the list below in the "commit-updated-policies" job + # and with the build type list in `builds.yml` + build-type: [main, beta, flask, mmi, desktop] + name: Update LavaMoat ${{ matrix.build-type }} application policy + runs-on: ubuntu-latest + needs: + - prepare + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Update LavaMoat ${{ matrix.build-type }} policy + run: yarn lavamoat:webapp:auto:ci '--build-types=${{ matrix.build-type }}' + env: + INFURA_PROJECT_ID: 00000000000 + - name: Cache ${{ matrix.build-type }} application policy + uses: actions/cache/save@v3 + with: + path: lavamoat/browserify/${{ matrix.build-type }} + key: cache-${{ matrix.build-type }}-${{ github.run_id }}-${{ github.run_attempt }} + + commit-updated-policies: + name: Commit the updated LavaMoat policies + runs-on: ubuntu-latest + needs: + - is-fork-pull-request + - update-lavamoat-build-policy + - update-lavamoat-webapp-policy + # Ensure forks don't get access to the LavaMoat update token + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - uses: actions/checkout@v3 + with: + # Use PAT to ensure that the commit later can trigger status check workflows + token: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Restore build policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/build-system + key: cache-build-${{ github.run_id }}-${{ github.run_attempt }} + # One restore step per build type: [main, beta, flask, mmi, desktop] + # Ensure this is synchronized with the list above in the "update-lavamoat-webapp-policy" job + # and with the build type list in `builds.yml` + - name: Restore main application policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/browserify/main + key: cache-main-${{ github.run_id }}-${{ github.run_attempt }} + - name: Restore beta application policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/browserify/beta + key: cache-beta-${{ github.run_id }}-${{ github.run_attempt }} + - name: Restore flask application policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/browserify/flask + key: cache-flask-${{ github.run_id }}-${{ github.run_attempt }} + - name: Restore mmi application policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/browserify/mmi + key: cache-mmi-${{ github.run_id }}-${{ github.run_attempt }} + - name: Restore desktop application policy + uses: actions/cache/restore@v3 + with: + path: lavamoat/browserify/desktop + key: cache-desktop-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Check whether there are policy changes + id: policy-changes + run: | + if git diff --exit-code + then + echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT" + else + echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" + fi + - name: Commit the updated policies + if: steps.policy-changes.outputs.HAS_CHANGES == 'true' + run: | + git config --global user.name 'MetaMask Bot' + git config --global user.email 'metamaskbot@users.noreply.github.com' + git commit -am "Update LavaMoat policies" + git push + - name: Post comment + run: | + if [[ $HAS_CHANGES == 'true' ]] + then + gh pr comment "${PR_NUMBER}" --body 'Policies updated' + else + gh pr comment "${PR_NUMBER}" --body 'No policy changes' + fi + env: + HAS_CHANGES: ${{ steps.policy-changes.outputs.HAS_CHANGES }} + GITHUB_TOKEN: ${{ secrets.LAVAMOAT_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} diff --git a/.mocharc.js b/.mocharc.js index a8843b0e1..2ed699e6b 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -6,7 +6,6 @@ module.exports = { './app/scripts/migrations/*.test.js', './app/scripts/platforms/*.test.js', './app/scripts/controllers/app-state.test.js', - './app/scripts/controllers/network/**/*.test.js', './app/scripts/controllers/permissions/**/*.test.js', './app/scripts/controllers/mmi-controller.test.js', './app/scripts/constants/error-utils.test.js', diff --git a/.storybook/i18n.js b/.storybook/i18n.js index 314ea544e..d6c15f07c 100644 --- a/.storybook/i18n.js +++ b/.storybook/i18n.js @@ -1,6 +1,6 @@ import React, { Component, createContext, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { getMessage } from '../ui/helpers/utils/i18n-helper'; +import { getMessage } from '../shared/modules/i18n'; import { I18nContext } from '../ui/contexts/i18n'; export { I18nContext }; diff --git a/.yarn/patches/@metamask-signature-controller-npm-3.0.0-8771b6885e.patch b/.yarn/patches/@metamask-signature-controller-npm-3.0.0-8771b6885e.patch deleted file mode 100644 index 4a2682995..000000000 --- a/.yarn/patches/@metamask-signature-controller-npm-3.0.0-8771b6885e.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/dist/SignatureController.js b/dist/SignatureController.js -index b39d274f4547ab4e8b647293199ec21c4a9e38ca..288e55c97c3e4a234874dd8b8986ba77576b0dc4 100644 ---- a/dist/SignatureController.js -+++ b/dist/SignatureController.js -@@ -308,12 +308,12 @@ _SignatureController_keyringController = new WeakMap(), _SignatureController_isE - const messageId = msgParams.metamaskId; - try { - const cleanMessageParams = yield messageManager.approveMessage(msgParams); -+ __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_acceptApproval).call(this, messageId); - const signature = yield getSignature(cleanMessageParams); - this.hub.emit(`${methodName}:signed`, { signature, messageId }); - if (!cleanMessageParams.deferSetAsSigned) { - messageManager.setMessageStatusSigned(messageId, signature); - } -- __classPrivateFieldGet(this, _SignatureController_instances, "m", _SignatureController_acceptApproval).call(this, messageId); - return __classPrivateFieldGet(this, _SignatureController_getAllState, "f").call(this); - } - catch (error) { diff --git a/README.md b/README.md index 9816c26fe..f503ca82f 100644 --- a/README.md +++ b/README.md @@ -147,19 +147,22 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack - The `allow-scripts` configuration in `package.json` - Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary. - Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies. -- The LavaMoat policy files. The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details: - - There are two sets of LavaMoat policy files: - - The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`. Add `--help` for usage. - - These should be regenerated whenever the production dependencies for the background change. - - The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`. - - This should be regenerated whenever the dependencies used by the build system itself change. - - Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate. - - Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms. - macOS and Windows users may see extraneous changes relating to optional dependencies. - - If you keep getting policy failures even after regenerating the policy files, try regenerating the policies after a clean install by doing: - - `rm -rf node_modules/ && yarn && yarn lavamoat:auto` - - Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis. - Refer to the LavaMoat documentation or ask for help if you run into any issues. +- The LavaMoat policy files + - If you are a MetaMask team member and your PR is on a repository branch, you can use the bot command `@metamaskbot update-policies` to ask the MetaMask bot to automatically update the policies for you. + - If your PR is from a fork, you can ask a MetaMask team member to help with updating the policy files. + - Manual update instructions: The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details: + - There are two sets of LavaMoat policy files: + - The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`. Add `--help` for usage. + - These should be regenerated whenever the production dependencies for the background change. + - The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`. + - This should be regenerated whenever the dependencies used by the build system itself change. + - Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate. + - Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms. + macOS and Windows users may see extraneous changes relating to optional dependencies. + - If you keep getting policy failures even after regenerating the policy files, try regenerating the policies after a clean install by doing: + - `rm -rf node_modules/ && yarn && yarn lavamoat:auto` + - Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis. + Refer to the LavaMoat documentation or ask for help if you run into any issues. ## Architecture diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 52d391c4a..248f25f40 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1562,6 +1562,12 @@ "message": "Betrüger aber schon.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Halten, um GWP anzuzeigen" + }, + "holdToRevealSRPTitle": { + "message": "Bewahren Sie Ihre GWP sicher auf" + }, "ignoreAll": { "message": "Alle ignorieren" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Ausloggen" }, - "lockTimeTooGreat": { - "message": "Sperrzeit ist zu groß" - }, "logo": { "message": "$1-Logo", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Das Einfügen schlug fehl, weil sie mehr als 24 Wörter enthielt. Eine geheime Wiederherstellungsphrase darf maximal 24 Wörter enthalten.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Sie können Ihre gesamte geheime Wiederherstellungsphrase in ein beliebiges Feld einfügen", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Anfangen" + }, + "srpSecurityQuizIntroduction": { + "message": "Zur Enthüllung Ihrer geheimen Wiederherstellungsphrase, müssen Sie zwei Fragen beantworten" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Wenn Sie Ihre geheime Wiederherstellungsphrase verlieren, kann MetaMask ..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Ihnen nicht helfen" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Schreiben Sie sie auf, gravieren Sie sie in Metall ein oder bewahren Sie sie an mehreren geheimen Orten auf, damit Sie sie niemals verlieren. Sollten Sie sie verlieren, ist sie für immer weg." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Richtig! Niemand kann Ihnen dabei helfen, Ihre geheime Wiederherstellungsphrase zurückzubekommen" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "diese für Sie zurückzubekommen" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Wenn Sie Ihre geheime Wiederherstellungsphrase verlieren, ist diese für immer verloren. Niemand kann Ihnen dabei helfen, sie zurückzubekommen, egal, was behauptet wird." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Falsch! Niemand kann Ihnen dabei helfen, Ihre geheime Wiederherstellungsphrase zurückzubekommen" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Sollte jemand, selbst ein Support-Mitarbeiter, nach Ihrer geheimen Wiederherstellungsphrase fragen ..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Werden Sie betrogen" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Jeder, der behauptet, Ihre gemeine Wiederherstellungsphrase zu benötigen, lügt Sie an. Wenn Sie diese mit solchen Personen teilen, werden diese Ihre Vermögenswerte stehlen." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Richtig! Es ist nie eine gute Idee, Ihre geheime Wiederherstellungsphrase mit anderen zu teilen" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Sie sollten sie dieser Person geben" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Jeder, der behauptet, Ihre gemeine Wiederherstellungsphrase zu benötigen, lügt Sie an. Wenn Sie diese mit einer solchen Person teilen, wird sie Ihre Vermögenswerte stehlen." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Nein! Teilen Sie Ihre geheime Wiederherstellungsphrase mit niemandem, niemals" + }, + "srpSecurityQuizTitle": { + "message": "Sicherheits-Quiz" + }, "srpToggleShow": { "message": "Dieses Wort der geheimen Wiederherstellungsphrase anzeigen/ausblenden", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 7403c072b..9361b3725 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1562,6 +1562,12 @@ "message": "αλλά οι απατεώνες μπορεί να το κάνουν.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Κρατήστε το πατημένο για να αποκαλυφθεί το ΜΦΑ" + }, + "holdToRevealSRPTitle": { + "message": "Κρατήστε το ΜΦΑ σας ασφαλές" + }, "ignoreAll": { "message": "Αγνόηση όλων" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Αποσύνδεση" }, - "lockTimeTooGreat": { - "message": "Ο χρόνος κλειδώματος είναι πολύ μεγάλος" - }, "logo": { "message": "Λογότυπο $1", "description": "$1 is the name of the ticker" @@ -3330,12 +3333,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Η επικόλληση απέτυχε επειδή περιείχε περισσότερες από 24 λέξεις. Μια μυστική φράση ανάκτησης μπορεί να αποτελείται από το πολύ 24 λέξεις.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Μπορείτε να επικολλήσετε ολόκληρη τη μυστική φράση ανάκτησής σας σε οποιοδήποτε πεδίο", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Ξεκινήστε" + }, + "srpSecurityQuizIntroduction": { + "message": "Για να σας αποκαλύψουμε τη Μυστική Φράση Ανάκτησης, πρέπει να απαντήσετε σωστά σε δύο ερωτήσεις" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Αν χάσετε τη Μυστική Φράση Ανάκτησης, το MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Δεν μπορεί να σας βοηθήσει" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Γράψτε την κάπου, χαράξτε την πάνω σε μέταλλο ή κρατήστε την σε πολλά μυστικά σημεία για να μην την χάσετε ποτέ. Αν την χάσετε, θα χαθεί για πάντα." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Σωστά! Κανείς δεν μπορεί να σας βοηθήσει να επαναφέρετε τη Μυστική Φράση Ανάκτησης" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Μπορεί να την επαναφέρει για εσάς" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Αν χάσετε τη Μυστική Φράση Ανάκτησης, θα την χάσετε για πάντα. Κανείς δεν μπορεί να σας βοηθήσει να την επαναφέρετε, ό,τι κι αν σας πει." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Λάθος! Κανείς δεν μπορεί να σας βοηθήσει να επαναφέρετε τη Μυστική Φράση Ανάκτησης" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Αν κάποιος, ακόμα και ένας τεχνικός υποστήριξης, σας ζητήσει τη Μυστική Φράση Ανάκτησης..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Σας έχουν εξαπατήσει" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Όποιος ισχυριστεί ότι χρειάζεται τη Μυστική Φράση Ανάκτησης, σας λέει ψέματα. Αν την μοιραστείτε μαζί του, θα κλέψει τα περιουσιακά σας στοιχεία." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Σωστά! Το να μοιράζεστε τη Μυστική Φράση Ανάκτησης δεν είναι καλή ιδέα" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Πρέπει να τους την δώσετε" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Όποιος ισχυριστεί ότι χρειάζεται τη Μυστική Φράση Ανάκτησης, σας λέει ψέματα. Αν την μοιραστείτε μαζί του, θα κλέψει τα περιουσιακά σας στοιχεία." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Όχι! Ποτέ μα ποτέ μην μοιραστείτε με κανέναν τη Μυστική Φράση Ανάκτησης" + }, + "srpSecurityQuizTitle": { + "message": "Κουίζ Ασφαλείας" + }, "srpToggleShow": { "message": "Εμφάνιση/Απόκρυψη αυτής της λέξης από τη μυστική φράση ανάκτησης", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index d3c554cc1..b0ef001ba 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2171,8 +2171,8 @@ "lockMetaMask": { "message": "Lock MetaMask" }, - "lockTimeTooGreat": { - "message": "Lock time is too great" + "lockTimeInvalid": { + "message": "Lock time must be a number between 0 and 10080" }, "logo": { "message": "$1 logo", @@ -2559,6 +2559,30 @@ "notePlaceholder": { "message": "The approver will see this note when approving the transaction at the custodian." }, + "notificationTransactionFailedMessage": { + "message": "Transaction $1 failed! $2", + "description": "Content of the browser notification that appears when a transaction fails" + }, + "notificationTransactionFailedMessageMMI": { + "message": "Transaction failed! $1", + "description": "Content of the browser notification that appears when a transaction fails in MMI" + }, + "notificationTransactionFailedTitle": { + "message": "Failed transaction", + "description": "Title of the browser notification that appears when a transaction fails" + }, + "notificationTransactionSuccessMessage": { + "message": "Transaction $1 confirmed!", + "description": "Content of the browser notification that appears when a transaction is confirmed" + }, + "notificationTransactionSuccessTitle": { + "message": "Confirmed transaction", + "description": "Title of the browser notification that appears when a transaction is confirmed" + }, + "notificationTransactionSuccessView": { + "message": "View on $1", + "description": "Additional content in browser notification that appears when a transaction is confirmed and has a block explorer URL" + }, "notifications": { "message": "Notifications" }, @@ -4000,12 +4024,66 @@ }, "srpPasteFailedTooManyWords": { "message": "Paste failed because it contained over 24 words. A secret recovery phrase can have a maximum of 24 words.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "You can paste your entire secret recovery phrase into any field", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Get started" + }, + "srpSecurityQuizImgAlt": { + "message": "An eye with a keyhole in the center, and three floating password fields" + }, + "srpSecurityQuizIntroduction": { + "message": "To reveal your Secret Recovery Phrase, you need to correctly answer two questions" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "If you lose your Secret Recovery Phrase, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Can’t help you" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Write it down, engrave it on metal, or keep it in multiple secret spots so you never lose it. If you lose it, it’s gone forever." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Right! No one can help get your Secret Recovery Phrase back" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Can get it back for you" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "If you lose your Secret Recovery Phrase, it’s gone forever. No one can help you get it back, no matter what they might say." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Wrong! No one can help get your Secret Recovery Phrase back" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "If anyone, even a support agent, asks for your Secret Recovery Phrase..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "You’re being scammed" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Anyone claiming to need your Secret Recovery Phrase is lying to you. If you share it with them, they will steal your assets." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Correct! Sharing your Secret Recovery Phrase is never a good idea" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "You should give it to them" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Anyone claiming to need your Secret Recovery Phrase is lying to you. If you share it with them, they will steal your assets." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Nope! Never share your Secret Recovery Phrase with anyone, ever" + }, + "srpSecurityQuizTitle": { + "message": "Security quiz" + }, "srpToggleShow": { "message": "Show/Hide this word of the secret recovery phrase", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index d290bc5a3..387497661 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1562,6 +1562,12 @@ "message": "pero los defraudadores sí.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Mantén presionado para revelar su SRP" + }, + "holdToRevealSRPTitle": { + "message": "Proteja su SRP" + }, "ignoreAll": { "message": "Ignorar todo" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Bloquear" }, - "lockTimeTooGreat": { - "message": "El tiempo de bloqueo es demasiado largo" - }, "logo": { "message": "Logo de $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Pegar falló porque contenía más de 24 palabras. Una frase de recuperación secreta puede tener un máximo de 24 palabras.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Puede pegar toda su frase secreta de recuperación en cualquier campo", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Iniciar" + }, + "srpSecurityQuizIntroduction": { + "message": "Para revelar su frase secreta de recuperación, debe responder correctamente dos preguntas" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Si extravía su frase secreta de recuperación, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "No puede ayudarlo" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Anótela, grábela en metal o guárdela en múltiples lugares secretos para que nunca la pierda. Si la extravía, se ha ido para siempre." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "¡Correcto! Nadie puede ayudarlo a recuperar su frase secreta de recuperación" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Puede recuperarla para usted" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Si pierde su frase secreta de recuperación, ésta desaparecerá para siempre. Nadie puede ayudarle a recuperarla, sin importar lo que digan." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "¡Incorrecto! Nadie puede ayudarlo a recuperar su frase secreta de recuperación" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Si alguien, incluso un agente de soporte, le pide su frase secreta de recuperación..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Lo están estafando" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Cualquiera que afirme necesitar su frase secreta de recuperación le está mintiendo. Si la comparte, le robarán sus activos." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "¡Correcto! Compartir su frase secreta de recuperación nunca es una buena idea" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Debiera brindársela" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Cualquiera que afirme necesitar su frase secreta de recuperación le está mintiendo. Si la comparte, le robarán sus activos." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "¡No! Nunca comparta su frase secreta de recuperación con nadie, nunca" + }, + "srpSecurityQuizTitle": { + "message": "Cuestionario de seguridad" + }, "srpToggleShow": { "message": "Mostrar/Ocultar esta palabra de la frase secreta de recuperación", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 93431b1b2..6562f505a 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1269,9 +1269,6 @@ "lock": { "message": "Bloquear" }, - "lockTimeTooGreat": { - "message": "El tiempo de bloqueo es demasiado largo" - }, "low": { "message": "Baja" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 39fb2dc33..ea020f961 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1562,6 +1562,12 @@ "message": "mais les hameçonneurs pourraient le faire.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Appuyez longuement pour révéler PSR" + }, + "holdToRevealSRPTitle": { + "message": "Protégez votre PSR" + }, "ignoreAll": { "message": "Ignorer tout" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Déconnexion" }, - "lockTimeTooGreat": { - "message": "Le temps de verrouillage est trop important" - }, "logo": { "message": "Logo $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Le collage a échoué parce que la phrase contenait plus de 24 mots. Une phrase secrète de récupération peut contenir un maximum de 24 mots.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Vous pouvez coller toute votre phrase de récupération secrète dans n’importe quel champ", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Commencer" + }, + "srpSecurityQuizIntroduction": { + "message": "Pour révéler votre Phrase secrète de récupération, vous devez répondre correctement à deux questions" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Si vous perdez votre Phrase secrète de récupération, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Ne pourra pas vous aider" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Gravez-la sur une plaque en métal ou inscrivez-la sur plusieurs bouts de papier et cachez-les dans différents endroits secrets pour ne jamais la perdre. Si vous la perdez, il n'y a aucun moyen de la récupérer." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "En effet ! Personne ne peut vous aider à récupérer votre Phrase secrète de récupération." + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Pourra la récupérer pour vous" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Personne ne peut vous aider à récupérer votre phrase secrète de récupération si jamais vous la perdez." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "C'est faux ! Personne ne peut vous aider à récupérer votre Phrase secrète de récupération." + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Si un membre du service d'assistance ou toute autre personne vous demande votre Phrase secrète de récupération..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Ne la lui fournissez pas, car cette personne essaie de vous arnaquer." + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Toute personne qui vous demande votre phrase secrète de récupération, que ce soit pour des raisons de sécurité ou autre, essaie de vous arnaquer." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "C'est exact ! Vous ne devez jamais partager votre Phrase secrète de récupération avec qui que ce soit." + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Vous devez la lui fournir" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Toute personne qui vous demande votre phrase secrète de récupération, que ce soit pour des raisons de sécurité ou autre, essaie de vous arnaquer." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "C'est faux ! Vous ne devez jamais partager votre Phrase secrète de récupération avec qui que ce soit." + }, + "srpSecurityQuizTitle": { + "message": "Quiz sur la sécurité" + }, "srpToggleShow": { "message": "Afficher / Masquer ce mot de la phrase de récupération secrète", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 87eb9b33a..5a6757562 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1562,6 +1562,12 @@ "message": "लेकिन फिशर कर सकते हैं।", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "SRP दिखाने के लिए होल्ड करें" + }, + "holdToRevealSRPTitle": { + "message": "अपना SRP सुरक्षित रखें" + }, "ignoreAll": { "message": "सभी को अनदेखा करें" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "लॉक" }, - "lockTimeTooGreat": { - "message": "लॉक समय बहुत अधिक है" - }, "logo": { "message": "$1 लोगो", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "पेस्ट विफल हुआ क्योंकि उसमें 24 से ज़्यादा शब्द हैं। सीक्रेट रिकवरी फ़्रेज़ में अधिकतम 24 शब्द हो सकते हैं।", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "आप अपना पूरा सीक्रेट रिकवरी फ़्रेज किसी भी फ़ील्ड में पेस्ट कर सकते हैं", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "शुरू करें" + }, + "srpSecurityQuizIntroduction": { + "message": "अपना सीक्रेट रिकवरी फ्रेज़ प्रकट करने के लिए, आपको दो प्रश्नों का सही उत्तर देना होगा" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "आपकी मदद नहीं कर सकता" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "इसे लिख लें, इसे किसी धातु पर उकेर दें, या इसे कई गुप्त स्थानों पर रखें ताकि आप इसे कभी न खोएं। यदि आप इसे खो देते हैं, तो यह हमेशा के लिए चला जाता है।" + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "सही! आपके सीक्रेट रिकवरी फ्रेज़ को वापस पाने में कोई भी सहायता नहीं कर सकता" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "आपके लिए इसे वापस ला सकते हैं" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "यदि आप अपना सीक्रेट रिकवरी फ्रेज़ खो देते हैं, तो यह हमेशा के लिए चला जाता है। इसे वापस पाने में कोई भी आपकी मदद नहीं कर सकता, चाहे वे कुछ भी कहें।" + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "गलत! आपके सीक्रेट रिकवरी फ्रेज़ को वापस पाने में कोई भी सहायता नहीं कर सकता" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "यदि कोई, यहां तक कि एक सहायक एजेंट भी, आपका सीक्रेट रिकवरी फ्रेज़ मांगता है..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "तो आपके साथ धोखा किया जा रहा है" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "आपके सीक्रेट रिकवरी फ्रेज़ की आवश्यकता का दावा करने वाला कोई भी व्यक्ति आपसे झूठ बोल रहा है। यदि आप इसे उनके साथ साझा करते हैं, तो वे आपकी संपत्ति चुरा लेंगे।" + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "सही! अपना सीक्रेट रिकवरी फ्रेज़ साझा करना कभी भी अच्छा विचार नहीं है" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "आपको उन्हें यह देना चाहिए" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "आपके सीक्रेट रिकवरी फ्रेज़ की आवश्यकता का दावा करने वाला कोई भी व्यक्ति आपसे झूठ बोल रहा है। यदि आप इसे उनके साथ साझा करते हैं, तो वे आपकी संपत्तियां चुरा लेंगे।" + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "नहीं! अपने सीक्रेट रिकवरी फ्रेज़ को कभी भी किसी के साथ साझा न करें" + }, + "srpSecurityQuizTitle": { + "message": "सुरक्षा प्रश्नोत्तरी" + }, "srpToggleShow": { "message": "सीक्रेट रिकवरी फ़्रेज का ये शब्द दिखाएं/छुपाएं", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 30e982f40..da9df8643 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1562,6 +1562,12 @@ "message": "tetapi penipu akan mencoba memintanya.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Tahan untuk mengungkap FPR" + }, + "holdToRevealSRPTitle": { + "message": "Jaga keamanan FPR Anda" + }, "ignoreAll": { "message": "Abaikan semua" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Kunci" }, - "lockTimeTooGreat": { - "message": "Lock time terlalu besar" - }, "logo": { "message": "Logo $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Gagal ditempel karena memuat lebih dari 24 kata. Frasa pemulihan rahasia dapat memuat maksimum 24 kata.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Anda bisa menempelkan seluruh frasa pemulihan rahasia ke bagian mana pun", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Mulai" + }, + "srpSecurityQuizIntroduction": { + "message": "Untuk mengungkapkan Frasa Pemulihan Rahasia, Anda perlu menjawab dua pertanyaan dengan benar" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Jika Anda kehilangan Frasa Pemulihan Rahasia, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Tidak dapat membantu Anda" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Catat, ukir pada logam, atau simpan di beberapa tempat rahasia agar Anda tidak pernah kehilangan. Jika Anda kehilangan, maka akan hilang selamanya." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Benar! Tidak ada yang dapat membantu mengembalikan Frasa Pemulihan Rahasia Anda" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Dapat mengembalikannya untuk Anda" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Jika Anda kehilangan Frasa Pemulihan Rahasia, maka akan hilang selamanya. Tidak ada yang dapat membantu Anda mengembalikannya, apa pun yang mereka katakan." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Salah! Tidak ada yang dapat membantu mengembalikan Frasa Pemulihan Rahasia Anda" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Jika ada yang menanyakan Frasa Pemulihan Rahasia Anda, bahkan agen pendukung,..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Anda ditipu" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Siapa pun yang mengaku membutuhkan Frasa Pemulihan Rahasia, mereka berbohong kepada Anda. Jika membaginya, mereka akan mencuri aset Anda." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Benar! Membagikan Frasa Pemulihan Rahasia bukanlah ide yang baik" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Anda harus memberikannya kepada mereka" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Siapa pun yang mengaku membutuhkan Frasa Pemulihan Rahasia, mereka berbohong kepada Anda. Jika membaginya, mereka akan mencuri aset Anda." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Tidak! Jangan pernah membagikan Frasa Pemulihan Rahasia kepada siapa pun" + }, + "srpSecurityQuizTitle": { + "message": "Kuis keamanan" + }, "srpToggleShow": { "message": "Tampilkan/Sembunyikan kata dari frasa pemulihan rahasia ini", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 4ba71fa64..d18255efd 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1117,9 +1117,6 @@ "lock": { "message": "Disconnetti" }, - "lockTimeTooGreat": { - "message": "Tempo di inattività troppo lungo" - }, "mainnet": { "message": "Rete Ethereum Principale" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index ea053b713..fee446d3e 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1562,6 +1562,12 @@ "message": "もし尋ねられた場合はフィッシング詐欺の可能性があります。", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "長押ししてSRPを表示" + }, + "holdToRevealSRPTitle": { + "message": "SRPは安全に保管してください" + }, "ignoreAll": { "message": "すべて無視" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "ロック" }, - "lockTimeTooGreat": { - "message": "ロック時間が長すぎます" - }, "logo": { "message": "$1 ロゴ", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "24 を超える単語が含まれていたため、貼り付けに失敗しました。秘密のリカバリーフレーズは 24 語までです。", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "秘密のリカバリーフレーズ全体をいずれかのフィールドに張り付けできます。", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "開始" + }, + "srpSecurityQuizIntroduction": { + "message": "秘密のリカバリーフレーズを表示するには、2 つの質問に正しく答える必要があります。" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "秘密のリカバリーフレーズをなくした場合、MetaMask は..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "どうすることもできません" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "書き留めたり金属に掘ったり、いくつかの秘密の場所に保管したりして、絶対になくさないようにしてください。なくした場合、一生戻ってきません。" + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "正解です!秘密のリカバリーフレーズは誰にも取り戻すことができません" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "それを取り戻すことができます" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "秘密のリカバリーフレーズをなくした場合、一生戻ってきません。誰が何と言おうと、誰にも取り戻すことはできません。" + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "不正解!秘密のリカバリーフレーズは誰にも取り戻せません" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "誰かに秘密のリカバリーフレーズを尋ねられたら、それがサポート担当者であっても..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "あなたは騙されようとしています" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "秘密のリカバリーフレーズが必要だと言われたら、それは嘘です。教えてしまったら資産を盗まれます。" + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "正解です!秘密のリカバリーフレーズは決して誰にも教えてはいけません" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "教えるべきです" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "秘密のリカバリーフレーズが必要だと言われたら、それは嘘です。教えてしまったら資産を盗まれます。" + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "不正解!秘密のリカバリーフレーズは決して誰にも教えないでください" + }, + "srpSecurityQuizTitle": { + "message": "セキュリティの質問" + }, "srpToggleShow": { "message": "秘密のリカバリーフレーズのこの単語を表示・非表示", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 440e0512a..46e74e3da 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1562,6 +1562,12 @@ "message": "오히려 피싱 사기꾼들이 요구할 수 있으니 주의가 필요합니다.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "눌러서 SRP 정보를 확인하세요" + }, + "holdToRevealSRPTitle": { + "message": "SRP 정보를 안전하게 보관하세요" + }, "ignoreAll": { "message": "모두 무시" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "잠금" }, - "lockTimeTooGreat": { - "message": "잠금 시간이 너무 깁니다" - }, "logo": { "message": "$1 로고", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "단어가 24개를 초과하여 붙여넣기에 실패했습니다. 비밀 복구 구문은 24개 이하의 단어로 이루어집니다.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "비밀 복구 구문 전체를 아무 입력란에 붙여넣을 수 있습니다", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "시작하기" + }, + "srpSecurityQuizIntroduction": { + "message": "비밀 복구 구문을 찾으려면 두 가지 질문에 올바르게 답해야 합니다" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "비밀 복구 구문을 분실하시면 MetaMask가..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "도와드릴 수 없습니다" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "따라서 이를 적거나, 금속 등에 새기거나, 여러 비밀 장소에 보관하여 절대로 잃어버리지 않도록 하세요. 한 번 잃어버리면 영원히 찾을 수 없습니다." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "맞습니다! 아무도 본인의 비밀 복구 구문을 복구할 수 없습니다" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "찾아드릴 수 있습니다" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "비밀 복구 구문은 한 번 잃어버리면 영원히 찾을 수 없습니다. 누가 뭐라고 해도 아무도 이를 찾아드리지 못합니다." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "아닙니다! 아무도 본인의 비밀 복구 구문을 복구할 수 없습니다" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "누군가, 심지어 고객 센터 직원이라고 해도 여러분의 비밀 복구 구문을 물어본다면..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "이는 반드시 사기입니다" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "비밀 복구 구문이 필요하다고 하는 사람은 모두 거짓말쟁이입니다. 그런 자들과 비밀 복구 구문을 공유하면 자산을 도둑맞게 됩니다." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "맞습니다! 비밀 복구 구문은 아무와도 공유하면 안 됩니다" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "주어야 합니다" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "비밀 복구 구문이 필요하다고 하는 사람은 모두 거짓말쟁이입니다. 그런 자들과 비밀 복구 구문을 공유하면 자산을 도둑맞게 됩니다." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "맞습니다! 비밀 복구 구문은 절대로 아무와도 공유하면 안 됩니다" + }, + "srpSecurityQuizTitle": { + "message": "보안 퀴즈" + }, "srpToggleShow": { "message": "비밀 복구 구문 중에서 이 단어 공개하기/숨기기", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index c2c399b48..8b0947625 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -816,9 +816,6 @@ "lock": { "message": "I-lock" }, - "lockTimeTooGreat": { - "message": "Masyadong matagal ang oras ng pag-lock" - }, "mainnet": { "message": "Ethereum Mainnet" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 6b46b4c39..1c26903ed 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1562,6 +1562,12 @@ "message": "mas os phishers talvez solicitem.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Mantenha pressionado para revelar FRS" + }, + "holdToRevealSRPTitle": { + "message": "Mantenha sua FRS protegida" + }, "ignoreAll": { "message": "Ignorar tudo" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Sair" }, - "lockTimeTooGreat": { - "message": "O tempo de bloqueio é longo demais" - }, "logo": { "message": "Logotipo do $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "A função colar falhou porque continha mais de 24 palavras. Uma frase secreta de recuperação pode ter no máximo 24 palavras.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Você pode colar a sua frase secreta de recuperação inteira em qualquer campo", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Começar" + }, + "srpSecurityQuizIntroduction": { + "message": "Para revelar sua Frase de Recuperação Secreta, você precisa responder corretamente duas perguntas" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Se você perder sua Frase de Recuperação Secreta, a MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Não poderá ajudar" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Anote-a, grave em metal ou guarde-a em diversos lugares secretos para que nunca a perca. Se perdê-la, é para sempre." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Certo! Ninguém pode ajudar a recuperar sua Frase de Recuperação Secreta" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Poderá recuperá-la para você" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Se você perder sua Frase de Recuperação Secreta, é para sempre. Ninguém consegue ajudar a recuperá-la, não importa o que digam." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Errado! Ninguém consegue recuperar sua Frase de Recuperação Secreta" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Se alguém, até mesmo um atendente do suporte, pedir sua Frase de Recuperação Secreta..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Você estará sendo vítima de um golpe" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Qualquer pessoa que afirme precisar da sua Frase de Recuperação Secreta está mentindo. Se você compartilhar com ela, seus ativos serão roubados." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Correto! Compartilhar sua Frase de Recuperação Secreta nunca é uma boa ideia" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Você deverá revelar" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Qualquer pessoa que afirme precisar da sua Frase de Recuperação Secreta está mentindo. Se você compartilhar com ela, seus ativos serão roubados." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Não! Não compartilhe sua Frase de Recuperação Secreta com ninguém, nunca" + }, + "srpSecurityQuizTitle": { + "message": "Quiz de segurança" + }, "srpToggleShow": { "message": "Mostrar/Ocultar esta palavra da frase secreta de recuperação", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 24e06aec1..c299dbfed 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1269,9 +1269,6 @@ "lock": { "message": "Bloquear" }, - "lockTimeTooGreat": { - "message": "O tempo de bloqueio é longo demais" - }, "low": { "message": "Baixa" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0383594a4..8fedcfa31 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1562,6 +1562,12 @@ "message": "но злоумышленники-фишеры могут.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Удерживайте, чтобы показать СФВ" + }, + "holdToRevealSRPTitle": { + "message": "Храните СФВ в безопасности" + }, "ignoreAll": { "message": "Игнорировать все" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Заблокировать" }, - "lockTimeTooGreat": { - "message": "Время блокировки слишком велико" - }, "logo": { "message": "логотип $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Не удалось вставить, так как он содержит более 24 слов. Секретная фраза для восстановления может содержать не более 24 слов.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Вы можете вставить всю свою секретную фразу для восстановления в любое поле", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Начать" + }, + "srpSecurityQuizIntroduction": { + "message": "Чтобы увидеть свою секретную фразу для восстановления, вам нужно правильно ответить на два вопроса" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Если вы потеряете свою секретную фразу для восстановления, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Не сможет вам помочь" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Запишите ее, выгравируйте ее на металле или храните в нескольких потайных местах, чтобы никогда не потерять. Если вы потеряете ее, она пропадет навсегда." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Правильно! Никто не может помочь вернуть вашу секретную фразу для восстановления" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Не сможет вернуть ее вам" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Если вы потеряете свою секретную фразу для восстановления, она пропадет навсегда. Никто не может помочь вам вернуть ее, что бы кто ни говорил." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Неправильно! Никто не может помочь вернуть вашу секретную фразу для восстановления" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Если кто-нибудь, даже представитель службы поддержки, попросит вашу секретную фразу для восстановления..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Вас обманывают" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Любой, кто утверждает, что ему нужна ваша секретная фраза для восстановления, лжет вам. Если вы сообщите эту фразу ему (ей), он(-а) украдет ваши активы." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Правильно! Сообщать кому-либо своей секретную фразу для восстановления — это всегда плохая идея" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Вы должны сообщите фразу ему (ей)" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Любой, кто утверждает, что ему нужна ваша секретная фраза для восстановления, лжет вам. Если вы сообщите эту фразу ему (ей), он(-а) украдет ваши активы." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Нет! Никогда никому не сообщайте никому свою секретную фразу для восстановления" + }, + "srpSecurityQuizTitle": { + "message": "Тест по безопасности" + }, "srpToggleShow": { "message": "Показать/скрыть это слово секретной фразы для восстановления", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index ef244c616..9318c44cf 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1562,6 +1562,12 @@ "message": "ngunit maaring hingin ng mga phisher.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "I-hold para ipakita ang SRP" + }, + "holdToRevealSRPTitle": { + "message": "Panatilihing ligtas ang iyong SRP" + }, "ignoreAll": { "message": "Huwag pansinin ang lahat" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "I-lock" }, - "lockTimeTooGreat": { - "message": "Masyadong matagal ang oras ng pag-lock" - }, "logo": { "message": "$1 logo", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Nabigong i-paste dahil naglalaman ito ng higit sa 24 na salita. Ang secret recovery phrase ay mayroong hanggang 24 na salita lamang.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Maaari mong i-paste ang iyong buong secret recovery phrase sa alinmang patlang", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Magsimula" + }, + "srpSecurityQuizIntroduction": { + "message": "Upang ipakita ang iyong Secret Recovery Phrase, kailangan mong sagutin nang tama ang dalawang tanong" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Kung mawala mo ang iyong Secret Recovery Phrase, ang MetaMask ay..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Hindi ka matutulungan" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Isulat ito, iukit sa metal, o itago ito sa maraming lihim na lugar upang hindi ito mawala. Kung nawala mo ito, wala na ito ng tuluyan." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Tama! Walang makakatulong na maibalik ang iyong Secret Recovery Phrase" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Maaari itong ibalik para sa iyo" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Kung nawala mo ang iyong Secret Recovery Phrase mawawala na ito nang tuluyan. Walang makakatulong sa iyo na maibalik ito, anuman ang maaaring sabihin nila." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Mali! Walang makakatulong na maibalik ang iyong Secret Recovery Phrase" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Kung sinuman, kahit isang ahente ng suporta, ay humingi ng iyong Secret Recovery Phrase..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Niloloko ka" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Sinumang nagsasabing nangangailangan ng iyong Secret Recovery Phrase ay nagsisinungaling sa iyo. Kung ibabahagi mo ito sa kanila, nanakawin nila ng iyong mga ari-arian." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Tama! Ang pagbabahagi ng iyong Secret Recovery Phrase ay hindi kailanman isang magandang ideya" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Dapat mong ibigay sa kanila" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Sinumang nagsasabing nangangailangan ng iyong Secret Recovery Phrase ay nagsisinungaling sa iyo. Kung ibabahagi mo ito sa kanila, nanakawin nila ng iyong mga ari-arian." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Hindi! Huwag kailanman ibahagi ang iyong Secret Recovery Phrase sa sinuman, kailanman" + }, + "srpSecurityQuizTitle": { + "message": "Pagsusulit sa seguridad" + }, "srpToggleShow": { "message": "Ipakita/Itago ang salitang ito ng secret recovery phrase", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3ed1752b6..0e5bab809 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1562,6 +1562,12 @@ "message": "ancak dolandırıcılar talep edilebilir.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "GKİ bilgisinin gösterilmesi için tut" + }, + "holdToRevealSRPTitle": { + "message": "GKİ bilgini güvende tut" + }, "ignoreAll": { "message": "Tümünü yoksay" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Kilitle" }, - "lockTimeTooGreat": { - "message": "Kilitleme süresi çok fazla" - }, "logo": { "message": "$1 logosu", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "24'ten fazla sözcük içerdiği için yapıştırma başarısız oldu. Gizli bir kurtarma ifadesi en fazla 24 sözcükten oluşabilir.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Gizli kurtarma ifadenin tamamını herhangi bir alana yapıştırabilirsin", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Başla" + }, + "srpSecurityQuizIntroduction": { + "message": "Gizli Kurtarma İfadenizi görmek için iki soruyu doğru cevaplamanız gerekmektedir" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Gizli Kurtarma İfadenizi kaybederseniz MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Size yardımcı olamaz" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Bir yere yazın, bir metalin üzerine kazıyın veya asla kaybetmemeniz için birden fazla noktada saklayın. Kaybederseniz sonsuza dek kaybolur." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Doğru! Hiç kimse Gizli Kurtarma İfadenizi geri almanıza yardımcı olamaz" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Size onu tekrar verebilir" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Gizli Kurtarma İfadenizi kaybederseniz sonsuza dek kaybolur. Söylediklerinin ne olduğuna bakılmaksızın hiç kimse onu geri almanıza yardımcı olamaz." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Yanlış! Hiç kimse Gizli Kurtarma İfadenizi geri almanıza yardımcı olamaz" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Herhangi birisi, bir destek temsilcisi bile sizden Gizli Kurtarma İfadenizi isterse..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Dolandırılıyorsunuzdur" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Gizli Kurtarma İfadenizi isteyen kişi size yalan söylüyordur. Kendisi ile paylaşırsanız varlıklarınızı çalacaktır." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Doğru! Gizli Kurtarma İfadenizi paylaşmak asla iyi bir fikir değildir" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Kendisine vermelisiniz" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Gizli Kurtarma İfadenizi isteyen kişi size yalan söylüyordur. Kendisi ile paylaşırsanız varlıklarınızı çalacaktır." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Hayır! Gizli Kurtarma İfadenizi asla hiç kimse ile paylaşmayın, asla" + }, + "srpSecurityQuizTitle": { + "message": "Güvenlik testi" + }, "srpToggleShow": { "message": "Gizli kurtarma ifadesinin bu sözcüğünü göster/gizle", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 50ff318e1..89b4aff06 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1562,6 +1562,12 @@ "message": "nhưng những kẻ lừa đảo qua mạng thì có.", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "Giữ để hiển thị SRP" + }, + "holdToRevealSRPTitle": { + "message": "Đảm bảo an toàn cho SRP của bạn" + }, "ignoreAll": { "message": "Bỏ qua tất cả" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "Khóa" }, - "lockTimeTooGreat": { - "message": "Thời gian khóa quá lớn" - }, "logo": { "message": "Logo $1", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "Dán không thành công vì cụm từ có nhiều hơn 24 từ. Cụm từ khôi phục bí mật chỉ có tối đa 24 từ.", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "Bạn có thể dán toàn bộ cụm từ khôi phục bí mật vào bất kỳ trường nào", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "Bắt đầu" + }, + "srpSecurityQuizIntroduction": { + "message": "Để hiển thị Cụm từ khôi phục bí mật, bạn cần trả lời đúng hai câu hỏi" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "Nếu bạn làm mất Cụm từ khôi phục bí mật, MetaMask..." + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "Không thể giúp bạn" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "Hãy viết ra, khắc lên kim loại hoặc cất giữ ở nhiều nơi bí mật để bạn không bao giờ làm mất nó. Nếu bạn làm mất, nó sẽ bị mất vĩnh viễn." + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "Đúng! Không ai có thể giúp bạn lấy lại Cụm từ khôi phục bí mật" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "Có thể lấy lại cho bạn" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "Nếu bạn làm mất Cụm từ khôi phục bí mật, nó sẽ bị mất vĩnh viễn. Dù mọi người có nói gì, thì cũng không ai có thể giúp bạn lấy lại." + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "Sai! Không ai có thể giúp bạn lấy lại Cụm từ khôi phục bí mật" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "Nếu có bất kỳ ai, kể cả nhân viên hỗ trợ, hỏi về Cụm từ khôi phục bí mật của bạn..." + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "Bạn đang bị lừa đảo" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "Bất kỳ ai nói rằng họ cần Cụm từ khôi phục bí mật của bạn thì đều đang nói dối bạn. Nếu bạn chia sẻ với họ thì họ sẽ đánh cắp tài sản của bạn." + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "Chính xác! Chia sẻ Cụm từ khôi phục bí mật chưa bao giờ là một ý hay" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "Bạn nên đưa nó cho họ" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "Bất kỳ ai nói rằng họ cần Cụm từ khôi phục bí mật của bạn thì đều đang nói dối bạn. Nếu bạn chia sẻ với họ thì họ sẽ đánh cắp tài sản của bạn." + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "Không! Tuyệt đối không bao giờ chia sẻ Cụm từ khôi phục bí mật của bạn với bất kỳ ai" + }, + "srpSecurityQuizTitle": { + "message": "Câu hỏi bảo mật" + }, "srpToggleShow": { "message": "Hiện/Ẩn từ này của cụm từ khôi phục bí mật", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index de0b5e697..43ea94627 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1562,6 +1562,12 @@ "message": "但网络钓鱼者可能会。", "description": "The text link in 'holdToRevealContent3'" }, + "holdToRevealSRP": { + "message": "按住以显示 助记词" + }, + "holdToRevealSRPTitle": { + "message": "保护您的 助记词 安全" + }, "ignoreAll": { "message": "忽略所有" }, @@ -1857,9 +1863,6 @@ "lock": { "message": "注销" }, - "lockTimeTooGreat": { - "message": "锁定时间过长" - }, "logo": { "message": "$1标志", "description": "$1 is the name of the ticker" @@ -3333,12 +3336,63 @@ }, "srpPasteFailedTooManyWords": { "message": "粘贴失败,因为它包含超过24个单词。一个助记词最多可包含24个单词。", - "description": "Description of SRP paste erorr when the pasted content has too many words" + "description": "Description of SRP paste error when the pasted content has too many words" }, "srpPasteTip": { "message": "您可以将整个助记词粘贴到任何字段中", "description": "Our secret recovery phrase input is split into one field per word. This message explains to users that they can paste their entire secrete recovery phrase into any field, and we will handle it correctly." }, + "srpSecurityQuizGetStarted": { + "message": "开始" + }, + "srpSecurityQuizIntroduction": { + "message": "要查看助记词,您需要答对两个问题" + }, + "srpSecurityQuizQuestionOneQuestion": { + "message": "如果您丢失了助记词,MetaMask......" + }, + "srpSecurityQuizQuestionOneRightAnswer": { + "message": "无法帮助您" + }, + "srpSecurityQuizQuestionOneRightAnswerDescription": { + "message": "将它写下来、刻在金属上,或保存在多个秘密位置,这样您就不会丢失它。如果丢失了,它就会永远消失。" + }, + "srpSecurityQuizQuestionOneRightAnswerTitle": { + "message": "答对了!没有人能够帮您找回您的助记词" + }, + "srpSecurityQuizQuestionOneWrongAnswer": { + "message": "可以为您找回来" + }, + "srpSecurityQuizQuestionOneWrongAnswerDescription": { + "message": "一旦遗失助记词,它将永远消失。无论他人如何保证,无人能够帮您找回。" + }, + "srpSecurityQuizQuestionOneWrongAnswerTitle": { + "message": "答错了!没有人能够帮您找回您的助记词" + }, + "srpSecurityQuizQuestionTwoQuestion": { + "message": "如果有人(即使是技术支持人员)查问您的助记词......" + }, + "srpSecurityQuizQuestionTwoRightAnswer": { + "message": "就是在对您进行诈骗" + }, + "srpSecurityQuizQuestionTwoRightAnswerDescription": { + "message": "任何声称需要您的助记词的人都在对您进行欺诈。如果您与他们分享助记词,他们就会偷窃您的资产。" + }, + "srpSecurityQuizQuestionTwoRightAnswerTitle": { + "message": "答对了!分享您的助记词绝对不是个好主意" + }, + "srpSecurityQuizQuestionTwoWrongAnswer": { + "message": "您应该交给他们" + }, + "srpSecurityQuizQuestionTwoWrongAnswerDescription": { + "message": "任何声称需要您的助记词的人都在对您进行欺诈。如果您与他们分享助记词,他们就会偷窃您的资产。" + }, + "srpSecurityQuizQuestionTwoWrongAnswerTitle": { + "message": "不!永远不要与任何人分享您的助记词" + }, + "srpSecurityQuizTitle": { + "message": "安全问答" + }, "srpToggleShow": { "message": "显示/隐藏助记词中的这个单词", "description": "Describes a toggle that is used to show or hide a single word of the secret recovery phrase" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index e6e45115b..914c3f3fe 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -821,9 +821,6 @@ "lock": { "message": "鎖定" }, - "lockTimeTooGreat": { - "message": "鎖定時間過長" - }, "mainnet": { "message": "以太坊 主網路" }, diff --git a/app/images/reveal-srp.png b/app/images/reveal-srp.png new file mode 100644 index 000000000..80feea7f6 Binary files /dev/null and b/app/images/reveal-srp.png differ diff --git a/app/scripts/background.js b/app/scripts/background.js index bd80b71ac..e3416b796 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -462,7 +462,7 @@ export function setupController( setupEnsIpfsResolver({ getCurrentChainId: () => - controller.networkController.store.getState().providerConfig.chainId, + controller.networkController.state.providerConfig.chainId, getIpfsGateway: controller.preferencesController.getIpfsGateway.bind( controller.preferencesController, ), diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 722f1f56a..1bbf4a4f8 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -13,6 +13,7 @@ import { POLLING_TOKEN_ENVIRONMENT_TYPES, ORIGIN_METAMASK, } from '../../../shared/constants/app'; +import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; export default class AppStateController extends EventEmitter { /** @@ -32,7 +33,7 @@ export default class AppStateController extends EventEmitter { this.onInactiveTimeout = onInactiveTimeout || (() => undefined); this.store = new ObservableStore({ - timeoutMinutes: 0, + timeoutMinutes: DEFAULT_AUTO_LOCK_TIME_LIMIT, connectedStatusPopoverHasBeenShown: true, defaultHomeActiveTabName: null, browserEnvironment: {}, diff --git a/app/scripts/controllers/backup.js b/app/scripts/controllers/backup.js index 5873bc08c..159f02d2d 100644 --- a/app/scripts/controllers/backup.js +++ b/app/scripts/controllers/backup.js @@ -31,7 +31,7 @@ export default class BackupController { } if (network) { - this.networkController.store.updateState(network); + this.networkController.loadBackup(network); } if (preferences || addressBook || network) { @@ -48,7 +48,7 @@ export default class BackupController { addressBook: { ...this.addressBookController.state }, network: { networkConfigurations: - this.networkController.store.getState().networkConfigurations, + this.networkController.state.networkConfigurations, }, }; diff --git a/app/scripts/controllers/backup.test.js b/app/scripts/controllers/backup.test.js index fdeba7faf..25f153a37 100644 --- a/app/scripts/controllers/backup.test.js +++ b/app/scripts/controllers/backup.test.js @@ -57,18 +57,15 @@ function getMockAddressBookController() { } function getMockNetworkController() { - const mcState = { + const state = { networkConfigurations: {}, - - update: (store) => (mcState.store = store), }; - mcState.store = { - getState: sinon.stub().returns(mcState), - updateState: (store) => (mcState.store = store), + const loadBackup = ({ networkConfigurations }) => { + Object.assign(state, { networkConfigurations }); }; - return mcState; + return { state, loadBackup }; } const jsonData = JSON.stringify({ @@ -174,28 +171,28 @@ describe('BackupController', function () { it('should restore backup', async function () { const backupController = getBackupController(); - backupController.restoreUserData(jsonData); + await backupController.restoreUserData(jsonData); // check networks backup assert.equal( - backupController.networkController.store.networkConfigurations[ + backupController.networkController.state.networkConfigurations[ 'network-configuration-id-1' ].chainId, '0x539', ); assert.equal( - backupController.networkController.store.networkConfigurations[ + backupController.networkController.state.networkConfigurations[ 'network-configuration-id-2' ].chainId, '0x38', ); assert.equal( - backupController.networkController.store.networkConfigurations[ + backupController.networkController.state.networkConfigurations[ 'network-configuration-id-3' ].chainId, '0x61', ); assert.equal( - backupController.networkController.store.networkConfigurations[ + backupController.networkController.state.networkConfigurations[ 'network-configuration-id-4' ].chainId, '0x89', diff --git a/app/scripts/controllers/decrypt-message.ts b/app/scripts/controllers/decrypt-message.ts index b9f37fea2..fee01257f 100644 --- a/app/scripts/controllers/decrypt-message.ts +++ b/app/scripts/controllers/decrypt-message.ts @@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & { }; export type StateMessage = Required< - Omit + Omit >; export type DecryptMessageControllerState = { diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 33e765f91..46719aebc 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -33,8 +33,10 @@ export default class DetectTokensController { * @param config.tokensController * @param config.assetsContractController * @param config.trackMetaMetricsEvent + * @param config.messenger */ constructor({ + messenger, interval = DEFAULT_INTERVAL, preferences, network, @@ -44,6 +46,7 @@ export default class DetectTokensController { assetsContractController = null, trackMetaMetricsEvent, } = {}) { + this.messenger = messenger; this.assetsContractController = assetsContractController; this.tokensController = tokensController; this.preferences = preferences; @@ -59,7 +62,7 @@ export default class DetectTokensController { }); this.hiddenTokens = this.tokensController?.state.ignoredTokens; this.detectedTokens = this.tokensController?.state.detectedTokens; - this.chainId = this.getChainIdFromNetworkStore(network); + this.chainId = this.getChainIdFromNetworkStore(); this._trackMetaMetricsEvent = trackMetaMetricsEvent; preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => { @@ -81,6 +84,13 @@ export default class DetectTokensController { this.detectedTokens = detectedTokens; }, ); + messenger.subscribe('NetworkController:stateChange', () => { + if (this.chainId !== this.getChainIdFromNetworkStore()) { + const chainId = this.getChainIdFromNetworkStore(); + this.chainId = chainId; + this.restartTokenDetection({ chainId: this.chainId }); + } + }); } /** @@ -93,7 +103,7 @@ export default class DetectTokensController { async detectNewTokens({ selectedAddress, chainId } = {}) { const addressAgainstWhichToDetect = selectedAddress ?? this.selectedAddress; const chainIdAgainstWhichToDetect = - chainId ?? this.getChainIdFromNetworkStore(this._network); + chainId ?? this.getChainIdFromNetworkStore(); if (!this.isActive) { return; } @@ -208,8 +218,8 @@ export default class DetectTokensController { this.interval = DEFAULT_INTERVAL; } - getChainIdFromNetworkStore(network) { - return network?.store.getState().providerConfig.chainId; + getChainIdFromNetworkStore() { + return this.network?.state.providerConfig.chainId; } /* eslint-disable accessor-pairs */ @@ -226,23 +236,6 @@ export default class DetectTokensController { }, interval); } - /** - * @type {object} - */ - set network(network) { - if (!network) { - return; - } - this._network = network; - this._network.store.subscribe(() => { - if (this.chainId !== this.getChainIdFromNetworkStore(network)) { - const chainId = this.getChainIdFromNetworkStore(network); - this.chainId = chainId; - this.restartTokenDetection({ chainId: this.chainId }); - } - }); - } - /** * In setter when isUnlocked is updated to true, detectNewTokens and restart polling * diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index a1fee90ce..beb0e9913 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -10,12 +10,19 @@ import { AssetsContractController, } from '@metamask/assets-controllers'; import { toHex } from '@metamask/controller-utils'; +import { NetworkController } from '@metamask/network-controller'; import { NETWORK_TYPES } from '../../../shared/constants/network'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; -import { NetworkController } from './network'; import PreferencesController from './preferences'; +function buildMessenger() { + return new ControllerMessenger().getRestricted({ + name: 'DetectTokensController', + allowedEvents: ['NetworkController:stateChange'], + }); +} + describe('DetectTokensController', function () { let sandbox, assetsContractController, @@ -230,23 +237,20 @@ describe('DetectTokensController', function () { onPreferencesStateChange: preferences.store.subscribe.bind( preferences.store, ), - onNetworkStateChange: (cb) => - network.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.providerConfig, - }, - }; - return cb(modifiedNetworkState); - }), + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', + ), }); assetsContractController = new AssetsContractController({ onPreferencesStateChange: preferences.store.subscribe.bind( preferences.store, ), - onNetworkStateChange: network.store.subscribe.bind(network.store), + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', + ), }); }); @@ -257,7 +261,7 @@ describe('DetectTokensController', function () { it('should poll on correct interval', async function () { const stub = sinon.stub(global, 'setInterval'); - new DetectTokensController({ interval: 1337 }); // eslint-disable-line no-new + new DetectTokensController({ messenger: buildMessenger(), interval: 1337 }); // eslint-disable-line no-new assert.strictEqual(stub.getCall(0).args[1], 1337); stub.restore(); }); @@ -266,6 +270,7 @@ describe('DetectTokensController', function () { const clock = sandbox.useFakeTimers(); await network.setProviderType(NETWORK_TYPES.MAINNET); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -302,6 +307,7 @@ describe('DetectTokensController', function () { }); await tokenListController.start(); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -325,6 +331,7 @@ describe('DetectTokensController', function () { sandbox.useFakeTimers(); await network.setProviderType(NETWORK_TYPES.MAINNET); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -376,6 +383,7 @@ describe('DetectTokensController', function () { sandbox.useFakeTimers(); await network.setProviderType(NETWORK_TYPES.MAINNET); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -434,6 +442,7 @@ describe('DetectTokensController', function () { it('should trigger detect new tokens when change address', async function () { sandbox.useFakeTimers(); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -453,6 +462,7 @@ describe('DetectTokensController', function () { it('should trigger detect new tokens when submit password', async function () { sandbox.useFakeTimers(); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -471,6 +481,7 @@ describe('DetectTokensController', function () { const clock = sandbox.useFakeTimers(); await network.setProviderType(NETWORK_TYPES.MAINNET); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, @@ -492,6 +503,7 @@ describe('DetectTokensController', function () { const clock = sandbox.useFakeTimers(); await network.setProviderType(NETWORK_TYPES.MAINNET); const controller = new DetectTokensController({ + messenger: buildMessenger(), preferences, network, keyringMemStore, diff --git a/app/scripts/controllers/encryption-public-key.ts b/app/scripts/controllers/encryption-public-key.ts index 904bed476..da50f1afe 100644 --- a/app/scripts/controllers/encryption-public-key.ts +++ b/app/scripts/controllers/encryption-public-key.ts @@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & { }; export type StateMessage = Required< - Omit + Omit > & { msgParams: string; }; diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 780da42b4..e881f4772 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -9,11 +9,7 @@ import { MetaMetricsUserTrait, } from '../../../shared/constants/metametrics'; import waitUntilCalled from '../../../test/lib/wait-until-called'; -import { - CHAIN_IDS, - CURRENCY_SYMBOLS, - NETWORK_TYPES, -} from '../../../shared/constants/network'; +import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network'; import * as Utils from '../lib/util'; import MetaMetricsController from './metametrics'; @@ -77,28 +73,6 @@ const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, }; -function getMockNetworkController() { - let state = { - providerConfig: { - type: NETWORK_TYPES.GOERLI, - chainId: FAKE_CHAIN_ID, - }, - network: 'loading', - }; - const onNetworkDidChange = sinon.stub(); - const updateState = (newState) => { - state = { ...state, ...newState }; - onNetworkDidChange.getCall(0).args[0](); - }; - return { - store: { - getState: () => state, - updateState, - }, - onNetworkDidChange, - }; -} - function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { let preferencesStore = { currentLocale, @@ -142,15 +116,16 @@ function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, preferencesStore = getMockPreferencesStore(), - networkController = getMockNetworkController(), + getCurrentChainId = () => FAKE_CHAIN_ID, + onNetworkDidChange = () => { + // do nothing + }, segmentInstance, } = {}) { return new MetaMetricsController({ segment: segmentInstance || segment, - getCurrentChainId: () => - networkController.store.getState().providerConfig.chainId, - onNetworkDidChange: - networkController.onNetworkDidChange.bind(networkController), + getCurrentChainId, + onNetworkDidChange, preferencesStore, version: '0.0.1', environment: 'test', @@ -166,6 +141,7 @@ function getMetaMetricsController({ extension: MOCK_EXTENSION, }); } + describe('MetaMetricsController', function () { const now = new Date(); let clock; @@ -213,17 +189,20 @@ describe('MetaMetricsController', function () { }); it('should update when network changes', function () { - const networkController = getMockNetworkController(); + let chainId = '0x111'; + let networkDidChangeListener; + const onNetworkDidChange = (listener) => { + networkDidChangeListener = listener; + }; const metaMetricsController = getMetaMetricsController({ - networkController, + getCurrentChainId: () => chainId, + onNetworkDidChange, }); - networkController.store.updateState({ - providerConfig: { - type: 'NEW_NETWORK', - chainId: '0xaab', - }, - }); - assert.strictEqual(metaMetricsController.chainId, '0xaab'); + + chainId = '0x222'; + networkDidChangeListener(); + + assert.strictEqual(metaMetricsController.chainId, '0x222'); }); it('should update when preferences changes', function () { diff --git a/app/scripts/controllers/mmi-controller.js b/app/scripts/controllers/mmi-controller.js index ca9933cfb..689472829 100644 --- a/app/scripts/controllers/mmi-controller.js +++ b/app/scripts/controllers/mmi-controller.js @@ -549,22 +549,25 @@ export default class MMIController extends EventEmitter { this.preferencesController.setSelectedAddress(address); } const selectedChainId = parseInt( - this.networkController.getCurrentChainId(), + this.networkController.state.providerConfig.chainId, 16, ); if (selectedChainId !== chainId && chainId === 1) { - this.networkController.setProviderType('mainnet'); + await this.networkController.setProviderType('mainnet'); } else if (selectedChainId !== chainId) { - const network = this.preferencesController - .getFrequentRpcListDetail() - .find((item) => parseInt(item.chainId, 16) === chainId); - this.networkController.setRpcTarget( - network.rpcUrl, - network.chainId, - network.ticker, - network.nickname, - ); + const foundNetworkConfiguration = Object.values( + this.networkController.state.networkConfigurations, + ).find((networkConfiguration) => { + return parseInt(networkConfiguration.chainId, 16) === chainId; + }); + + if (foundNetworkConfiguration !== undefined) { + await this.networkConfiguration.setActiveNetwork( + foundNetworkConfiguration.id, + ); + } } + getPermissionBackgroundApiMethods( this.permissionController, ).addPermittedAccount(origin, address); diff --git a/app/scripts/controllers/network/create-network-client.test.ts b/app/scripts/controllers/network/create-network-client.test.ts deleted file mode 100644 index 7e674392b..000000000 --- a/app/scripts/controllers/network/create-network-client.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NetworkClientType } from './create-network-client'; -import { testsForProviderType } from './provider-api-tests/shared-tests'; - -describe('createNetworkClient', () => { - testsForProviderType(NetworkClientType.Infura); - testsForProviderType(NetworkClientType.Custom); -}); diff --git a/app/scripts/controllers/network/create-network-client.ts b/app/scripts/controllers/network/create-network-client.ts deleted file mode 100644 index 6e96b71f5..000000000 --- a/app/scripts/controllers/network/create-network-client.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - createAsyncMiddleware, - createScaffoldMiddleware, - JsonRpcEngine, - mergeMiddleware, - JsonRpcMiddleware, -} from 'json-rpc-engine'; -import { - createBlockCacheMiddleware, - createBlockRefMiddleware, - createBlockRefRewriteMiddleware, - createBlockTrackerInspectorMiddleware, - createInflightCacheMiddleware, - createFetchMiddleware, - createRetryOnEmptyMiddleware, -} from '@metamask/eth-json-rpc-middleware'; -import { - providerFromEngine, - providerFromMiddleware, - SafeEventEmitterProvider, -} from '@metamask/eth-json-rpc-provider'; -import { createInfuraMiddleware } from '@metamask/eth-json-rpc-infura'; -import type { Hex } from '@metamask/utils/dist'; -import { PollingBlockTracker } from 'eth-block-tracker/dist'; -import { SECOND } from '../../../../shared/constants/time'; -import { - BUILT_IN_INFURA_NETWORKS, - BuiltInInfuraNetwork, -} from '../../../../shared/constants/network'; - -export enum NetworkClientType { - Custom = 'custom', - Infura = 'infura', -} - -type CustomNetworkConfiguration = { - chainId: Hex; - rpcUrl: string; - type: NetworkClientType.Custom; -}; - -type InfuraNetworkConfiguration = { - network: BuiltInInfuraNetwork; - infuraProjectId: string; - type: NetworkClientType.Infura; -}; - -/** - * Create a JSON RPC network client for a specific network. - * - * @param networkConfig - The network configuration. - * @returns - */ -export function createNetworkClient( - networkConfig: CustomNetworkConfiguration | InfuraNetworkConfiguration, -): { provider: SafeEventEmitterProvider; blockTracker: PollingBlockTracker } { - const rpcApiMiddleware = - networkConfig.type === NetworkClientType.Infura - ? createInfuraMiddleware({ - network: networkConfig.network, - projectId: networkConfig.infuraProjectId, - maxAttempts: 5, - source: 'metamask', - }) - : createFetchMiddleware({ - btoa: global.btoa, - fetch: global.fetch, - rpcUrl: networkConfig.rpcUrl, - }); - - const rpcProvider = providerFromMiddleware(rpcApiMiddleware); - - const blockTrackerOpts = - process.env.IN_TEST && networkConfig.type === 'custom' - ? { pollingInterval: SECOND } - : {}; - const blockTracker = new PollingBlockTracker({ - ...blockTrackerOpts, - provider: rpcProvider, - }); - - const networkMiddleware = - networkConfig.type === NetworkClientType.Infura - ? createInfuraNetworkMiddleware({ - blockTracker, - network: networkConfig.network, - rpcProvider, - rpcApiMiddleware, - }) - : createCustomNetworkMiddleware({ - blockTracker, - chainId: networkConfig.chainId, - rpcApiMiddleware, - }); - - const engine = new JsonRpcEngine(); - - engine.push(networkMiddleware); - - const provider = providerFromEngine(engine); - - return { provider, blockTracker }; -} - -function createInfuraNetworkMiddleware({ - blockTracker, - network, - rpcProvider, - rpcApiMiddleware, -}: { - blockTracker: PollingBlockTracker; - network: BuiltInInfuraNetwork; - rpcProvider: SafeEventEmitterProvider; - rpcApiMiddleware: JsonRpcMiddleware; -}) { - return mergeMiddleware([ - createNetworkAndChainIdMiddleware({ network }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), - createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); -} - -function createNetworkAndChainIdMiddleware({ - network, -}: { - network: BuiltInInfuraNetwork; -}) { - if (!BUILT_IN_INFURA_NETWORKS[network]) { - throw new Error(`createInfuraClient - unknown network "${network}"`); - } - - const { chainId, networkId } = BUILT_IN_INFURA_NETWORKS[network]; - - return createScaffoldMiddleware({ - eth_chainId: chainId, - net_version: networkId, - }); -} - -const createChainIdMiddleware = ( - chainId: string, -): JsonRpcMiddleware => { - return (req, res, next, end) => { - if (req.method === 'eth_chainId') { - res.result = chainId; - return end(); - } - return next(); - }; -}; - -function createCustomNetworkMiddleware({ - blockTracker, - chainId, - rpcApiMiddleware, -}: { - blockTracker: PollingBlockTracker; - chainId: string; - rpcApiMiddleware: any; -}) { - const testMiddlewares = process.env.IN_TEST - ? [createEstimateGasDelayTestMiddleware()] - : []; - - return mergeMiddleware([ - ...testMiddlewares, - createChainIdMiddleware(chainId), - createBlockRefRewriteMiddleware({ blockTracker }), - createBlockCacheMiddleware({ blockTracker }), - createInflightCacheMiddleware(), - createBlockTrackerInspectorMiddleware({ blockTracker }), - rpcApiMiddleware, - ]); -} - -/** - * For use in tests only. - * Adds a delay to `eth_estimateGas` calls. - */ -function createEstimateGasDelayTestMiddleware() { - return createAsyncMiddleware(async (req, _, next) => { - if (req.method === 'eth_estimateGas') { - await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); - } - return next(); - }); -} diff --git a/app/scripts/controllers/network/index.ts b/app/scripts/controllers/network/index.ts deleted file mode 100644 index de3e59ea1..000000000 --- a/app/scripts/controllers/network/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './network-controller'; diff --git a/app/scripts/controllers/network/network-controller.test.ts b/app/scripts/controllers/network/network-controller.test.ts deleted file mode 100644 index 9a8a2f933..000000000 --- a/app/scripts/controllers/network/network-controller.test.ts +++ /dev/null @@ -1,7277 +0,0 @@ -// The `refreshNetworkTests` helper defines tests outside of a `describe` block. -/* eslint-disable jest/require-top-level-describe */ - -import { inspect, isDeepStrictEqual, promisify } from 'util'; -import assert from 'assert'; -import { get } from 'lodash'; -import { v4 } from 'uuid'; -import nock from 'nock'; -import { ControllerMessenger } from '@metamask/base-controller'; -import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; -import { toHex } from '@metamask/controller-utils'; -import { when, resetAllWhenMocks } from 'jest-when'; -import { ethErrors } from 'eth-rpc-errors'; -import { - NetworkStatus, - NETWORK_TYPES, -} from '../../../../shared/constants/network'; -import { - NetworkController, - NetworkControllerAction, - NetworkControllerEvent, - NetworkControllerOptions, - NetworkControllerState, - ProviderConfiguration, -} from './network-controller'; -import { - createNetworkClient, - NetworkClientType, -} from './create-network-client'; -import { FakeBlockTracker } from './test/fake-block-tracker'; -import { FakeProvider, FakeProviderStub } from './test/fake-provider'; - -jest.mock('./create-network-client'); - -jest.mock('uuid', () => { - const actual = jest.requireActual('uuid'); - - return { - ...actual, - v4: jest.fn(), - }; -}); - -/** - * A block header object that `eth_getBlockByNumber` can be mocked to return. - * Note that this type does not specify all of the properties present within the - * block header; within these tests, we are only interested in `number` and - * `baseFeePerGas`. - */ -type Block = { - number: string; - baseFeePerGas?: string; -}; - -const createNetworkClientMock = jest.mocked(createNetworkClient); -const uuidV4Mock = jest.mocked(v4); - -/** - * A dummy block that matches the pre-EIP-1559 format (i.e. it doesn't have the - * `baseFeePerGas` property). - */ -const PRE_1559_BLOCK: Block = { - number: '0x42', -}; - -/** - * A dummy block that matches the pre-EIP-1559 format (i.e. it has the - * `baseFeePerGas` property). - */ -const POST_1559_BLOCK: Block = { - ...PRE_1559_BLOCK, - baseFeePerGas: '0x63c498a46', -}; - -/** - * An alias for `POST_1559_BLOCK`, for tests that don't care about which kind of - * block they're looking for. - */ -const BLOCK: Block = POST_1559_BLOCK; - -/** - * The networks that NetworkController recognizes as built-in Infura networks, - * along with information we expect to be true for those networks. - */ -const INFURA_NETWORKS = [ - { - networkType: NETWORK_TYPES.MAINNET, - chainId: toHex(1), - ticker: 'ETH', - blockExplorerUrl: 'https://etherscan.io', - }, - { - networkType: NETWORK_TYPES.GOERLI, - chainId: toHex(5), - ticker: 'GoerliETH', - blockExplorerUrl: 'https://goerli.etherscan.io', - }, - { - networkType: NETWORK_TYPES.SEPOLIA, - chainId: toHex(11155111), - ticker: 'SepoliaETH', - blockExplorerUrl: 'https://sepolia.etherscan.io', - }, -]; - -/** - * A response object for a successful request to `eth_getBlockByNumber`. It is - * assumed that the block number here is insignificant to the test. - */ -const SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE = { - result: BLOCK, -}; - -/** - * A response object for a successful request to `net_version`. It is assumed - * that the network ID here is insignificant to the test. - */ -const SUCCESSFUL_NET_VERSION_RESPONSE = { - result: '42', -}; - -/** - * A response object for a request that has been geoblocked by Infura. - */ -const BLOCKED_INFURA_JSON_RPC_ERROR = ethErrors.rpc.internal( - JSON.stringify({ error: 'countryBlocked' }), -); - -/** - * A response object for a unsuccessful request to any RPC method. It is assumed - * that the error here is insignificant to the test. - */ -const GENERIC_JSON_RPC_ERROR = ethErrors.rpc.internal( - JSON.stringify({ error: 'oops' }), -); - -describe('NetworkController', () => { - beforeEach(() => { - // Disable all requests, even those to localhost - nock.disableNetConnect(); - }); - - afterEach(() => { - nock.enableNetConnect('localhost'); - nock.cleanAll(); - resetAllWhenMocks(); - jest.resetAllMocks(); - }); - - describe('constructor', () => { - const invalidInfuraProjectIds = [undefined, null, {}, 1]; - invalidInfuraProjectIds.forEach((invalidProjectId) => { - it(`throws given an invalid Infura ID of "${inspect( - invalidProjectId, - )}"`, () => { - const messenger = buildMessenger(); - const restrictedMessenger = buildNetworkControllerMessenger(messenger); - expect( - () => - new NetworkController({ - messenger: restrictedMessenger, - // @ts-expect-error We are intentionally passing bad input. - infuraProjectId: invalidProjectId, - }), - ).toThrow('Invalid Infura project ID'); - }); - }); - - it('initializes the state with some defaults', async () => { - await withController(({ controller }) => { - expect(controller.store.getState()).toMatchInlineSnapshot(` - { - "networkConfigurations": {}, - "networkDetails": { - "EIPS": {}, - }, - "networkId": null, - "networkStatus": "unknown", - "providerConfig": { - "chainId": "0x539", - "nickname": "Localhost 8545", - "rpcUrl": "http://localhost:8545", - "ticker": "ETH", - "type": "rpc", - }, - } - `); - }); - }); - - it('merges the given state into the default state', async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'http://example-custom-rpc.metamask.io', - chainId: '0x9999' as const, - nickname: 'Test initial state', - }, - networkDetails: { - EIPS: { - 1559: true, - }, - }, - }, - }, - ({ controller }) => { - expect(controller.store.getState()).toMatchInlineSnapshot(` - { - "networkConfigurations": {}, - "networkDetails": { - "EIPS": { - "1559": true, - }, - }, - "networkId": null, - "networkStatus": "unknown", - "providerConfig": { - "chainId": "0x9999", - "nickname": "Test initial state", - "rpcUrl": "http://example-custom-rpc.metamask.io", - "type": "rpc", - }, - } - `); - }, - ); - }); - }); - - describe('destroy', () => { - it('does not throw if called before the provider is initialized', async () => { - await withController(async ({ controller }) => { - expect(await controller.destroy()).toBeUndefined(); - }); - }); - - it('stops the block tracker for the currently selected network as long as the provider has been initialized', async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - const { blockTracker } = controller.getProviderAndBlockTracker(); - assert(blockTracker, 'Block tracker is somehow unset'); - // The block tracker starts running after a listener is attached - blockTracker.addListener('latest', () => { - // do nothing - }); - expect(blockTracker.isRunning()).toBe(true); - - await controller.destroy(); - - expect(blockTracker.isRunning()).toBe(false); - }); - }); - }); - - describe('initializeProvider', () => { - describe('when the type in the provider config is invalid', () => { - it('throws', async () => { - const invalidProviderConfig = {}; - await withController( - /* @ts-expect-error We're intentionally passing bad input. */ - { - state: { - providerConfig: invalidProviderConfig, - }, - }, - async ({ controller }) => { - await expect(async () => { - await controller.initializeProvider(); - }).rejects.toThrow("Unrecognized network type: 'undefined'"); - }, - ); - }); - }); - - for (const { networkType } of INFURA_NETWORKS) { - describe(`when the type in the provider config is "${networkType}"`, () => { - it(`creates a network client for the ${networkType} Infura network, capturing the resulting provider`, async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await controller.initializeProvider(); - - expect(createNetworkClientMock).toHaveBeenCalledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const { result } = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }); - expect(result).toBe('test response'); - }, - ); - }); - - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller: NetworkController) => { - await controller.initializeProvider(); - }, - }); - }); - } - - describe(`when the type in the provider configuration is "rpc"`, () => { - describe('if chainId and rpcUrl are present in the provider config', () => { - it('creates a network client for a custom RPC endpoint using the provider config, capturing the resulting provider', async () => { - await withController( - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'http://example.com', - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await controller.initializeProvider(); - - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: toHex(1337), - rpcUrl: 'http://example.com', - type: NetworkClientType.Custom, - }); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is not set'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const { result } = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }); - expect(result).toBe('test response'); - }, - ); - }); - - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - initialState: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - }, - operation: async (controller: NetworkController) => { - await controller.initializeProvider(); - }, - }); - }); - - describe('if chainId is missing from the provider config', () => { - it('throws', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.initializeProvider(), - ).rejects.toThrow( - 'chainId must be provided for custom RPC endpoints', - ); - }, - ); - }); - - it('does not create a network client or capture a provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - try { - await controller.initializeProvider(); - } catch { - // ignore the error - } - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeNull(); - expect(blockTracker).toBeNull(); - }, - ); - }); - }); - - describe('if rpcUrl is missing from the provider config', () => { - it('throws', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.initializeProvider(), - ).rejects.toThrow( - 'rpcUrl must be provided for custom RPC endpoints', - ); - }, - ); - }); - - it('does not create a network client or capture a provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: undefined, - }), - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - try { - await controller.initializeProvider(); - } catch { - // ignore the error - } - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeNull(); - expect(blockTracker).toBeNull(); - }, - ); - }); - }); - }); - }); - - describe('getProviderAndBlockTracker', () => { - it('returns objects that proxy to the provider and block tracker as long as the provider has been initialized', async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - - expect(provider).toHaveProperty('sendAsync'); - expect(blockTracker).toHaveProperty('checkForLatestBlock'); - }); - }); - - it("returns null for both the provider and block tracker if the provider hasn't been initialized yet", async () => { - await withController(async ({ controller }) => { - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - - expect(provider).toBeNull(); - expect(blockTracker).toBeNull(); - }); - }); - - for (const { networkType } of INFURA_NETWORKS) { - describe(`when the type in the provider configuration is changed to "${networkType}"`, () => { - it(`returns a provider object that was pointed to another network before the switch and is pointed to "${networkType}" afterward`, async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 1', - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - - const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( - provider, - ); - const response1 = await promisifiedSendAsync1({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response1.result).toBe('test response 1'); - - await controller.setProviderType(networkType); - const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( - provider, - ); - const response2 = await promisifiedSendAsync2({ - id: '2', - jsonrpc: '2.0', - method: 'test', - }); - expect(response2.result).toBe('test response 2'); - }, - ); - }); - }); - } - - describe('when the type in the provider configuration is changed to "rpc"', () => { - it('returns a provider object that was pointed to another network before the switch and is pointed to the new network', async () => { - await withController( - { - state: { - providerConfig: { - type: 'goerli', - // NOTE: This doesn't need to match the logical chain ID of - // the network selected, it just needs to exist - chainId: '0x9999999', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - ticker: 'ABC', - id: 'testNetworkConfigurationId', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 1', - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response 2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: '0x1337', - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - - const promisifiedSendAsync1 = promisify(provider.sendAsync).bind( - provider, - ); - const response1 = await promisifiedSendAsync1({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response1.result).toBe('test response 1'); - - await controller.setActiveNetwork('testNetworkConfigurationId'); - const promisifiedSendAsync2 = promisify(provider.sendAsync).bind( - provider, - ); - const response2 = await promisifiedSendAsync2({ - id: '2', - jsonrpc: '2.0', - method: 'test', - }); - expect(response2.result).toBe('test response 2'); - }, - ); - }); - }); - }); - - describe('getEIP1559Compatibility', () => { - describe('if no provider has been set yet', () => { - it('does not make any state changes', async () => { - await withController(async ({ controller }) => { - const promiseForNoStateChanges = waitForStateChanges({ - controller, - count: 0, - operation: async () => { - await controller.getEIP1559Compatibility(); - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }); - }); - - it('returns false', async () => { - await withController(async ({ controller }) => { - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(false); - }); - }); - }); - - describe('if a provider has been set but networkDetails.EIPS in state already has a "1559" property', () => { - it('does not make any state changes', async () => { - await withController( - { - state: { - networkDetails: { - EIPS: { - 1559: true, - }, - }, - }, - }, - async ({ controller }) => { - setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - const promiseForNoStateChanges = waitForStateChanges({ - controller, - count: 0, - operation: async () => { - await controller.getEIP1559Compatibility(); - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }, - ); - }); - - it('returns the value of the "1559" property', async () => { - await withController( - { - state: { - networkDetails: { - EIPS: { - 1559: true, - }, - }, - }, - }, - async ({ controller }) => { - setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(true); - }, - ); - }); - }); - - describe('if a provider has been set and networkDetails.EIPS in state does not already have a "1559" property', () => { - describe('if the request for the latest block is successful', () => { - describe('if the latest block has a "baseFeePerGas" property', () => { - it('sets the "1559" property to true', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(true); - }); - }); - - it('returns true', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(true); - }); - }); - }); - - describe('if the latest block does not have a "baseFeePerGas" property', () => { - it('sets the "1559" property to false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }); - }); - - it('returns false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(false); - }); - }); - }); - - describe('if the request for the latest block responds with null', () => { - it('sets the "1559" property to false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: null, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await controller.getEIP1559Compatibility(); - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBe(false); - }); - }); - - it('returns false', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: null, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const isEIP1559Compatible = - await controller.getEIP1559Compatibility(); - - expect(isEIP1559Compatible).toBe(false); - }); - }); - }); - }); - - describe('if the request for the latest block is unsuccessful', () => { - it('does not make any state changes', async () => { - await withController(async ({ controller }) => { - setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const promiseForNoStateChanges = waitForStateChanges({ - controller, - count: 0, - operation: async () => { - try { - await controller.getEIP1559Compatibility(); - } catch (error) { - // ignore error - } - }, - }); - - expect(Boolean(promiseForNoStateChanges)).toBe(true); - }); - }); - }); - }); - }); - - describe('lookupNetwork', () => { - describe('if a provider has not been set', () => { - it('does not change network in state', async () => { - await withController(async ({ controller }) => { - const promiseForNetworkChanges = waitForStateChanges({ - controller, - propertyPath: ['networkId'], - }); - - await controller.lookupNetwork(); - - await expect(promiseForNetworkChanges).toNeverResolve(); - }); - }); - }); - - [ - NETWORK_TYPES.MAINNET, - NETWORK_TYPES.GOERLI, - NETWORK_TYPES.SEPOLIA, - ].forEach((networkType) => { - describe(`when the provider config in state contains a network type of "${networkType}"`, () => { - describe('if the network was switched after the net_version request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkStatus).toBe( - 'unknown', - ); - }, - ); - }); - - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.store.getState().networkId).toBe('1'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkId).toBe('2'); - }, - ); - }); - - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - }, - ); - }); - - it('emits infuraIsUnblocked, not infuraIsBlocked, assuming that the first network was blocked', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - }); - const promiseForNoInfuraIsBlockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForNoInfuraIsBlockedEvents).toBeFulfilled(); - }, - ); - }); - }); - - describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkStatus).toBe( - 'unknown', - ); - }, - ); - }); - - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.store.getState().networkId).toBe('1'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkId).toBe('2'); - }, - ); - }); - - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - }, - ); - }); - - it('emits infuraIsUnblocked, not infuraIsBlocked, assuming that the first network was blocked', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ type: networkType }), - networkConfigurations: { - testNetworkConfigurationId: { - id: 'testNetworkConfigurationId', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'ABC', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setActiveNetwork( - 'testNetworkConfigurationId', - ); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - }); - const promiseForNoInfuraIsBlockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForNoInfuraIsBlockedEvents).toBeFulfilled(); - }, - ); - }); - }); - - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.lookupNetwork(); - }, - }); - }); - }); - - describe(`when the provider config in state contains a network type of "rpc"`, () => { - describe('if the network was switched after the net_version request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.store.getState().networkId).toBe('1'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkId).toBe('2'); - }, - ); - }); - - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - }, - ); - }); - - it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network was blocked and the first network was not', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForInfuraIsBlockedEvents).toBeFulfilled(); - }, - ); - }); - }); - - describe('if the network was switched after the eth_getBlockByNumber request started but before it completed', () => { - it('stores the network status of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - error: GENERIC_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - it('stores the ID of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: async () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.initializeProvider(); - }, - }); - expect(controller.store.getState().networkId).toBe('1'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkId'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkId).toBe('2'); - }, - ); - }); - - it('stores the EIP-1559 support of the second network, not the first', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: { - result: '2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - }, - ); - }); - - it('emits infuraIsBlocked, not infuraIsUnblocked, if the second network was blocked and the first network was not', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called via `lookupNetwork` directly - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - beforeCompleting: () => { - // Intentionally not awaited because don't want this to - // block the `net_version` request - controller.setProviderType(NETWORK_TYPES.GOERLI); - }, - }, - ]), - buildFakeProvider([ - // Called when switching networks - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - chainId: toHex(1337), - rpcUrl: 'https://mock-rpc-url', - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlockedEvents = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.lookupNetwork(); - }, - }); - - await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForInfuraIsBlockedEvents).toBeFulfilled(); - }, - ); - }); - }); - - lookupNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - initialState: { - providerConfig: buildProviderConfig({ type: NETWORK_TYPES.RPC }), - }, - operation: async (controller) => { - await controller.lookupNetwork(); - }, - }); - }); - }); - - describe('setActiveNetwork', () => { - refreshNetworkTests({ - expectedProviderConfig: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - type: NETWORK_TYPES.RPC, - }, - initialState: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: undefined, - }, - }, - }, - operation: async (controller) => { - await controller.setActiveNetwork('testNetworkConfigurationId'); - }, - }); - - describe('if the given ID does not match a network configuration in networkConfigurations', () => { - it('throws', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('invalidNetworkConfigurationId'), - ).rejects.toThrow( - new Error( - 'networkConfigurationId invalidNetworkConfigurationId does not match a configured networkConfiguration', - ), - ); - }, - ); - }); - }); - - describe('if the network config does not contain an RPC URL', () => { - it('throws', async () => { - await withController( - // @ts-expect-error RPC URL intentionally omitted - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: undefined, - chainId: toHex(222), - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), - ).rejects.toThrow( - 'rpcUrl must be provided for custom RPC endpoints', - ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeNull(); - expect(blockTracker).toBeNull(); - }, - ); - }); - }); - - describe('if the network config does not contain a chain ID', () => { - it('throws', async () => { - await withController( - // @ts-expect-error chain ID intentionally omitted - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId1: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId1', - rpcPrefs: undefined, - }, - testNetworkConfigurationId2: { - rpcUrl: 'http://somethingexisting.com', - chainId: undefined, - ticker: 'something existing', - nickname: 'something existing', - id: 'testNetworkConfigurationId2', - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await expect(() => - controller.setActiveNetwork('testNetworkConfigurationId2'), - ).rejects.toThrow( - 'chainId must be provided for custom RPC endpoints', - ); - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - const { provider, blockTracker } = - controller.getProviderAndBlockTracker(); - expect(provider).toBeNull(); - expect(blockTracker).toBeNull(); - }, - ); - }); - }); - - it('overwrites the provider configuration given a networkConfigurationId that matches a configured networkConfiguration', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfiguration: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClient); - - await controller.setActiveNetwork('testNetworkConfiguration'); - - expect(controller.store.getState().providerConfig).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - nickname: 'something existing', - id: 'testNetworkConfigurationId', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer-2.com', - }, - }); - }, - ); - }); - }); - - describe('setProviderType', () => { - for (const { - networkType, - chainId, - ticker, - blockExplorerUrl, - } of INFURA_NETWORKS) { - describe(`given a network type of "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: networkType, - }), - operation: async (controller) => { - await controller.setProviderType(networkType); - }, - }); - }); - - it(`overwrites the provider configuration using a predetermined chainId, ticker, and blockExplorerUrl for "${networkType}", clearing id, rpcUrl, and nickname`, async () => { - await withController( - { - state: { - providerConfig: { - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: '0x1337', - nickname: 'test-chain', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - await controller.setProviderType(networkType); - - expect(controller.store.getState().providerConfig).toStrictEqual({ - type: networkType, - rpcUrl: undefined, - chainId, - ticker, - nickname: undefined, - rpcPrefs: { blockExplorerUrl }, - id: undefined, - }); - }, - ); - }); - } - - describe('given a network type of "rpc"', () => { - it('throws because there is no way to switch to a custom RPC endpoint using this method', async () => { - await withController( - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://somethingexisting.com', - chainId: toHex(99999), - ticker: 'something existing', - nickname: 'something existing', - }, - }, - }, - async ({ controller }) => { - await expect(() => - controller.setProviderType(NETWORK_TYPES.RPC), - ).rejects.toThrow( - 'NetworkController - cannot call "setProviderType" with type "rpc". Use "setActiveNetwork"', - ); - }, - ); - }); - - it("doesn't set a provider", async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - try { - await controller.setProviderType(NETWORK_TYPES.RPC); - } catch { - // catch the rejection (it is tested above) - } - - expect(createNetworkClientMock).not.toHaveBeenCalled(); - expect(controller.getProviderAndBlockTracker().provider).toBeNull(); - }); - }); - - it('does not update networkDetails.EIPS in state', async () => { - await withController(async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', - }, - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - try { - await controller.setProviderType(NETWORK_TYPES.RPC); - } catch { - // catch the rejection (it is tested above) - } - - expect( - controller.store.getState().networkDetails.EIPS[1559], - ).toBeUndefined(); - }); - }); - }); - - describe('given an invalid Infura network name', () => { - it('throws', async () => { - await withController(async ({ controller }) => { - await expect(() => - controller.setProviderType('invalid-infura-network'), - ).rejects.toThrow( - new Error('Unknown Infura provider type "invalid-infura-network".'), - ); - }); - }); - }); - }); - - describe('resetConnection', () => { - [ - NETWORK_TYPES.MAINNET, - NETWORK_TYPES.GOERLI, - NETWORK_TYPES.SEPOLIA, - ].forEach((networkType) => { - describe(`when the type in the provider configuration is "${networkType}"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ type: networkType }), - initialState: { - providerConfig: buildProviderConfig({ type: networkType }), - }, - operation: async (controller) => { - await controller.resetConnection(); - }, - }); - }); - }); - - describe(`when the type in the provider configuration is "rpc"`, () => { - refreshNetworkTests({ - expectedProviderConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - initialState: { - providerConfig: buildProviderConfig({ type: NETWORK_TYPES.RPC }), - }, - operation: async (controller) => { - await controller.resetConnection(); - }, - }); - }); - }); - - describe('NetworkController:getProviderConfig action', () => { - it('returns the provider config in state', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.MAINNET, - }), - }, - }, - async ({ messenger }) => { - const providerConfig = await messenger.call( - 'NetworkController:getProviderConfig', - ); - - expect(providerConfig).toStrictEqual( - buildProviderConfig({ - type: NETWORK_TYPES.MAINNET, - }), - ); - }, - ); - }); - }); - - describe('NetworkController:getEthQuery action', () => { - it('returns a EthQuery object that can be used to make requests to the currently selected network', async () => { - await withController(async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'test_method', - params: [], - }, - response: { - result: 'test response', - }, - }, - ], - }); - - const ethQuery = messenger.call('NetworkController:getEthQuery'); - assert(ethQuery, 'ethQuery is not set'); - - const promisifiedSendAsync = promisify(ethQuery.sendAsync).bind( - ethQuery, - ); - const result = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'test_method', - params: [], - }); - expect(result).toBe('test response'); - }); - }); - - it('returns undefined if the provider has not been set yet', async () => { - await withController(({ messenger }) => { - const ethQuery = messenger.call('NetworkController:getEthQuery'); - - expect(ethQuery).toBeUndefined(); - }); - }); - }); - - describe('rollbackToPreviousProvider', () => { - for (const { networkType } of INFURA_NETWORKS) { - describe(`if the previous provider configuration had a type of "${networkType}"`, () => { - it('emits networkWillChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkWillChange).toBeFulfilled(); - }, - ); - }); - - it('emits networkDidChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setActiveNetwork('testNetworkConfiguration'); - - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkDidChange).toBeFulfilled(); - }, - ); - }); - - it('overwrites the the current provider configuration with the previous provider configuration', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.store.getState().providerConfig).toStrictEqual({ - type: 'rpc', - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - nickname: 'test network', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }); - - await controller.rollbackToPreviousProvider(); - - expect(controller.store.getState().providerConfig).toStrictEqual( - buildProviderConfig({ - type: networkType, - }), - ); - }, - ); - }); - - it('resets the network status to "unknown" before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect(controller.store.getState().networkStatus).toBe( - 'unknown', - ); - }, - }); - }, - ); - }); - - it('clears EIP-1559 support for the network from state before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: {}, - }); - }, - }); - }, - ); - }); - - it(`initializes a provider pointed to the "${networkType}" Infura network`, async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - - await controller.rollbackToPreviousProvider(); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); - }, - ); - }); - - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - - await controller.rollbackToPreviousProvider(); - - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); - - it('emits infuraIsBlocked or infuraIsUnblocked, depending on whether Infura is blocking requests for the previous network', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - const promiseForNoInfuraIsUnblockedEvents = - waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - }); - const promiseForInfuraIsBlocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - }); - - await controller.rollbackToPreviousProvider(); - - await expect(promiseForNoInfuraIsUnblockedEvents).toBeFulfilled(); - await expect(promiseForInfuraIsBlocked).toBeFulfilled(); - }, - ); - }); - - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - error: ethErrors.rpc.methodNotFound(), - }, - ]), - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.store.getState().networkStatus).toBe( - 'unavailable', - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - expect(controller.store.getState().networkStatus).toBe( - 'available', - ); - }, - ); - }); - - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: networkType, - }), - networkConfigurations: { - testNetworkConfiguration: { - id: 'testNetworkConfiguration', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - ticker: 'TEST', - }, - }, - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - network: networkType, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setActiveNetwork('testNetworkConfiguration'); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // rollbackToPreviousProvider clears networkDetails first, and - // then updates it to what we expect it to be - count: 2, - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - }, - ); - }); - }); - } - - describe(`if the previous provider configuration had a type of "rpc"`, () => { - it('emits networkWillChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(NETWORK_TYPES.GOERLI); - - const networkWillChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkWillChange).toBeFulfilled(); - }, - ); - }); - - it('emits networkDidChange', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - }), - }, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - await controller.setProviderType(NETWORK_TYPES.GOERLI); - - const networkDidChange = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - controller.rollbackToPreviousProvider(); - }, - }); - - await expect(networkDidChange).toBeFulfilled(); - }, - ); - }); - - it('overwrites the the current provider configuration with the previous provider configuration', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - nickname: 'network', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.store.getState().providerConfig).toStrictEqual({ - type: 'goerli', - rpcUrl: undefined, - chainId: toHex(5), - ticker: 'GoerliETH', - nickname: undefined, - rpcPrefs: { - blockExplorerUrl: 'https://goerli.etherscan.io', - }, - id: undefined, - }); - - await controller.rollbackToPreviousProvider(); - expect(controller.store.getState().providerConfig).toStrictEqual( - buildProviderConfig({ - type: 'rpc', - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - nickname: 'network', - ticker: 'TEST', - rpcPrefs: { - blockExplorerUrl: 'https://test-block-explorer.com', - }, - }), - ); - }, - ); - }); - - it('resets the network state to "unknown" before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.store.getState().networkStatus).toBe('available'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect(controller.store.getState().networkStatus).toBe( - 'unknown', - ); - }, - }); - }, - ); - }); - - it('clears EIP-1559 support for the network from state before updating the provider', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - buildFakeProvider(), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // We only care about the first state change, because it - // happens before networkDidChange - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // while this operation is in-progress - controller.rollbackToPreviousProvider(); - }, - beforeResolving: () => { - expect( - controller.store.getState().networkDetails, - ).toStrictEqual({ - EIPS: {}, - }); - }, - }); - }, - ); - }); - - it('initializes a provider pointed to the given RPC URL', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider(), - buildFakeProvider([ - { - request: { - method: 'test', - }, - response: { - result: 'test response', - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - - await controller.rollbackToPreviousProvider(); - - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider, 'Provider is somehow unset'); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const response = await promisifiedSendAsync({ - id: '1', - jsonrpc: '2.0', - method: 'test', - }); - expect(response.result).toBe('test response'); - }, - ); - }); - - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - - await controller.rollbackToPreviousProvider(); - - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); - - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller, messenger }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - - const promiseForInfuraIsUnblocked = waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await controller.rollbackToPreviousProvider(); - }, - }); - - await expect(promiseForInfuraIsUnblocked).toBeFulfilled(); - }, - ); - }); - - it('checks the status of the previous network again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - error: ethErrors.rpc.methodNotFound(), - }, - ]), - buildFakeProvider([ - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.store.getState().networkStatus).toBe( - 'unavailable', - ); - - await controller.rollbackToPreviousProvider(); - expect(controller.store.getState().networkStatus).toBe('available'); - }, - ); - }); - - it('checks whether the previous network supports EIP-1559 again and updates state accordingly', async () => { - await withController( - { - state: { - providerConfig: buildProviderConfig({ - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - }), - }, - infuraProjectId: 'some-infura-project-id', - }, - async ({ controller }) => { - const fakeProviders = [ - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: PRE_1559_BLOCK, - }, - }, - ]), - buildFakeProvider([ - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: POST_1559_BLOCK, - }, - }, - ]), - ]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - mockCreateNetworkClient() - .calledWith({ - network: NETWORK_TYPES.GOERLI, - infuraProjectId: 'some-infura-project-id', - type: NetworkClientType.Infura, - }) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith({ - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(1337), - type: NetworkClientType.Custom, - }) - .mockReturnValue(fakeNetworkClients[1]); - await controller.setProviderType('goerli'); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - - await controller.rollbackToPreviousProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - }, - ); - }); - }); - }); - - describe('upsertNetworkConfiguration', () => { - it('adds the given network configuration when its rpcURL does not match an existing configuration', async () => { - uuidV4Mock.mockImplementationOnce(() => 'network-configuration-id-1'); - - await withController(async ({ controller }) => { - const rpcUrlNetwork = { - chainId: toHex(9999), - rpcUrl: 'https://test-rpc.com', - ticker: 'RPC', - }; - - expect(controller.store.getState().networkConfigurations).toStrictEqual( - {}, - ); - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual( - expect.arrayContaining([ - { - ...rpcUrlNetwork, - nickname: undefined, - rpcPrefs: undefined, - id: 'network-configuration-id-1', - }, - ]), - ); - }); - }); - - it('update a network configuration when the configuration being added has an rpcURL that matches an existing configuration', async () => { - await withController( - { - state: { - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://rpc-url.com', - ticker: 'old_rpc_ticker', - nickname: 'old_rpc_nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1), - id: 'testNetworkConfigurationId', - }, - }, - }, - }, - async ({ controller }) => { - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://rpc-url.com', - ticker: 'new_rpc_ticker', - nickname: 'new_rpc_nickname', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - }, - { referrer: 'https://test-dapp.com', source: 'dapp' }, - ); - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual( - expect.arrayContaining([ - { - rpcUrl: 'https://rpc-url.com', - nickname: 'new_rpc_nickname', - ticker: 'new_rpc_ticker', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - id: 'testNetworkConfigurationId', - }, - ]), - ); - }, - ); - }); - - it('throws if the given chain ID is not a 0x-prefixed hex number', async () => { - const invalidChainId = '1'; - await withController(async ({ controller }) => { - await expect(async () => - controller.upsertNetworkConfiguration( - { - // @ts-expect-error Intentionally invalid - chainId: invalidChainId, - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - rpcUrl: 'rpc_url', - ticker: 'RPC', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - `Invalid chain ID "${invalidChainId}": invalid hex string.`, - ), - ); - }); - }); - - it('throws if the given chain ID is greater than the maximum allowed ID', async () => { - await withController(async ({ controller }) => { - await expect(async () => - controller.upsertNetworkConfiguration( - { - chainId: '0xFFFFFFFFFFFFFFFF', - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - rpcUrl: 'rpc_url', - ticker: 'RPC', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'Invalid chain ID "0xFFFFFFFFFFFFFFFF": numerical value greater than max safe value.', - ), - ); - }); - }); - - it('throws if the no (or a falsy) rpcUrl is passed', async () => { - await withController(async ({ controller }) => { - await expect(() => - controller.upsertNetworkConfiguration( - /* @ts-expect-error We are intentionally passing bad input. */ - { - chainId: toHex(9999), - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - ticker: 'RPC', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'An rpcUrl is required to add or update network configuration', - ), - ); - }); - }); - - it('throws if rpcUrl passed is not a valid Url', async () => { - await withController(async ({ controller }) => { - await expect(async () => - controller.upsertNetworkConfiguration( - { - chainId: toHex(9999), - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - ticker: 'RPC', - rpcUrl: 'test', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow(new Error('rpcUrl must be a valid URL')); - }); - }); - - it('throws if the no (or a falsy) ticker is passed', async () => { - await withController(async ({ controller }) => { - await expect(async () => - controller.upsertNetworkConfiguration( - // @ts-expect-error - we want to test the case where no ticker is present. - { - chainId: toHex(5), - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - rpcUrl: 'https://mock-rpc-url', - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ), - ).rejects.toThrow( - new Error( - 'A ticker is required to add or update networkConfiguration', - ), - ); - }); - }); - - it('throws if an options object is not passed as a second argument', async () => { - await withController(async ({ controller }) => { - await expect(async () => - // @ts-expect-error - we want to test the case where no second arg is passed. - controller.upsertNetworkConfiguration({ - chainId: toHex(5), - nickname: 'RPC', - rpcPrefs: { blockExplorerUrl: 'test-block-explorer.com' }, - rpcUrl: 'https://mock-rpc-url', - }), - ).rejects.toThrow('Cannot read properties of undefined'); - }); - }); - - it('throws if referrer and source arguments are not passed', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - const trackEventSpy = jest.fn(); - await withController( - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - }, - }, - trackMetaMetricsEvent: trackEventSpy, - }, - async ({ controller }) => { - const newNetworkConfiguration = { - rpcUrl: 'https://new-chain-rpc-url', - chainId: toHex(222), - ticker: 'NEW', - nickname: 'new-chain', - rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, - }; - - await expect(async () => - // @ts-expect-error - we want to test the case where the options object is empty. - controller.upsertNetworkConfiguration(newNetworkConfiguration, {}), - ).rejects.toThrow( - 'referrer and source are required arguments for adding or updating a network configuration', - ); - }, - ); - }); - - it('should add the given network if all required properties are present but nither rpcPrefs nor nickname properties are passed', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - await withController( - { - state: { - networkConfigurations: {}, - }, - }, - async ({ controller }) => { - const rpcUrlNetwork = { - chainId: toHex(1), - rpcUrl: 'https://test-rpc-url', - ticker: 'test_ticker', - }; - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual( - expect.arrayContaining([ - { - ...rpcUrlNetwork, - nickname: undefined, - rpcPrefs: undefined, - id: 'networkConfigurationId', - }, - ]), - ); - }, - ); - }); - - it('adds new networkConfiguration to networkController store, but only adds valid properties (rpcUrl, chainId, ticker, nickname, rpcPrefs) and fills any missing properties from this list as undefined', async function () { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - await withController( - { - state: { - networkConfigurations: {}, - }, - }, - async ({ controller }) => { - const rpcUrlNetwork = { - chainId: toHex(1), - rpcUrl: 'https://test-rpc-url', - ticker: 'test_ticker', - invalidKey: 'new-chain', - invalidKey2: {}, - }; - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual( - expect.arrayContaining([ - { - chainId: toHex(1), - rpcUrl: 'https://test-rpc-url', - ticker: 'test_ticker', - nickname: undefined, - rpcPrefs: undefined, - id: 'networkConfigurationId', - }, - ]), - ); - }, - ); - }); - - it('should add the given network configuration if its rpcURL does not match an existing configuration without changing or overwriting other configurations', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId2'); - await withController( - { - state: { - networkConfigurations: { - networkConfigurationId: { - rpcUrl: 'https://test-rpc-url', - ticker: 'ticker', - nickname: 'nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - }, - }, - }, - async ({ controller }) => { - const rpcUrlNetwork = { - chainId: toHex(1), - nickname: 'RPC', - rpcPrefs: undefined, - rpcUrl: 'https://test-rpc-url-2', - ticker: 'RPC', - }; - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual( - expect.arrayContaining([ - { - rpcUrl: 'https://test-rpc-url', - ticker: 'ticker', - nickname: 'nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - { ...rpcUrlNetwork, id: 'networkConfigurationId2' }, - ]), - ); - }, - ); - }); - - it('should use the given configuration to update an existing network configuration that has a matching rpcUrl', async () => { - await withController( - { - state: { - networkConfigurations: { - networkConfigurationId: { - rpcUrl: 'https://test-rpc-url', - ticker: 'old_rpc_ticker', - nickname: 'old_rpc_chainName', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - }, - }, - }, - - async ({ controller }) => { - const updatedConfiguration = { - rpcUrl: 'https://test-rpc-url', - ticker: 'new_rpc_ticker', - nickname: 'new_rpc_chainName', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - }; - await controller.upsertNetworkConfiguration(updatedConfiguration, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual([ - { - rpcUrl: 'https://test-rpc-url', - nickname: 'new_rpc_chainName', - ticker: 'new_rpc_ticker', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - ]); - }, - ); - }); - - it('should use the given configuration to update an existing network configuration that has a matching rpcUrl without changing or overwriting other networkConfigurations', async () => { - await withController( - { - state: { - networkConfigurations: { - networkConfigurationId: { - rpcUrl: 'https://test-rpc-url', - ticker: 'ticker', - nickname: 'nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - networkConfigurationId2: { - rpcUrl: 'https://test-rpc-url-2', - ticker: 'ticker-2', - nickname: 'nickname-2', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(9999), - id: 'networkConfigurationId2', - }, - }, - }, - }, - async ({ controller }) => { - await controller.upsertNetworkConfiguration( - { - rpcUrl: 'https://test-rpc-url', - ticker: 'new-ticker', - nickname: 'new-nickname', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - }, - { - referrer: 'https://test-dapp.com', - source: 'dapp', - }, - ); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual([ - { - rpcUrl: 'https://test-rpc-url', - ticker: 'new-ticker', - nickname: 'new-nickname', - rpcPrefs: { blockExplorerUrl: 'alternativetestchainscan.io' }, - chainId: toHex(1), - id: 'networkConfigurationId', - }, - { - rpcUrl: 'https://test-rpc-url-2', - ticker: 'ticker-2', - nickname: 'nickname-2', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(9999), - id: 'networkConfigurationId2', - }, - ]); - }, - ); - }); - - it('should add the given network and not set it to active if the setActive option is not passed (or a falsy value is passed)', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - const originalProvider = { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - }; - await withController( - { - state: { - providerConfig: originalProvider, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const rpcUrlNetwork = { - chainId: toHex(222), - rpcUrl: 'https://test-rpc-url', - ticker: 'test_ticker', - }; - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect(controller.store.getState().providerConfig).toStrictEqual( - originalProvider, - ); - }, - ); - }); - - it('should add the given network and set it to active if the setActive option is passed as true', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - await withController( - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - }, - }, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - const rpcUrlNetwork = { - rpcUrl: 'https://test-rpc-url', - chainId: toHex(222), - ticker: 'test_ticker', - }; - - await controller.upsertNetworkConfiguration(rpcUrlNetwork, { - setActive: true, - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect(controller.store.getState().providerConfig).toStrictEqual({ - type: 'rpc', - rpcUrl: 'https://test-rpc-url', - chainId: toHex(222), - ticker: 'test_ticker', - id: 'networkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }); - }, - ); - }); - - it('adds new networkConfiguration to networkController store and calls to the metametrics event tracking with the correct values', async () => { - uuidV4Mock.mockImplementationOnce(() => 'networkConfigurationId'); - const trackEventSpy = jest.fn(); - await withController( - { - state: { - providerConfig: { - type: NETWORK_TYPES.RPC, - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - networkConfigurations: { - testNetworkConfigurationId: { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - }, - }, - trackMetaMetricsEvent: trackEventSpy, - }, - async ({ controller }) => { - const newNetworkConfiguration = { - rpcUrl: 'https://new-chain-rpc-url', - chainId: toHex(222), - ticker: 'NEW', - nickname: 'new-chain', - rpcPrefs: { blockExplorerUrl: 'https://block-explorer' }, - }; - - await controller.upsertNetworkConfiguration(newNetworkConfiguration, { - referrer: 'https://test-dapp.com', - source: 'dapp', - }); - - expect( - Object.values(controller.store.getState().networkConfigurations), - ).toStrictEqual([ - { - rpcUrl: 'https://mock-rpc-url', - chainId: toHex(111), - ticker: 'TEST', - id: 'testNetworkConfigurationId', - nickname: undefined, - rpcPrefs: undefined, - }, - { - ...newNetworkConfiguration, - id: 'networkConfigurationId', - }, - ]); - expect(trackEventSpy).toHaveBeenCalledWith({ - event: 'Custom Network Added', - category: 'Network', - referrer: { - url: 'https://test-dapp.com', - }, - properties: { - chain_id: toHex(222), - symbol: 'NEW', - source: 'dapp', - }, - }); - }, - ); - }); - }); - - describe('removeNetworkConfigurations', () => { - it('remove a network configuration', async () => { - const testNetworkConfigurationId = 'testNetworkConfigurationId'; - await withController( - { - state: { - networkConfigurations: { - [testNetworkConfigurationId]: { - rpcUrl: 'https://rpc-url.com', - ticker: 'old_rpc_ticker', - nickname: 'old_rpc_nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1337), - id: testNetworkConfigurationId, - }, - }, - }, - }, - async ({ controller }) => { - controller.removeNetworkConfiguration(testNetworkConfigurationId); - expect( - controller.store.getState().networkConfigurations, - ).toStrictEqual({}); - }, - ); - }); - - it('throws if the networkConfigurationId it is passed does not correspond to a network configuration in state', async () => { - const testNetworkConfigurationId = 'testNetworkConfigurationId'; - const invalidNetworkConfigurationId = 'invalidNetworkConfigurationId'; - await withController( - { - state: { - networkConfigurations: { - [testNetworkConfigurationId]: { - rpcUrl: 'https://rpc-url.com', - ticker: 'old_rpc_ticker', - nickname: 'old_rpc_nickname', - rpcPrefs: { blockExplorerUrl: 'testchainscan.io' }, - chainId: toHex(1337), - id: testNetworkConfigurationId, - }, - }, - }, - }, - async ({ controller }) => { - expect(() => - controller.removeNetworkConfiguration( - invalidNetworkConfigurationId, - ), - ).toThrow( - `networkConfigurationId ${invalidNetworkConfigurationId} does not match a configured networkConfiguration`, - ); - }, - ); - }); - }); -}); - -/** - * Creates a mocked version of `createNetworkClient` where multiple mock - * invocations can be specified. A default implementation is provided so that if - * none of the actual invocations of the function match the mock invocations - * then an error will be thrown. - */ -function mockCreateNetworkClient() { - return when(createNetworkClientMock).mockImplementation((options) => { - const inspectedOptions = inspect(options, { depth: null, compact: true }); - const lines = [ - `No fake network client was specified for ${inspectedOptions}.`, - 'Make sure to mock this invocation of `createNetworkClient`.', - ]; - if ('infuraProjectId' in options) { - lines.push( - '(You might have forgotten to pass an `infuraProjectId` to `withController`.)', - ); - } - throw new Error(lines.join('\n')); - }); -} - -/** - * Test an operation that performs a `#switchNetwork` call with the given - * provider configuration. All effects of the `#switchNetwork` call should be - * covered by these tests. - * - * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. - * @param args.initialState - The initial state of the network controller. - * @param args.operation - The operation to test. - */ -function refreshNetworkTests({ - expectedProviderConfig, - initialState, - operation, -}: { - expectedProviderConfig: ProviderConfiguration; - initialState?: Partial; - operation: (controller: NetworkController) => Promise; -}) { - it('emits networkWillChange', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkWillChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkWillChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - operation(controller); - }, - }); - - await expect(networkWillChange).toBeTruthy(); - }, - ); - }); - - it('emits networkDidChange', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - const fakeProvider = buildFakeProvider(); - const fakeNetworkClient = buildFakeClient(fakeProvider); - mockCreateNetworkClient().mockReturnValue(fakeNetworkClient); - - const networkDidChange = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:networkDidChange', - operation: () => { - // Intentionally not awaited because we're capturing an event - // emitted partway through the operation - operation(controller); - }, - }); - - await expect(networkDidChange).toBeTruthy(); - }, - ); - }); - - it('clears network id from state', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: { - result: '1', - }, - }, - // Called during network lookup after resetting connection. - // Delayed to ensure that we can check the network id - // before this resolves. - { - delay: 1, - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: '0x1', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - expect(controller.store.getState().networkId).toBe('1'); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // We only care about the first state change, because it - // happens before the network lookup - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // partway through the operation - operation(controller); - }, - }); - - expect(controller.store.getState().networkId).toBeNull(); - }, - ); - }); - - it('clears network status from state', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - // Called during network lookup after resetting connection. - // Delayed to ensure that we can check the network status - // before this resolves. - { - delay: 1, - request: { - method: 'net_version', - }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - }, - { - delay: 1, - request: { - method: 'eth_getBlockByNumber', - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - expect(controller.store.getState().networkStatus).toBe( - NetworkStatus.Available, - ); - - await waitForStateChanges({ - controller, - propertyPath: ['networkStatus'], - // We only care about the first state change, because it - // happens before the network lookup - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // partway through the operation - operation(controller); - }, - }); - - expect(controller.store.getState().networkStatus).toBe( - NetworkStatus.Unknown, - ); - }, - ); - }); - - it('clears network details from state', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - // Called during provider initialization - { - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: '0x1', - }, - }, - // Called during network lookup after resetting connection. - // Delayed to ensure that we can check the network details - // before this resolves. - { - delay: 1, - request: { - method: 'eth_getBlockByNumber', - }, - response: { - result: '0x1', - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - await controller.initializeProvider(); - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: false, - }, - }); - - await waitForStateChanges({ - controller, - propertyPath: ['networkDetails'], - // We only care about the first state change, because it - // happens before the network lookup - count: 1, - operation: () => { - // Intentionally not awaited because we want to check state - // partway through the operation - operation(controller); - }, - }); - - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: {}, - }); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('sets the provider to a custom RPC provider initialized with the RPC target and chain ID', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_chainId', - }, - response: { - result: toHex(111), - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await operation(controller); - - expect(createNetworkClientMock).toHaveBeenCalledWith({ - chainId: expectedProviderConfig.chainId, - rpcUrl: expectedProviderConfig.rpcUrl, - type: NetworkClientType.Custom, - }); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const chainIdResult = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }); - expect(chainIdResult.result).toBe(toHex(111)); - }, - ); - }); - } else { - it(`sets the provider to an Infura provider pointed to ${expectedProviderConfig.type}`, async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProvider = buildFakeProvider([ - { - request: { - method: 'eth_chainId', - }, - response: { - result: toHex(1337), - }, - }, - ]); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - - await operation(controller); - - expect(createNetworkClientMock).toHaveBeenCalledWith({ - network: expectedProviderConfig.type, - infuraProjectId: 'infura-project-id', - type: NetworkClientType.Infura, - }); - const { provider } = controller.getProviderAndBlockTracker(); - assert(provider); - const promisifiedSendAsync = promisify(provider.sendAsync).bind( - provider, - ); - const chainIdResult = await promisifiedSendAsync({ - id: 1, - jsonrpc: '2.0', - method: 'eth_chainId', - params: [], - }); - expect(chainIdResult.result).toBe(toHex(1337)); - }, - ); - }); - } - - it('replaces the provider object underlying the provider proxy without creating a new instance of the proxy itself', async () => { - await withController( - { - infuraProjectId: 'infura-project-id', - state: initialState, - }, - async ({ controller }) => { - const fakeProviders = [buildFakeProvider(), buildFakeProvider()]; - const fakeNetworkClients = [ - buildFakeClient(fakeProviders[0]), - buildFakeClient(fakeProviders[1]), - ]; - const providerType = controller.store.getState().providerConfig.type; - const initializationNetworkClientOptions: Parameters< - typeof createNetworkClient - >[0] = - providerType === NETWORK_TYPES.RPC - ? { - chainId: controller.store.getState().providerConfig.chainId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: controller.store.getState().providerConfig.rpcUrl!, - type: NetworkClientType.Custom, - } - : { - network: providerType, - infuraProjectId: 'infura-project-id', - type: NetworkClientType.Infura, - }; - const operationNetworkClientOptions: Parameters< - typeof createNetworkClient - >[0] = - expectedProviderConfig.type === NETWORK_TYPES.RPC - ? { - chainId: toHex(expectedProviderConfig.chainId), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - rpcUrl: expectedProviderConfig.rpcUrl!, - type: NetworkClientType.Custom, - } - : { - network: expectedProviderConfig.type, - infuraProjectId: 'infura-project-id', - type: NetworkClientType.Infura, - }; - mockCreateNetworkClient() - .calledWith(initializationNetworkClientOptions) - .mockReturnValue(fakeNetworkClients[0]) - .calledWith(operationNetworkClientOptions) - .mockReturnValue(fakeNetworkClients[1]); - await controller.initializeProvider(); - const { provider: providerBefore } = - controller.getProviderAndBlockTracker(); - - await operation(controller); - - const { provider: providerAfter } = - controller.getProviderAndBlockTracker(); - expect(providerBefore).toBe(providerAfter); - }, - ); - }); - - lookupNetworkTests({ expectedProviderConfig, initialState, operation }); -} - -/** - * Test an operation that performs a `lookupNetwork` call with the given - * provider configuration. All effects of the `lookupNetwork` call should be - * covered by these tests. - * - * @param args - Arguments. - * @param args.expectedProviderConfig - The provider configuration that the - * operation is expected to set. - * @param args.initialState - The initial state of the network controller. - * @param args.operation - The operation to test. - */ -function lookupNetworkTests({ - expectedProviderConfig, - initialState, - operation, -}: { - expectedProviderConfig: ProviderConfiguration; - initialState?: Partial; - operation: (controller: NetworkController) => Promise; -}) { - describe('if the network ID and network details requests resolve successfully', () => { - const validNetworkIds = [12345, '12345', toHex(12345)]; - for (const networkId of validNetworkIds) { - describe(`with a network id of '${networkId}'`, () => { - describe('if the current network is different from the network in state', () => { - it('updates the network in state to match', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: networkId }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkId).toBe('12345'); - }, - ); - }); - }); - - describe('if the version of the current network is the same as that in state', () => { - it('does not change network ID in state', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: networkId }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - await expect(controller.store.getState().networkId).toBe( - '12345', - ); - }, - ); - }); - }); - }); - } - - describe('if the network details of the current network are different from the network details in state', () => { - it('updates the network in state to match', async () => { - await withController( - { - state: { - ...initialState, - networkDetails: { - EIPS: { - 1559: false, - }, - }, - }, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', - }, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - }, - ); - }); - }); - - describe('if the network details of the current network are the same as the network details in state', () => { - it('does not change network details in state', async () => { - await withController( - { - state: { - ...initialState, - networkDetails: { - EIPS: { - 1559: true, - }, - }, - }, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: { - result: { - baseFeePerGas: '0x1', - }, - }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkDetails).toStrictEqual({ - EIPS: { - 1559: true, - }, - }); - }, - ); - }); - }); - - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); - - describe('if an RPC error is encountered while retrieving the version of the current network', () => { - it('updates the network in state to "unavailable"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unavailable'); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); - - describe('if a country blocked error is encountered while retrieving the version of the current network', () => { - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('updates the network in state to "blocked"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('blocked'); - }, - ); - }); - - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - - it('emits infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - }); - - describe('if an internal error is encountered while retrieving the version of the current network', () => { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); - - describe('if an invalid network ID is returned', () => { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { method: 'net_version' }, - response: { result: 'invalid' }, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); - - describe('if an RPC error is encountered while retrieving the network details of the current network', () => { - it('updates the network in state to "unavailable"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unavailable'); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.limitExceeded('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); - - describe('if a country blocked error is encountered while retrieving the network details of the current network', () => { - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('updates the network in state to "blocked"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('blocked'); - }, - ); - }); - - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - - it('emits infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: BLOCKED_INFURA_JSON_RPC_ERROR, - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - }); - - describe('if an internal error is encountered while retrieving the network details of the current network', () => { - it('updates the network in state to "unknown"', async () => { - await withController( - { - state: initialState, - }, - async ({ controller }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.internal('some error'), - }, - ], - }); - - await operation(controller); - - expect(controller.store.getState().networkStatus).toBe('unknown'); - }, - ); - }); - - if (expectedProviderConfig.type === NETWORK_TYPES.RPC) { - it('emits infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } else { - it('does not emit infuraIsUnblocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsUnblocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - } - - it('does not emit infuraIsBlocked', async () => { - await withController( - { - state: initialState, - }, - async ({ controller, messenger }) => { - await setFakeProvider(controller, { - stubs: [ - { - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - error: ethErrors.rpc.internal('some error'), - }, - ], - stubLookupNetworkWhileSetting: true, - }); - - const payloads = await waitForPublishedEvents({ - messenger, - eventType: 'NetworkController:infuraIsBlocked', - count: 0, - operation: async () => { - await operation(controller); - }, - }); - - expect(payloads).toBeTruthy(); - }, - ); - }); - }); -} - -/** - * Builds a controller messenger. - * - * @returns The controller messenger. - */ -function buildMessenger() { - return new ControllerMessenger< - NetworkControllerAction, - NetworkControllerEvent - >(); -} - -/** - * Build a restricted controller messenger for the network controller. - * - * @param messenger - A controller messenger. - * @returns The network controller restricted messenger. - */ -function buildNetworkControllerMessenger(messenger = buildMessenger()) { - return messenger.getRestricted({ - name: 'NetworkController', - allowedActions: [ - 'NetworkController:getProviderConfig', - 'NetworkController:getEthQuery', - ], - allowedEvents: [ - 'NetworkController:networkDidChange', - 'NetworkController:networkWillChange', - 'NetworkController:infuraIsBlocked', - 'NetworkController:infuraIsUnblocked', - ], - }); -} - -/** - * The callback that `withController` takes. - */ -type WithControllerCallback = ({ - controller, -}: { - controller: NetworkController; - messenger: ControllerMessenger< - NetworkControllerAction, - NetworkControllerEvent - >; -}) => Promise | ReturnValue; - -/** - * A partial form of the options that `NetworkController` takes. - */ -type WithControllerOptions = Partial; - -/** - * The arguments that `withController` takes. - */ -type WithControllerArgs = - | [WithControllerCallback] - | [ - Omit, - WithControllerCallback, - ]; - -/** - * Builds a controller based on the given options, and calls the given function - * with that controller. - * - * @param args - Either a function, or an options bag + a function. The options - * bag is equivalent to the options that NetworkController takes (although - * `messenger` and `infuraProjectId` are filled in if not given); the function - * will be called with the built controller. - * @returns Whatever the callback returns. - */ -async function withController( - ...args: WithControllerArgs -): Promise { - const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; - const messenger = buildMessenger(); - const restrictedMessenger = buildNetworkControllerMessenger(messenger); - const controller = new NetworkController({ - messenger: restrictedMessenger, - trackMetaMetricsEvent: jest.fn(), - infuraProjectId: 'infura-project-id', - ...rest, - }); - try { - return await fn({ controller, messenger }); - } finally { - await controller.destroy(); - } -} - -/** - * Builds a complete ProviderConfig object, filling in values that are not - * provided with defaults. - * - * @param config - An incomplete ProviderConfig object. - * @returns The complete ProviderConfig object. - */ -function buildProviderConfig( - config: Partial = {}, -): ProviderConfiguration { - if (config.type && config.type !== NETWORK_TYPES.RPC) { - const networkConfig = INFURA_NETWORKS.find( - ({ networkType }) => networkType === config.type, - ); - if (!networkConfig) { - throw new Error(`Invalid type: ${config.type}`); - } - return { - ...networkConfig, - // This is redundant with the spread operation below, but this was - // required for TypeScript to understand that this property was set to an - // Infura type. - type: config.type, - ...config, - }; - } - return { - type: NETWORK_TYPES.RPC, - chainId: '0x42', - nickname: undefined, - rpcUrl: 'http://doesntmatter.com', - ...config, - }; -} - -/** - * Builds an object that `createInfuraProvider` or `createJsonRpcClient` returns. - * - * @param provider - provider to use if you dont want the defaults - * @returns The object. - */ -function buildFakeClient( - provider: SafeEventEmitterProvider = buildFakeProvider(), -) { - return { - provider, - blockTracker: new FakeBlockTracker({ provider }), - }; -} - -/** - * Builds fake provider engine object that `createMetamaskProvider` returns, - * with canned responses optionally provided for certain RPC methods. - * - * @param stubs - The list of RPC methods you want to stub along with their - * responses. - * @returns The object. - */ -function buildFakeProvider(stubs: FakeProviderStub[] = []) { - const completeStubs = stubs.slice(); - if (!stubs.some((stub) => stub.request.method === 'eth_getBlockByNumber')) { - completeStubs.unshift({ - request: { - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - response: SUCCESSFUL_ETH_GET_BLOCK_BY_NUMBER_RESPONSE, - discardAfterMatching: false, - }); - } - if (!stubs.some((stub) => stub.request.method === 'net_version')) { - completeStubs.unshift({ - request: { method: 'net_version' }, - response: SUCCESSFUL_NET_VERSION_RESPONSE, - discardAfterMatching: false, - }); - } - return new FakeProvider({ stubs: completeStubs }); -} - -/** - * Asks the controller to set the provider in the simplest way, stubbing the - * provider appropriately so as not to cause any errors to be thrown. This is - * useful in tests where it doesn't matter how the provider gets set, just that - * it does. Canned responses may be optionally provided for certain RPC methods - * on the provider. - * - * @param controller - The network controller. - * @param options - Additional options. - * @param options.stubs - The set of RPC methods you want to stub on the - * provider along with their responses. - * @param options.stubLookupNetworkWhileSetting - Whether to stub the call to - * `lookupNetwork` that happens when the provider is set. This option is useful - * in tests that need a provider to get set but also call `lookupNetwork` on - * their own. In this case, since the `providerConfig` setter already calls - * `lookupNetwork` once, and since `lookupNetwork` is called out of band, the - * test may run with unexpected results. By stubbing `lookupNetwork` before - * setting the provider, the test is free to explicitly call it. - * @returns The set provider. - */ -async function setFakeProvider( - controller: NetworkController, - { - stubs = [], - stubLookupNetworkWhileSetting = false, - }: { - stubs?: FakeProviderStub[]; - stubLookupNetworkWhileSetting?: boolean; - } = {}, -): Promise { - const fakeProvider = buildFakeProvider(stubs); - const fakeNetworkClient = buildFakeClient(fakeProvider); - createNetworkClientMock.mockReturnValue(fakeNetworkClient); - const lookupNetworkMock = jest.spyOn(controller, 'lookupNetwork'); - - if (stubLookupNetworkWhileSetting) { - lookupNetworkMock.mockResolvedValue(undefined); - } - - await controller.initializeProvider(); - assert(controller.getProviderAndBlockTracker().provider); - - if (stubLookupNetworkWhileSetting) { - lookupNetworkMock.mockRestore(); - } -} - -/** - * Waits for changes to the primary observable store of a controller to occur - * before proceeding. May be called with a function, in which case waiting will - * occur after the function is called; or may be called standalone if you want - * to assert that no state changes occurred. - * - * @param args - The arguments. - * @param args.controller - The network controller. - * @param args.propertyPath - The path of the property you expect the state - * changes to concern. - * @param args.count - The number of events you expect to occur. If null, this - * function will wait until no events have occurred in `wait` number of - * milliseconds. Default: 1. - * @param args.duration - The amount of time in milliseconds to wait for the - * expected number of filtered state changes to occur before resolving the - * promise that this function returns (default: 150). - * @param args.operation - A function to run that will presumably produce the - * state changes in question. - * @param args.beforeResolving - In some tests, state updates happen so fast, we - * need to make an assertion immediately after the event in question occurs. - * However, if we wait until the promise this function returns resolves to do - * so, some other state update to the same - * property may have happened. This option allows you to make an assertion - * _before_ the promise resolves. This has the added benefit of allowing you to - * maintain the "arrange, act, assert" ordering in your test, meaning that you - * can still call the method that kicks off the event and then make the - * assertion afterward instead of the other way around. - * @returns A promise that resolves to an array of state objects (that is, the - * contents of the store) when the specified number of filtered state changes - * have occurred, or all of them if no number has been specified. - */ -async function waitForStateChanges({ - controller, - propertyPath, - count: expectedInterestingStateCount = 1, - duration: timeBeforeAssumingNoMoreStateChanges = 150, - operation = () => { - // do nothing - }, - beforeResolving = async () => { - // do nothing - }, -}: { - controller: NetworkController; - propertyPath?: string[]; - count?: number | null; - duration?: number; - operation?: () => void | Promise; - beforeResolving?: () => void | Promise; -}) { - const initialState = { ...controller.store.getState() }; - let isTimerRunning = false; - - const promiseForStateChanges = new Promise((resolve, reject) => { - // We need to declare this variable first, then assign it later, so that - // ESLint won't complain that resetTimer is referring to this variable - // before it's declared. And we need to use let so that we can assign it - // below. - /* eslint-disable-next-line prefer-const */ - let eventListener: (...args: any[]) => void; - let timer: NodeJS.Timeout | undefined; - const allStates: NetworkControllerState[] = []; - const interestingStates: NetworkControllerState[] = []; - - const stopTimer = () => { - if (timer) { - clearTimeout(timer); - } - isTimerRunning = false; - }; - - async function end() { - stopTimer(); - - controller.store.unsubscribe(eventListener); - - await beforeResolving(); - - const shouldEnd = - expectedInterestingStateCount === null - ? interestingStates.length > 0 - : interestingStates.length === expectedInterestingStateCount; - - if (shouldEnd) { - resolve(interestingStates); - } else { - // Using a string instead of an Error leads to better backtraces. - /* eslint-disable-next-line prefer-promise-reject-errors */ - const expectedInterestingStateCountFragment = - expectedInterestingStateCount === null - ? 'any number of' - : expectedInterestingStateCount; - const propertyPathFragment = - propertyPath === undefined ? '' : ` on \`${propertyPath.join('.')}\``; - const actualInterestingStateCountFragment = - expectedInterestingStateCount === null - ? 'none' - : interestingStates.length; - const primaryMessage = `Expected to receive ${expectedInterestingStateCountFragment} state change(s)${propertyPathFragment}, but received ${actualInterestingStateCountFragment} after ${timeBeforeAssumingNoMoreStateChanges}ms.`; - reject( - [ - primaryMessage, - 'Initial state:', - inspect(initialState, { depth: null }), - 'All state changes (without filtering):', - inspect(allStates, { depth: null }), - 'Filtered state changes:', - inspect(interestingStates, { depth: null }), - ].join('\n\n'), - ); - } - } - - const resetTimer = () => { - stopTimer(); - timer = setTimeout(() => { - if (isTimerRunning) { - end(); - } - }, timeBeforeAssumingNoMoreStateChanges); - isTimerRunning = true; - }; - - eventListener = (newState) => { - const isInteresting = - propertyPath === undefined - ? true - : isStateChangeInteresting( - newState, - allStates.length > 0 - ? allStates[allStates.length - 1] - : initialState, - propertyPath, - ); - - allStates.push({ ...newState }); - - if (isInteresting) { - interestingStates.push(newState); - if (interestingStates.length === expectedInterestingStateCount) { - end(); - } else { - resetTimer(); - } - } - }; - - controller.store.subscribe(eventListener); - resetTimer(); - }); - - await operation(); - - return await promiseForStateChanges; -} - -/** - * Waits for controller events to be emitted before proceeding. - * - * @param args - The arguments to this function. - * @param args.messenger - The messenger suited for NetworkController. - * @param args.eventType - The type of NetworkController event you want to wait - * for. - * @param args.count - The number of events you expect to occur (default: 1). - * @param args.filter - A function used to discard events that are not of - * interest. - * @param args.wait - The amount of time in milliseconds to wait for the - * expected number of filtered events to occur before resolving the promise that - * this function returns (default: 150). - * @param args.operation - A function to run that will presumably produce the - * events in question. - * @param args.beforeResolving - In some tests, state updates happen so fast, we - * need to make an assertion immediately after the event in question occurs. - * However, if we wait until the promise this function returns resolves to do - * so, some other state update to the same - * property may have happened. This option allows you to make an assertion - * _before_ the promise resolves. This has the added benefit of allowing you to - * maintain the "arrange, act, assert" ordering in your test, meaning that you - * can still call the method that kicks off the event and then make the - * assertion afterward instead of the other way around. - * @returns A promise that resolves to the list of payloads for the set of - * events, optionally filtered, when a specific number of them have occurred. - */ -async function waitForPublishedEvents({ - messenger, - eventType, - count: expectedNumberOfEvents = 1, - filter: isEventPayloadInteresting = () => true, - wait: timeBeforeAssumingNoMoreEvents = 150, - operation = () => { - // do nothing - }, - beforeResolving = async () => { - // do nothing - }, -}: { - messenger: ControllerMessenger; - eventType: E['type']; - count?: number; - filter?: (payload: E['payload']) => boolean; - wait?: number; - operation?: () => void | Promise; - beforeResolving?: () => void | Promise; -}): Promise { - const promiseForEventPayloads = new Promise( - (resolve, reject) => { - let timer: NodeJS.Timeout | undefined; - const allEventPayloads: E['payload'][] = []; - const interestingEventPayloads: E['payload'][] = []; - let alreadyEnded = false; - - // We're using `any` here because there seems to be some mismatch between - // the signature of `subscribe` and the way that we're using it. Try - // changing `any` to either `((...args: E['payload']) => void)` or - // `ExtractEventHandler` to see the issue. - const eventListener: any = (...payload: E['payload']) => { - allEventPayloads.push(payload); - - if (isEventPayloadInteresting(payload)) { - interestingEventPayloads.push(payload); - if (interestingEventPayloads.length === expectedNumberOfEvents) { - stopTimer(); - end(); - } else { - resetTimer(); - } - } - }; - - async function end() { - if (!alreadyEnded) { - alreadyEnded = true; - messenger.unsubscribe(eventType, eventListener); - - await beforeResolving(); - - if (interestingEventPayloads.length === expectedNumberOfEvents) { - resolve(interestingEventPayloads); - } else { - // Using a string instead of an Error leads to better backtraces. - /* eslint-disable-next-line prefer-promise-reject-errors */ - reject( - `Expected to receive ${expectedNumberOfEvents} ${eventType} event(s), but received ${ - interestingEventPayloads.length - } after ${timeBeforeAssumingNoMoreEvents}ms.\n\nAll payloads:\n\n${inspect( - allEventPayloads, - { depth: null }, - )}`, - ); - } - } - } - - function stopTimer() { - if (timer) { - clearTimeout(timer); - } - } - - function resetTimer() { - stopTimer(); - timer = setTimeout(() => { - end(); - }, timeBeforeAssumingNoMoreEvents); - } - - messenger.subscribe(eventType, eventListener); - resetTimer(); - }, - ); - - if (operation) { - await operation(); - } - - return await promiseForEventPayloads; -} - -/** - * Returns whether two places in different state objects have different values. - * - * @param currentState - The current state object. - * @param prevState - The previous state object. - * @param propertyPath - A property path within both objects. - * @returns True or false, depending on the result. - */ -function isStateChangeInteresting( - currentState: Record, - prevState: Record, - propertyPath: PropertyKey[], -): boolean { - return !isDeepStrictEqual( - get(currentState, propertyPath), - get(prevState, propertyPath), - ); -} diff --git a/app/scripts/controllers/network/network-controller.ts b/app/scripts/controllers/network/network-controller.ts deleted file mode 100644 index a4417a8e2..000000000 --- a/app/scripts/controllers/network/network-controller.ts +++ /dev/null @@ -1,1196 +0,0 @@ -import { strict as assert } from 'assert'; -import EventEmitter from 'events'; -import { ObservableStore } from '@metamask/obs-store'; -import log from 'loglevel'; -import { - createSwappableProxy, - createEventEmitterProxy, - SwappableProxy, -} from '@metamask/swappable-obj-proxy'; -import EthQuery from 'eth-query'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; -import { v4 as uuid } from 'uuid'; -import { Hex, isPlainObject, isStrictHexString } from '@metamask/utils'; -import { errorCodes } from 'eth-rpc-errors'; -import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider'; -import { PollingBlockTracker } from 'eth-block-tracker'; -import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; -import { - INFURA_PROVIDER_TYPES, - INFURA_BLOCKED_KEY, - TEST_NETWORK_TICKER_MAP, - CHAIN_IDS, - NETWORK_TYPES, - BUILT_IN_INFURA_NETWORKS, - BuiltInInfuraNetwork, - NetworkStatus, -} from '../../../../shared/constants/network'; -import { - isPrefixedFormattedHexString, - isSafeChainId, -} from '../../../../shared/modules/network.utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventPayload, -} from '../../../../shared/constants/metametrics'; -import { isErrorWithMessage } from '../../../../shared/modules/error'; -import { - createNetworkClient, - NetworkClientType, -} from './create-network-client'; - -/** - * The name of NetworkController. - */ -const name = 'NetworkController'; - -/** - * A block header object that `eth_getBlockByNumber` returns. Note that this - * type does not specify all of the properties present within the block header; - * within NetworkController, we are only interested in `baseFeePerGas`. - */ -type Block = { - baseFeePerGas?: unknown; -}; - -/** - * Encodes a few pieces of information: - * - * - Whether or not a provider is configured for an Infura network or a - * non-Infura network. - * - If an Infura network, then which network. - * - If a non-Infura network, then whether the network exists locally or - * remotely. - * - * Primarily used to build the network client and check the availability of a - * network. - */ -export type ProviderType = BuiltInInfuraNetwork | typeof NETWORK_TYPES.RPC; - -/** - * The network ID of a network. - */ -type NetworkId = `${number}`; - -/** - * The ID of a network configuration. - */ -type NetworkConfigurationId = string; - -/** - * The chain ID of a network. - */ -type ChainId = Hex; - -/** - * `networkWillChange` is published when the current network is about to be - * switched, but the new provider has not been created and no state changes have - * occurred yet. - */ -export type NetworkControllerNetworkWillChangeEvent = { - type: 'NetworkController:networkWillChange'; - payload: []; -}; - -/** - * `networkDidChange` is published after a provider has been created for a newly - * switched network (but before the network has been confirmed to be available). - */ -export type NetworkControllerNetworkDidChangeEvent = { - type: 'NetworkController:networkDidChange'; - payload: []; -}; - -/** - * `infuraIsBlocked` is published after the network is switched to an Infura - * network, but when Infura returns an error blocking the user based on their - * location. - */ -export type NetworkControllerInfuraIsBlockedEvent = { - type: 'NetworkController:infuraIsBlocked'; - payload: []; -}; - -/** - * `infuraIsBlocked` is published either after the network is switched to an - * Infura network and Infura does not return an error blocking the user based on - * their location, or the network is switched to a non-Infura network. - */ -export type NetworkControllerInfuraIsUnblockedEvent = { - type: 'NetworkController:infuraIsUnblocked'; - payload: []; -}; - -/** - * The set of events that the NetworkController messenger can publish. - */ -export type NetworkControllerEvent = - | NetworkControllerNetworkDidChangeEvent - | NetworkControllerNetworkWillChangeEvent - | NetworkControllerInfuraIsBlockedEvent - | NetworkControllerInfuraIsUnblockedEvent; - -export type NetworkControllerGetProviderConfigAction = { - type: `NetworkController:getProviderConfig`; - handler: () => ProviderConfiguration; -}; - -export type NetworkControllerGetEthQueryAction = { - type: `NetworkController:getEthQuery`; - handler: () => EthQuery | undefined; -}; - -export type NetworkControllerAction = - | NetworkControllerGetProviderConfigAction - | NetworkControllerGetEthQueryAction; - -/** - * The messenger that the NetworkController uses to publish events. - */ -export type NetworkControllerMessenger = RestrictedControllerMessenger< - typeof name, - NetworkControllerAction, - NetworkControllerEvent, - string, - string ->; - -/** - * Information used to set up the middleware stack for a particular kind of - * network. Currently has overlap with `NetworkConfiguration`, although the - * two will be merged down the road. - */ -export type ProviderConfiguration = { - /** - * Either a type of Infura network, "localhost" for a locally operated - * network, or "rpc" for everything else. - */ - type: ProviderType; - /** - * The chain ID as per EIP-155. - */ - chainId: ChainId; - /** - * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". - */ - rpcUrl?: string; - /** - * The shortname of the currency used by the network. - */ - ticker?: string; - /** - * The user-customizable name of the network. - */ - nickname?: string; - /** - * User-customizable details for the network. - */ - rpcPrefs?: { - blockExplorerUrl?: string; - }; - /** - * The ID of the network configuration used to build this provider config. - */ - id?: NetworkConfigurationId; -}; - -/** - * The contents of the `networkId` store. - */ -type NetworkIdState = NetworkId | null; - -/** - * Information about the network not held by any other part of state. Currently - * only used to capture whether a network supports EIP-1559. - */ -type NetworkDetails = { - /** - * EIPs supported by the network. - */ - EIPS: { - [eipNumber: number]: boolean | undefined; - }; - [otherProperty: string]: unknown; -}; - -/** - * A "network configuration" represents connection data directly provided by - * users via the wallet UI for a custom network (we already have this - * information for networks that come pre-shipped with the wallet). Ultimately - * used to set up the middleware stack so that the wallet can make requests to - * the network. Currently has overlap with `ProviderConfiguration`, although the - * two will be merged down the road. - */ -type NetworkConfiguration = { - /** - * The unique ID of the network configuration. Useful for switching to and - * removing specific networks. - */ - id: NetworkConfigurationId; - /** - * The URL of the RPC endpoint. Only used when `type` is "localhost" or "rpc". - */ - rpcUrl: string; - /** - * The chain ID as per EIP-155. - */ - chainId: ChainId; - /** - * The shortname of the currency used for this network. - */ - ticker: string; - /** - * The user-customizable name of the network. - */ - nickname?: string; - /** - * User-customizable details for the network. - */ - rpcPrefs?: { - blockExplorerUrl: string; - }; -}; - -/** - * A set of network configurations, keyed by ID. - */ -type NetworkConfigurations = Record< - NetworkConfigurationId, - NetworkConfiguration ->; - -/** - * The state that NetworkController holds after combining its individual stores. - */ -export type NetworkControllerState = { - providerConfig: ProviderConfiguration; - networkId: NetworkIdState; - networkStatus: NetworkStatus; - networkDetails: NetworkDetails; - networkConfigurations: NetworkConfigurations; -}; - -/** - * The options that NetworkController takes. - */ -export type NetworkControllerOptions = { - messenger: NetworkControllerMessenger; - state?: { - providerConfig?: ProviderConfiguration; - networkDetails?: NetworkDetails; - networkConfigurations?: NetworkConfigurations; - }; - infuraProjectId: string; - trackMetaMetricsEvent: (payload: MetaMetricsEventPayload) => void; -}; - -/** - * Type guard for determining whether the given value is an error object with a - * `code` property, such as an instance of Error. - * - * TODO: Move this to @metamask/utils - * - * @param error - The object to check. - * @returns True if `error` has a `code`, false otherwise. - */ -function isErrorWithCode(error: unknown): error is { code: string | number } { - return typeof error === 'object' && error !== null && 'code' in error; -} - -/** - * Convert the given value into a valid network ID. The ID is accepted - * as either a number, a decimal string, or a 0x-prefixed hex string. - * - * @param value - The network ID to convert, in an unknown format. - * @returns A valid network ID (as a decimal string) - * @throws If the given value cannot be safely parsed. - */ -function convertNetworkId(value: unknown): NetworkId { - if (typeof value === 'number' && !Number.isNaN(value)) { - return `${value}`; - } else if (isStrictHexString(value)) { - return hexToDecimal(value) as NetworkId; - } else if (typeof value === 'string' && /^\d+$/u.test(value)) { - return value as NetworkId; - } - throw new Error(`Cannot parse as a valid network ID: '${value}'`); -} - -/** - * Builds the default provider config used to initialize the network controller. - */ -function buildDefaultProviderConfigState(): ProviderConfiguration { - if (process.env.IN_TEST) { - return { - type: NETWORK_TYPES.RPC, - rpcUrl: 'http://localhost:8545', - chainId: '0x539', - nickname: 'Localhost 8545', - ticker: 'ETH', - }; - } else if ( - process.env.METAMASK_DEBUG || - process.env.METAMASK_ENVIRONMENT === 'test' - ) { - return { - type: NETWORK_TYPES.GOERLI, - chainId: CHAIN_IDS.GOERLI, - ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], - }; - } - - return { - type: NETWORK_TYPES.MAINNET, - chainId: CHAIN_IDS.MAINNET, - ticker: 'ETH', - }; -} - -/** - * Builds the default network ID state used to initialize the network - * controller. - */ -function buildDefaultNetworkIdState(): NetworkIdState { - return null; -} - -/** - * Builds the default network status state used to initialize the network - * controller. - */ -function buildDefaultNetworkStatusState(): NetworkStatus { - return NetworkStatus.Unknown; -} - -/** - * Builds the default network details state used to initialize the - * network controller. - */ -function buildDefaultNetworkDetailsState(): NetworkDetails { - return { - EIPS: {}, - }; -} - -/** - * Builds the default network configurations state used to initialize the - * network controller. - */ -function buildDefaultNetworkConfigurationsState(): NetworkConfigurations { - return {}; -} - -/** - * Builds the default state for the network controller. - * - * @returns The default network controller state. - */ -function buildDefaultState() { - return { - providerConfig: buildDefaultProviderConfigState(), - networkId: buildDefaultNetworkIdState(), - networkStatus: buildDefaultNetworkStatusState(), - networkDetails: buildDefaultNetworkDetailsState(), - networkConfigurations: buildDefaultNetworkConfigurationsState(), - }; -} - -/** - * Returns whether the given argument is a type that our Infura middleware - * recognizes. We can't calculate this inline because the usual type of `type`, - * which we get from the provider config, is not a subset of the type of - * `INFURA_PROVIDER_TYPES`, but rather a superset, and therefore we cannot make - * a proper comparison without TypeScript complaining. However, if we downcast - * both variables, then we are able to achieve this. As a bonus, this function - * also types the given argument as a `BuiltInInfuraNetwork` assuming that the - * check succeeds. - * - * @param type - A type to compare. - * @returns True or false, depending on whether the given type is one that our - * Infura middleware recognizes. - */ -function isInfuraProviderType(type: string): type is BuiltInInfuraNetwork { - const infuraProviderTypes: readonly string[] = INFURA_PROVIDER_TYPES; - return infuraProviderTypes.includes(type); -} - -/** - * The network controller creates and manages the "provider" object which allows - * our code and external dapps to make requests to a network. The requests are - * filtered through a set of middleware (provided by - * [`eth-json-rpc-middleware`][1]) which not only performs the HTTP request to - * the appropriate RPC endpoint but also uses caching to limit duplicate - * requests to Infura and smoothens interactions with the blockchain in general. - * - * [1]: https://github.com/MetaMask/eth-json-rpc-middleware - */ -export class NetworkController extends EventEmitter { - /** - * The messenger that NetworkController uses to publish events. - */ - #messenger: NetworkControllerMessenger; - - /** - * Observable store containing the provider configuration for the previously - * configured network. - */ - #previousProviderConfig: ProviderConfiguration; - - /** - * Observable store containing a combination of data from all of the - * individual stores. - */ - store: ObservableStore; - - #provider: SafeEventEmitterProvider | null; - - #blockTracker: PollingBlockTracker | null; - - #providerProxy: SwappableProxy | null; - - #blockTrackerProxy: SwappableProxy | null; - - #ethQuery: EthQuery | undefined; - - #infuraProjectId: NetworkControllerOptions['infuraProjectId']; - - #trackMetaMetricsEvent: NetworkControllerOptions['trackMetaMetricsEvent']; - - /** - * Constructs a network controller. - * - * @param options - Options for this constructor. - * @param options.messenger - The NetworkController messenger. - * @param options.state - Initial controller state. - * @param options.infuraProjectId - The Infura project ID. - * @param options.trackMetaMetricsEvent - A method to forward events to the - * {@link MetaMetricsController}. - */ - constructor({ - messenger, - state = {}, - infuraProjectId, - trackMetaMetricsEvent, - }: NetworkControllerOptions) { - super(); - - this.#messenger = messenger; - - this.store = new ObservableStore({ - ...buildDefaultState(), - ...state, - }); - this.#previousProviderConfig = this.store.getState().providerConfig; - - // provider and block tracker - this.#provider = null; - this.#blockTracker = null; - - // provider and block tracker proxies - because the network changes - this.#providerProxy = null; - this.#blockTrackerProxy = null; - - if (!infuraProjectId || typeof infuraProjectId !== 'string') { - throw new Error('Invalid Infura project ID'); - } - this.#infuraProjectId = infuraProjectId; - this.#trackMetaMetricsEvent = trackMetaMetricsEvent; - - this.#messenger.registerActionHandler(`${name}:getProviderConfig`, () => { - return this.store.getState().providerConfig; - }); - this.#messenger.registerActionHandler(`${name}:getEthQuery`, () => { - return this.#ethQuery; - }); - } - - /** - * Deactivates the controller, stopping any ongoing polling. - * - * In-progress requests will not be aborted. - */ - async destroy(): Promise { - await this.#blockTracker?.destroy(); - } - - /** - * Creates the provider and block tracker for the configured network, - * using the provider to gather details about the network. - */ - async initializeProvider(): Promise { - const { type, rpcUrl, chainId } = this.store.getState().providerConfig; - this.#configureProvider({ type, rpcUrl, chainId }); - await this.lookupNetwork(); - } - - /** - * Returns the proxies wrapping the currently set provider and block tracker. - */ - getProviderAndBlockTracker(): { - provider: SwappableProxy | null; - blockTracker: SwappableProxy | null; - } { - const provider = this.#providerProxy; - const blockTracker = this.#blockTrackerProxy; - return { provider, blockTracker }; - } - - /** - * Determines whether the network supports EIP-1559 by checking whether the - * latest block has a `baseFeePerGas` property, then updates state - * appropriately. - * - * @returns A promise that resolves to true if the network supports EIP-1559 - * and false otherwise. - */ - async getEIP1559Compatibility(): Promise { - const { EIPS } = this.store.getState().networkDetails; - // NOTE: This isn't necessary anymore because the block cache middleware - // already prevents duplicate requests from taking place - if (EIPS[1559] !== undefined) { - return EIPS[1559]; - } - - const { provider } = this.getProviderAndBlockTracker(); - if (!provider) { - // Really we should throw an error if a provider hasn't been initialized - // yet, but that might have undesirable repercussions, so return false for - // now - return false; - } - - const supportsEIP1559 = await this.#determineEIP1559Compatibility(provider); - const { networkDetails } = this.store.getState(); - this.store.updateState({ - networkDetails: { - ...networkDetails, - EIPS: { - ...networkDetails.EIPS, - 1559: supportsEIP1559, - }, - }, - }); - return supportsEIP1559; - } - - /** - * Performs side effects after switching to a network. If the network is - * available, updates the network state with the network ID of the network and - * stores whether the network supports EIP-1559; otherwise clears said - * information about the network that may have been previously stored. - * - * @fires infuraIsBlocked if the network is Infura-supported and is blocking - * requests. - * @fires infuraIsUnblocked if the network is Infura-supported and is not - * blocking requests, or if the network is not Infura-supported. - */ - async lookupNetwork(): Promise { - const { type } = this.store.getState().providerConfig; - const { provider } = this.getProviderAndBlockTracker(); - let networkChanged = false; - let networkId: NetworkIdState = null; - let supportsEIP1559 = false; - let networkStatus: NetworkStatus; - - if (provider === null) { - log.warn( - 'NetworkController - lookupNetwork aborted due to missing provider', - ); - return; - } - - const isInfura = isInfuraProviderType(type); - - const listener = () => { - networkChanged = true; - this.#messenger.unsubscribe( - 'NetworkController:networkDidChange', - listener, - ); - }; - this.#messenger.subscribe('NetworkController:networkDidChange', listener); - - try { - const results = await Promise.all([ - this.#getNetworkId(provider), - this.#determineEIP1559Compatibility(provider), - ]); - const possibleNetworkId = results[0]; - networkId = convertNetworkId(possibleNetworkId); - supportsEIP1559 = results[1]; - networkStatus = NetworkStatus.Available; - } catch (error) { - if (isErrorWithCode(error)) { - let responseBody; - if (isInfura && isErrorWithMessage(error)) { - try { - responseBody = JSON.parse(error.message); - } catch { - // error.message must not be JSON - } - } - - if ( - isPlainObject(responseBody) && - responseBody.error === INFURA_BLOCKED_KEY - ) { - networkStatus = NetworkStatus.Blocked; - } else if (error.code === errorCodes.rpc.internal) { - networkStatus = NetworkStatus.Unknown; - } else { - networkStatus = NetworkStatus.Unavailable; - } - } else { - log.warn( - 'NetworkController - could not determine network status', - error, - ); - networkStatus = NetworkStatus.Unknown; - } - } - - if (networkChanged) { - // If the network has changed, then `lookupNetwork` either has been or is - // in the process of being called, so we don't need to go further. - return; - } - this.#messenger.unsubscribe('NetworkController:networkDidChange', listener); - - this.store.updateState({ - networkStatus, - }); - - if (networkStatus === NetworkStatus.Available) { - const { networkDetails } = this.store.getState(); - this.store.updateState({ - networkId, - networkDetails: { - ...networkDetails, - EIPS: { - ...networkDetails.EIPS, - 1559: supportsEIP1559, - }, - }, - }); - } else { - this.#resetNetworkId(); - this.#resetNetworkDetails(); - } - - if (isInfura) { - if (networkStatus === NetworkStatus.Available) { - this.#messenger.publish('NetworkController:infuraIsUnblocked'); - } else if (networkStatus === NetworkStatus.Blocked) { - this.#messenger.publish('NetworkController:infuraIsBlocked'); - } - } else { - // Always publish infuraIsUnblocked regardless of network status to - // prevent consumers from being stuck in a blocked state if they were - // previously connected to an Infura network that was blocked - this.#messenger.publish('NetworkController:infuraIsUnblocked'); - } - } - - /** - * Switches to the network specified by a network configuration. - * - * @param networkConfigurationId - The unique identifier that refers to a - * previously added network configuration. - * @returns The URL of the RPC endpoint representing the newly switched - * network. - */ - async setActiveNetwork(networkConfigurationId: NetworkConfigurationId) { - const targetNetwork = - this.store.getState().networkConfigurations[networkConfigurationId]; - - if (!targetNetwork) { - throw new Error( - `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, - ); - } - - await this.#setProviderConfig({ - type: NETWORK_TYPES.RPC, - ...targetNetwork, - }); - - return targetNetwork.rpcUrl; - } - - /** - * Switches to an Infura-supported network. - * - * @param type - The shortname of the network. - * @throws if the `type` is "rpc" or if it is not a known Infura-supported - * network. - */ - async setProviderType(type: string) { - assert.notStrictEqual( - type, - NETWORK_TYPES.RPC, - `NetworkController - cannot call "setProviderType" with type "${NETWORK_TYPES.RPC}". Use "setActiveNetwork"`, - ); - assert.ok( - isInfuraProviderType(type), - `Unknown Infura provider type "${type}".`, - ); - const network = BUILT_IN_INFURA_NETWORKS[type]; - await this.#setProviderConfig({ - type, - rpcUrl: undefined, - chainId: network.chainId, - ticker: 'ticker' in network ? network.ticker : 'ETH', - nickname: undefined, - rpcPrefs: { blockExplorerUrl: network.blockExplorerUrl }, - id: undefined, - }); - } - - /** - * Re-initializes the provider and block tracker for the current network. - */ - async resetConnection() { - await this.#setProviderConfig(this.store.getState().providerConfig); - } - - /** - * Switches to the previous network, assuming that the current network is - * different than the initial network (if it is, then this is equivalent to - * calling `resetConnection`). - */ - async rollbackToPreviousProvider() { - const config = this.#previousProviderConfig; - this.store.updateState({ - providerConfig: config, - }); - await this.#switchNetwork(config); - } - - /** - * Fetches the latest block for the network. - * - * @param provider - A provider, which is guaranteed to be available. - * @returns A promise that either resolves to the block header or null if - * there is no latest block, or rejects with an error. - */ - #getLatestBlock(provider: SafeEventEmitterProvider): Promise { - return new Promise((resolve, reject) => { - const ethQuery = new EthQuery(provider); - ethQuery.sendAsync<['latest', false], Block | null>( - { method: 'eth_getBlockByNumber', params: ['latest', false] }, - (...args) => { - if (args[0] === null) { - resolve(args[1]); - } else { - reject(args[0]); - } - }, - ); - }); - } - - /** - * Fetches the network ID for the network. - * - * @param provider - A provider, which is guaranteed to be available. - * @returns A promise that either resolves to the network ID, or rejects with - * an error. - */ - async #getNetworkId(provider: SafeEventEmitterProvider): Promise { - const ethQuery = new EthQuery(provider); - return await new Promise((resolve, reject) => { - ethQuery.sendAsync( - { method: 'net_version' }, - (...args) => { - if (args[0] === null) { - resolve(args[1]); - } else { - reject(args[0]); - } - }, - ); - }); - } - - /** - * Clears the stored network ID. - */ - #resetNetworkId(): void { - this.store.updateState({ - networkId: buildDefaultNetworkIdState(), - }); - } - - /** - * Resets network status to the default ("unknown"). - */ - #resetNetworkStatus(): void { - this.store.updateState({ - networkStatus: buildDefaultNetworkStatusState(), - }); - } - - /** - * Clears details previously stored for the network. - */ - #resetNetworkDetails(): void { - this.store.updateState({ - networkDetails: buildDefaultNetworkDetailsState(), - }); - } - - /** - * Stores the given provider configuration representing a network in state, - * then uses it to create a new provider for that network. - * - * @param providerConfig - The provider configuration. - */ - async #setProviderConfig(providerConfig: ProviderConfiguration) { - this.#previousProviderConfig = this.store.getState().providerConfig; - this.store.updateState({ providerConfig }); - await this.#switchNetwork(providerConfig); - } - - /** - * Retrieves the latest block from the currently selected network; if the - * block has a `baseFeePerGas` property, then we know that the network - * supports EIP-1559; otherwise it doesn't. - * - * @param provider - A provider, which is guaranteed to be available. - * @returns A promise that resolves to true if the network supports EIP-1559 - * and false otherwise. - */ - async #determineEIP1559Compatibility( - provider: SafeEventEmitterProvider, - ): Promise { - const latestBlock = await this.#getLatestBlock(provider); - return latestBlock?.baseFeePerGas !== undefined; - } - - /** - * Executes a series of steps to change the current network: - * - * 1. Notifies subscribers that the network is about to change. - * 2. Clears state associated with the current network. - * 3. Creates a new network client along with a provider for the desired - * network. - * 4. Notifies subscribes that the network has changed. - * - * @param providerConfig - The provider configuration object that specifies - * the new network. - */ - async #switchNetwork(providerConfig: ProviderConfiguration) { - const { type, rpcUrl, chainId } = providerConfig; - this.#messenger.publish('NetworkController:networkWillChange'); - this.#resetNetworkId(); - this.#resetNetworkStatus(); - this.#resetNetworkDetails(); - this.#configureProvider({ type, rpcUrl, chainId }); - this.#messenger.publish('NetworkController:networkDidChange'); - await this.lookupNetwork(); - } - - /** - * Creates a network client (a stack of middleware along with a provider and - * block tracker) to talk to a network. - * - * @param args - The arguments. - * @param args.type - The provider type. - * @param args.rpcUrl - The URL of the RPC endpoint that represents the - * network. Only used for non-Infura networks. - * @param args.chainId - The chain ID of the network (as per EIP-155). Only - * used for non-Infura-supported networks (as we already know the chain ID of - * any Infura-supported network). - * @throws if the `type` if not a known Infura-supported network. - */ - #configureProvider({ - type, - rpcUrl, - chainId, - }: { - type: ProviderType; - rpcUrl: string | undefined; - chainId: Hex | undefined; - }): void { - const isInfura = isInfuraProviderType(type); - if (isInfura) { - this.#configureInfuraProvider({ - type, - infuraProjectId: this.#infuraProjectId, - }); - } else if (type === NETWORK_TYPES.RPC) { - if (chainId === undefined) { - throw new Error('chainId must be provided for custom RPC endpoints'); - } - if (rpcUrl === undefined) { - throw new Error('rpcUrl must be provided for custom RPC endpoints'); - } - this.#configureStandardProvider(rpcUrl, chainId); - } else { - throw new Error(`Unrecognized network type: '${type}'`); - } - } - - /** - * Creates a new instance of EthQuery that wraps the current provider and - * saves it for future usage. - */ - #registerProvider() { - const { provider } = this.getProviderAndBlockTracker(); - - if (provider) { - this.#ethQuery = new EthQuery(provider); - } - } - - /** - * Creates a network client (a stack of middleware along with a provider and - * block tracker) to talk to an Infura-supported network. - * - * @param args - The arguments. - * @param args.type - The shortname of the Infura network (see - * {@link NETWORK_TYPES}). - * @param args.infuraProjectId - An Infura API key. ("Project ID" is a - * now-obsolete term we've retained for backward compatibility.) - */ - #configureInfuraProvider({ - type, - infuraProjectId, - }: { - type: BuiltInInfuraNetwork; - infuraProjectId: NetworkControllerOptions['infuraProjectId']; - }): void { - log.info('NetworkController - #configureInfuraProvider', type); - const { provider, blockTracker } = createNetworkClient({ - network: type, - infuraProjectId, - type: NetworkClientType.Infura, - }); - this.#updateProvider(provider, blockTracker); - } - - /** - * Creates a network client (a stack of middleware along with a provider and - * block tracker) to talk to a non-Infura-supported network. - * - * @param rpcUrl - The URL of the RPC endpoint that represents the network. - * @param chainId - The chain ID of the network (as per EIP-155). - */ - #configureStandardProvider(rpcUrl: string, chainId: ChainId): void { - log.info('NetworkController - #configureStandardProvider', rpcUrl); - const { provider, blockTracker } = createNetworkClient({ - chainId, - rpcUrl, - type: NetworkClientType.Custom, - }); - this.#updateProvider(provider, blockTracker); - } - - /** - * Given a provider and a block tracker, updates any proxies pointing to - * these objects that have been previously set, or initializes any proxies - * that have not been previously set, then creates an instance of EthQuery - * that wraps the provider. - * - * @param provider - The provider. - * @param blockTracker - The block tracker. - */ - #updateProvider( - provider: SafeEventEmitterProvider, - blockTracker: PollingBlockTracker, - ) { - this.#setProviderAndBlockTracker({ - provider, - blockTracker, - }); - this.#registerProvider(); - } - - /** - * Given a provider and a block tracker, updates any proxies pointing to - * these objects that have been previously set, or initializes any proxies - * that have not been previously set. - * - * @param args - The arguments. - * @param args.provider - The provider. - * @param args.blockTracker - The block tracker. - */ - #setProviderAndBlockTracker({ - provider, - blockTracker, - }: { - provider: SafeEventEmitterProvider; - blockTracker: PollingBlockTracker; - }): void { - // update or initialize proxies - if (this.#providerProxy) { - this.#providerProxy.setTarget(provider); - } else { - this.#providerProxy = createSwappableProxy(provider); - } - if (this.#blockTrackerProxy) { - this.#blockTrackerProxy.setTarget(blockTracker); - } else { - this.#blockTrackerProxy = createEventEmitterProxy(blockTracker, { - eventFilter: 'skipInternal', - }); - } - // set new provider and blockTracker - this.#provider = provider; - this.#blockTracker = blockTracker; - } - - /** - * Network Configuration management functions - */ - - /** - * Updates an existing network configuration matching the same RPC URL as the - * given network configuration; otherwise adds the network configuration. - * Following the upsert, the `trackMetaMetricsEvent` callback specified - * via the NetworkController constructor will be called to (presumably) create - * a MetaMetrics event. - * - * @param networkConfiguration - The network configuration to upsert. - * @param networkConfiguration.chainId - The chain ID of the network as per - * EIP-155. - * @param networkConfiguration.ticker - The shortname of the currency used by - * the network. - * @param networkConfiguration.nickname - The user-customizable name of the - * network. - * @param networkConfiguration.rpcPrefs - User-customizable details for the - * network. - * @param networkConfiguration.rpcUrl - The URL of the RPC endpoint. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.setActive - Switches to the network specified by - * the given network configuration following the upsert. - * @param additionalArgs.referrer - The site from which the call originated, - * or 'metamask' for internal calls; used for event metrics. - * @param additionalArgs.source - Where the metric event originated (i.e. from - * a dapp or from the network form); used for event metrics. - * @throws if the `chainID` does not match EIP-155 or is too large. - * @throws if `rpcUrl` is not a valid URL. - * @returns The ID for the added or updated network configuration. - */ - async upsertNetworkConfiguration( - { - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - }: Omit, - { - setActive = false, - referrer, - source, - }: { - setActive?: boolean; - referrer: string; - source: string; - }, - ): Promise { - assert.ok( - isPrefixedFormattedHexString(chainId), - `Invalid chain ID "${chainId}": invalid hex string.`, - ); - assert.ok( - isSafeChainId(parseInt(chainId, 16)), - `Invalid chain ID "${chainId}": numerical value greater than max safe value.`, - ); - - if (!rpcUrl) { - throw new Error( - 'An rpcUrl is required to add or update network configuration', - ); - } - - if (!referrer || !source) { - throw new Error( - 'referrer and source are required arguments for adding or updating a network configuration', - ); - } - - try { - // eslint-disable-next-line no-new - new URL(rpcUrl); - } catch (e) { - if (isErrorWithMessage(e) && e.message.includes('Invalid URL')) { - throw new Error('rpcUrl must be a valid URL'); - } - } - - if (!ticker) { - throw new Error( - 'A ticker is required to add or update networkConfiguration', - ); - } - - const { networkConfigurations } = this.store.getState(); - const newNetworkConfiguration = { - rpcUrl, - chainId, - ticker, - nickname, - rpcPrefs, - }; - - const oldNetworkConfigurationId = Object.values(networkConfigurations).find( - (networkConfiguration) => - networkConfiguration.rpcUrl?.toLowerCase() === rpcUrl?.toLowerCase(), - )?.id; - - const newNetworkConfigurationId = oldNetworkConfigurationId || uuid(); - this.store.updateState({ - networkConfigurations: { - ...networkConfigurations, - [newNetworkConfigurationId]: { - ...newNetworkConfiguration, - id: newNetworkConfigurationId, - }, - }, - }); - - if (!oldNetworkConfigurationId) { - this.#trackMetaMetricsEvent({ - event: 'Custom Network Added', - category: MetaMetricsEventCategory.Network, - referrer: { - url: referrer, - }, - properties: { - chain_id: chainId, - symbol: ticker, - source, - }, - }); - } - - if (setActive) { - await this.setActiveNetwork(newNetworkConfigurationId); - } - - return newNetworkConfigurationId; - } - - /** - * Removes a network configuration from state. - * - * @param networkConfigurationId - The unique id for the network configuration - * to remove. - */ - removeNetworkConfiguration(networkConfigurationId: NetworkConfigurationId) { - if (!this.store.getState().networkConfigurations[networkConfigurationId]) { - throw new Error( - `networkConfigurationId ${networkConfigurationId} does not match a configured networkConfiguration`, - ); - } - const networkConfigurations = { - ...this.store.getState().networkConfigurations, - }; - delete networkConfigurations[networkConfigurationId]; - this.store.updateState({ - networkConfigurations, - }); - } -} diff --git a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts b/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts deleted file mode 100644 index 4ee0f633d..000000000 --- a/app/scripts/controllers/network/provider-api-tests/block-hash-in-response.ts +++ /dev/null @@ -1,284 +0,0 @@ -/* eslint-disable jest/require-top-level-describe, jest/no-export */ - -import { - ProviderType, - withMockedCommunications, - withNetworkClient, -} from './helpers'; - -type TestsForRpcMethodThatCheckForBlockHashInResponseOptions = { - providerType: ProviderType; - numberOfParameters: number; -}; - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * use `blockHash` in the response data to determine whether the response is - * cacheable. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.numberOfParameters - The number of parameters supported - * by the method under test. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom` (default: "infura"). - */ -export function testsForRpcMethodsThatCheckForBlockHashInResponse( - method: string, - { - numberOfParameters, - providerType, - }: TestsForRpcMethodThatCheckForBlockHashInResponseOptions, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('does not hit the RPC endpoint more than once for identical requests and it has a valid blockHash', async () => { - const requests = [{ method }, { method }]; - const mockResult = { blockHash: '0x1' }; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResult }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResult, mockResult]); - }); - }); - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [{ method }, { method }]; - const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not occur - // because of the second RPC request, but rather because we call - // `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - it('does not reuse the result of a previous request if result.blockHash was null', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { blockHash: null, extra: 'some value' }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - it('does not reuse the result of a previous request if result.blockHash was undefined', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { extra: 'some value' }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - it('does not reuse the result of a previous request if result.blockHash was "0x0000000000000000000000000000000000000000000000000000000000000000"', async () => { - const requests = [{ method }, { method }]; - const mockResults = [ - { - blockHash: - '0x0000000000000000000000000000000000000000000000000000000000000000', - extra: 'some value', - }, - { blockHash: '0x100', extra: 'some other value' }, - ]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { method }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [{ method }, { method }]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - it(`does not reuse the result of a previous request with a valid blockHash if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = [{ blockHash: '0x100' }, { blockHash: '0x200' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } -} diff --git a/app/scripts/controllers/network/provider-api-tests/block-param.ts b/app/scripts/controllers/network/provider-api-tests/block-param.ts deleted file mode 100644 index 92fb65f04..000000000 --- a/app/scripts/controllers/network/provider-api-tests/block-param.ts +++ /dev/null @@ -1,2070 +0,0 @@ -/* eslint-disable jest/require-top-level-describe, jest/no-export */ - -import { - buildMockParams, - buildRequestWithReplacedBlockParam, - ProviderType, - waitForPromiseToBeFulfilledAfterRunningAllTimers, - withMockedCommunications, - withNetworkClient, -} from './helpers'; -import { - buildFetchFailedErrorMessage, - buildInfuraClientRetriesExhaustedErrorMessage, - buildJsonRpcEngineEmptyResponseErrorMessage, -} from './shared-tests'; - -type TestsForRpcMethodSupportingBlockParam = { - providerType: ProviderType; - blockParamIndex: number; - numberOfParameters: number; -}; - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * takes a block parameter. The value of this parameter can be either a block - * number or a block tag ("latest", "earliest", or "pending") and affects how - * the method is cached. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.blockParamIndex - The index of the block parameter. - * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. - * @param additionalArgs.providerType - The type of provider being tested. - * either `infura` or `custom` (default: "infura"). - */ -/* eslint-disable-next-line jest/no-export */ -export function testsForRpcMethodSupportingBlockParam( - method: string, - { - blockParamIndex, - numberOfParameters, - providerType, - }: TestsForRpcMethodSupportingBlockParam, -) { - describe.each([ - ['given no block tag', undefined], - ['given a block tag of "latest"', 'latest'], - ])('%s', (_desc, blockParam) => { - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - if (paramIndex === blockParamIndex) { - // testing changes in block param is covered under later tests - continue; - } - - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - firstMockParams[blockParamIndex] = blockParam; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [ - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. - // The first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not - // occur because of the second RPC request, but rather because we - // call `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x200' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x200', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { - const requests = [ - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - { method, params: buildMockParams({ blockParamIndex, blockParam }) }, - ]; - const mockResults = ['first result', 'second result', 'third result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number, and we delay it. - comms.mockRpcCall({ - delay: 100, - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - // The previous two requests will happen again, in the same order. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[2], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[2] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the - // handlers, so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }, - ); - - expect(results).toStrictEqual([ - mockResults[0], - mockResults[0], - mockResults[0], - ]); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 405 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 405, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'The method does not exist / is not available', - ); - }); - }); - - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) - // because of what both middleware treat as rate limiting errors. In this - // case, the fetch middleware treats a 418 response from the RPC endpoint as - // such an error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - id: 123, - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":123,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws an error with a custom message if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); - }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); - }); - } - - it('throws an undescriptive error message if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - id: 12345, - jsonrpc: '2.0', - error: 'some error', - httpStatus: 420, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - const msg = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(msg); - }); - }); - - [503, 504].forEach((httpStatus) => { - it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'some error', - httpStatus, - }, - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura middleware and custom RPC middleware detect a 503 or - // 504 response and retry the request to the RPC endpoint automatically - // but differ in what sort of response is returned when the number of - // retries is exhausted. - if (providerType === 'infura') { - it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout'), - ); - }); - }); - } else { - it(`produces an empty response if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - }); - - it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'ETIMEDOUT: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but each - // produces a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('produces an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // The Infura middleware treats a response that contains an ECONNRESET - // message as an innocuous error that is likely to disappear on a retry. The - // custom RPC middleware, on the other hand, does not specially handle this - // error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - } - - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will - // also attempt to retry the request. However, this error handling code is - // slightly different between the two. As the error in this case is a - // SyntaxError, the Infura middleware will catch it immediately, whereas the - // custom RPC middleware will catch it and re-throw a separate error, which - // it then catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if a "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if a "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'failed to parse response body: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'failed to parse response body: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('produces an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - const errorMessage = 'Failed to fetch: Some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - }); - - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - params: buildMockParams({ blockParam, blockParamIndex }), - }; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - // - // Here we have the request fail for the first 4 tries, then - // succeed on the 5th try. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: 'Failed to fetch: Some message', - times: 4, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('produces an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the - // block-cache middleware will request the latest block number - // through the block tracker to determine the cache key. Later, - // the block-ref middleware will request the latest block number - // again to resolve the value of "latest", but the block number is - // cached once made, so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - }); - - describe.each([ - ['given a block tag of "earliest"', 'earliest', 'earliest'], - ['given a block number', 'block number', '0x100'], - ])('%s', (_desc, blockParamType, blockParam) => { - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. This block number doesn't - // matter. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - if (paramIndex === blockParamIndex) { - // testing changes in block param is covered under later tests - continue; - } - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - firstMockParams[blockParamIndex] = blockParam; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the block-cache - // middleware will request the latest block number through the block - // tracker to determine the cache key. Later, the block-ref - // middleware will request the latest block number again to resolve - // the value of "latest", but the block number is cached once made, - // so we only need to mock the request once. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // The block-ref middleware will make the request as specified - // except that the block param is replaced with the latest block - // number. - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('reuses the result of a previous request even if the latest block number was updated since', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC - // request. The second block tracker request, however, does not - // occur because of the second RPC request, but rather because we - // call `clock.runAll()` below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a - // new block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - if (blockParamType === 'earliest') { - it('treats "0x00" as a synonym for "earliest"', async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x00' }), - }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. It - // doesn't matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - } - - if (blockParamType === 'block number') { - it('does not reuse the result of a previous request if it was made with different arguments than this one', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const requests = [ - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x100' }), - }, - { - method, - params: buildMockParams({ blockParamIndex, blockParam: '0x200' }), - }, - ]; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: 'first result' }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: 'second result' }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(['first result', 'second result']); - }); - }); - - describe.each( - [ - ['less than the current block number', '0x200'], - ['equal to the curent block number', '0x100'], - ] as any, - '%s', - (_nestedDesc: string, currentBlockNumber: string) => { - it('makes an additional request to the RPC endpoint', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: 'the result' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - if (providerType === 'infura') { - it(`retries up to 10 times if a "${emptyValue}" response is returned, returning successful non-empty response if there is one on the 10th try`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: emptyValue }, - times: 9, - }); - comms.mockRpcCall({ - request, - response: { result: 'some value' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall, clock }) => - waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ), - ); - - expect(result).toStrictEqual('some value'); - }, - ); - }); - - it(`retries up to 10 times if a "${emptyValue}" response is returned, failing after the 10th try`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - times: 10, - }); - - const promiseForResult = withNetworkClient( - { providerType }, - ({ makeRpcCall, clock }) => - waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ), - ); - - await expect(promiseForResult).rejects.toThrow( - 'RetryOnEmptyMiddleware - retries exhausted', - ); - }, - ); - }); - } else { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }, - ); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications( - { providerType }, - async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ - blockNumber: currentBlockNumber, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => - makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }, - ); - }); - } - } - }, - ); - - describe('greater than the current block number', () => { - it('makes an additional request to the RPC endpoint', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request, - response: { result: 'the result' }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - request, - blockParamIndex, - '0x100', - ), - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [ - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - { - method, - // Note that `blockParam` is `0x100` here - params: buildMockParams({ blockParamIndex, blockParam }), - }, - ]; - const mockResults = [emptyValue, { blockHash: '0x100' }]; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x42' }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[0], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: buildRequestWithReplacedBlockParam( - requests[1], - blockParamIndex, - '0x100', - ), - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - }); - } - }); - - describe('given a block tag of "pending"', () => { - const params = buildMockParams({ blockParamIndex, blockParam: 'pending' }); - - it('hits the RPC endpoint on all calls and does not cache anything', async () => { - const requests = [ - { method, params }, - { method, params }, - ]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest - // block number is retrieved through the block tracker first. It - // doesn't matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - }); -} diff --git a/app/scripts/controllers/network/provider-api-tests/helpers.ts b/app/scripts/controllers/network/provider-api-tests/helpers.ts deleted file mode 100644 index 104715d13..000000000 --- a/app/scripts/controllers/network/provider-api-tests/helpers.ts +++ /dev/null @@ -1,543 +0,0 @@ -import nock, { Scope as NockScope } from 'nock'; -import sinon from 'sinon'; -import type { JSONRPCResponse } from '@json-rpc-specification/meta-schema'; -import EthQuery from 'eth-query'; -import { Hex } from '@metamask/utils'; -import { BuiltInInfuraNetwork } from '../../../../../shared/constants/network'; -import { - createNetworkClient, - NetworkClientType, -} from '../create-network-client'; - -/** - * A dummy value for the `infuraProjectId` option that `createInfuraClient` - * needs. (Infura should not be hit during tests, but just in case, this should - * not refer to a real project ID.) - */ -const MOCK_INFURA_PROJECT_ID = 'abc123'; - -/** - * A dummy value for the `rpcUrl` option that `createJsonRpcClient` needs. (This - * should not be hit during tests, but just in case, this should also not refer - * to a real Infura URL.) - */ -const MOCK_RPC_URL = 'http://foo.com'; - -/** - * A default value for the `eth_blockNumber` request that the block tracker - * makes. - */ -const DEFAULT_LATEST_BLOCK_NUMBER = '0x42'; - -/** - * A reference to the original `setTimeout` function so that we can use it even - * when using fake timers. - */ -const originalSetTimeout = setTimeout; - -/** - * If you're having trouble writing a test and you're wondering why the test - * keeps failing, you can set `process.env.DEBUG_PROVIDER_TESTS` to `1`. This - * will turn on some extra logging. - * - * @param args - The arguments that `console.log` takes. - */ -function debug(...args: any) { - if (process.env.DEBUG_PROVIDER_TESTS === '1') { - console.log(...args); - } -} - -/** - * Builds a Nock scope object for mocking provider requests. - * - * @param rpcUrl - The URL of the RPC endpoint. - * @returns The nock scope. - */ -function buildScopeForMockingRequests(rpcUrl: string): NockScope { - return nock(rpcUrl).filteringRequestBody((body) => { - debug('Nock Received Request: ', body); - return body; - }); -} - -type Request = { method: string; params?: any[] }; -type Response = { - id?: number | string; - jsonrpc?: '2.0'; - error?: any; - result?: any; - httpStatus?: number; -}; -type ResponseBody = { body: JSONRPCResponse }; -type BodyOrResponse = ResponseBody | Response; -type CurriedMockRpcCallOptions = { - request: Request; - // The response data. - response?: BodyOrResponse; - /** - * An error to throw while making the request. - * Takes precedence over `response`. - */ - error?: Error | string; - /** - * The amount of time that should pass before the - * request resolves with the response. - */ - delay?: number; - /** - * The number of times that the request is - * expected to be made. - */ - times?: number; -}; - -type MockRpcCallOptions = { - // A nock scope (a set of mocked requests scoped to a certain base URL). - nockScope: nock.Scope; -} & CurriedMockRpcCallOptions; - -type MockRpcCallResult = nock.Interceptor | nock.Scope; - -/** - * Mocks a JSON-RPC request sent to the provider with the given response. - * Provider type is inferred from the base url set on the nockScope. - * - * @param args - The arguments. - * @param args.nockScope - A nock scope (a set of mocked requests scoped to a - * certain base URL). - * @param args.request - The request data. - * @param args.response - Information concerning the response that the request - * should have. If a `body` property is present, this is taken as the complete - * response body. If an `httpStatus` property is present, then it is taken as - * the HTTP status code to respond with. Properties other than these two are - * used to build a complete response body (including `id` and `jsonrpc` - * properties). - * @param args.error - An error to throw while making the request. Takes - * precedence over `response`. - * @param args.delay - The amount of time that should pass before the request - * resolves with the response. - * @param args.times - The number of times that the request is expected to be - * made. - * @returns The nock scope. - */ -function mockRpcCall({ - nockScope, - request, - response, - error, - delay, - times, -}: MockRpcCallOptions): MockRpcCallResult { - // eth-query always passes `params`, so even if we don't supply this property, - // for consistency with makeRpcCall, assume that the `body` contains it - const { method, params = [], ...rest } = request; - let httpStatus = 200; - let completeResponse: JSONRPCResponse = { id: 2, jsonrpc: '2.0' }; - if (response !== undefined) { - if ('body' in response) { - completeResponse = response.body; - } else { - if (response.error) { - completeResponse.error = response.error; - } else { - completeResponse.result = response.result; - } - if (response.httpStatus) { - httpStatus = response.httpStatus; - } - } - } - /* @ts-expect-error The types for Nock do not include `basePath` in the interface for Nock.Scope. */ - const url = new URL(nockScope.basePath).hostname.match(/(\.|^)infura.io$/u) - ? `/v3/${MOCK_INFURA_PROJECT_ID}` - : '/'; - - debug('Mocking request:', { - url, - method, - params, - response, - error, - ...rest, - times, - }); - - let nockRequest = nockScope.post(url, { - id: /\d*/u, - jsonrpc: '2.0', - method, - params, - ...rest, - }); - - if (delay !== undefined) { - nockRequest = nockRequest.delay(delay); - } - - if (times !== undefined) { - nockRequest = nockRequest.times(times); - } - - if (error !== undefined) { - return nockRequest.replyWithError(error); - } else if (completeResponse !== undefined) { - return nockRequest.reply(httpStatus, (_, requestBody: any) => { - if (response !== undefined && !('body' in response)) { - if (response.id === undefined) { - completeResponse.id = requestBody.id; - } else { - completeResponse.id = response.id; - } - } - debug('Nock returning Response', completeResponse); - return completeResponse; - }); - } - return nockRequest; -} - -type MockBlockTrackerRequestOptions = { - /** - * A nock scope (a set of mocked requests scoped to a certain base url). - */ - nockScope: NockScope; - /** - * The block number that the block tracker should report, as a 0x-prefixed hex - * string. - */ - blockNumber: string; -}; - -/** - * Mocks the next request for the latest block that the block tracker will make. - * - * @param args - The arguments. - * @param args.nockScope - A nock scope (a set of mocked requests scoped to a - * certain base URL). - * @param args.blockNumber - The block number that the block tracker should - * report, as a 0x-prefixed hex string. - */ -function mockNextBlockTrackerRequest({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}: MockBlockTrackerRequestOptions) { - mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }); -} - -/** - * Mocks all requests for the latest block that the block tracker will make. - * - * @param args - The arguments. - * @param args.nockScope - A nock scope (a set of mocked requests scoped to a - * certain base URL). - * @param args.blockNumber - The block number that the block tracker should - * report, as a 0x-prefixed hex string. - */ -async function mockAllBlockTrackerRequests({ - nockScope, - blockNumber = DEFAULT_LATEST_BLOCK_NUMBER, -}: MockBlockTrackerRequestOptions) { - const result = await mockRpcCall({ - nockScope, - request: { method: 'eth_blockNumber', params: [] }, - response: { result: blockNumber }, - }); - - if ('persist' in result) { - result.persist(); - } -} - -/** - * Makes a JSON-RPC call through the given eth-query object. - * - * @param ethQuery - The eth-query object. - * @param request - The request data. - * @returns A promise that either resolves with the result from the JSON-RPC - * response if it is successful or rejects with the error from the JSON-RPC - * response otherwise. - */ -function makeRpcCall(ethQuery: EthQuery, request: Request) { - return new Promise((resolve, reject) => { - debug('[makeRpcCall] making request', request); - ethQuery.sendAsync(request, (error, result) => { - debug('[makeRpcCall > ethQuery handler] error', error, 'result', result); - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); -} - -export type ProviderType = 'infura' | 'custom'; - -export type MockOptions = { - infuraNetwork?: BuiltInInfuraNetwork; - providerType: ProviderType; - customRpcUrl?: string; - customChainId?: Hex; -}; - -export type MockCommunications = { - mockNextBlockTrackerRequest: (options?: any) => void; - mockAllBlockTrackerRequests: (options?: any) => void; - mockRpcCall: (options: CurriedMockRpcCallOptions) => MockRpcCallResult; - rpcUrl: string; - infuraNetwork: BuiltInInfuraNetwork; -}; - -/** - * Sets up request mocks for requests to the provider. - * - * @param options - An options bag. - * @param options.providerType - The type of network client being tested. - * @param options.infuraNetwork - The name of the Infura network being tested, - * assuming that `providerType` is "infura" (default: "mainnet"). - * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming - * that `providerType` is "custom". - * @param fn - A function which will be called with an object that allows - * interaction with the network client. - * @returns The return value of the given function. - */ -export async function withMockedCommunications( - { - providerType, - infuraNetwork = 'mainnet', - customRpcUrl = MOCK_RPC_URL, - }: MockOptions, - fn: (comms: MockCommunications) => Promise, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - const rpcUrl = - providerType === 'infura' - ? `https://${infuraNetwork}.infura.io` - : customRpcUrl; - const nockScope = buildScopeForMockingRequests(rpcUrl); - const curriedMockNextBlockTrackerRequest = (localOptions: any) => - mockNextBlockTrackerRequest({ nockScope, ...localOptions }); - const curriedMockAllBlockTrackerRequests = (localOptions: any) => - mockAllBlockTrackerRequests({ nockScope, ...localOptions }); - const curriedMockRpcCall = (localOptions: any) => - mockRpcCall({ nockScope, ...localOptions }); - - const comms = { - mockNextBlockTrackerRequest: curriedMockNextBlockTrackerRequest, - mockAllBlockTrackerRequests: curriedMockAllBlockTrackerRequests, - mockRpcCall: curriedMockRpcCall, - rpcUrl, - infuraNetwork, - }; - - try { - return await fn(comms); - } finally { - nock.isDone(); - nock.cleanAll(); - } -} - -type MockNetworkClient = { - blockTracker: any; - clock: sinon.SinonFakeTimers; - makeRpcCall: (request: Request) => Promise; - makeRpcCallsInSeries: (requests: Request[]) => Promise; -}; - -/** - * Some middleware contain logic which retries the request if some condition - * applies. This retrying always happens out of band via `setTimeout`, and - * because we are stubbing time via Jest's fake timers, we have to manually - * advance the clock so that the `setTimeout` handlers get fired. We don't know - * when these timers will get created, however, so we have to keep advancing - * timers until the request has been made an appropriate number of times. - * Unfortunately we don't have a good way to know how many times a request has - * been retried, but the good news is that the middleware won't end, and thus - * the promise which the RPC call returns won't get fulfilled, until all retries - * have been made. - * - * @param promise - The promise which is returned by the RPC call. - * @param clock - A Sinon clock object which can be used to advance to the next - * `setTimeout` handler. - */ -export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( - promise: any, - clock: any, -) { - let hasPromiseBeenFulfilled = false; - let numTimesClockHasBeenAdvanced = 0; - - promise - .catch((error: any) => { - // This is used to silence Node.js warnings about the rejection - // being handled asynchronously. The error is handled later when - // `promise` is awaited, but we log it here anyway in case it gets - // swallowed. - debug(error); - }) - .finally(() => { - hasPromiseBeenFulfilled = true; - }); - - // `hasPromiseBeenFulfilled` is modified asynchronously. - /* eslint-disable-next-line no-unmodified-loop-condition */ - while (!hasPromiseBeenFulfilled && numTimesClockHasBeenAdvanced < 15) { - clock.runAll(); - await new Promise((resolve) => originalSetTimeout(resolve, 10)); - numTimesClockHasBeenAdvanced += 1; - } - - return promise; -} - -/** - * Builds a provider from the middleware (for the provider type) along with a - * block tracker, runs the given function with those two things, and then - * ensures the block tracker is stopped at the end. - * - * @param options - An options bag. - * @param options.providerType - The type of network client being tested. - * @param options.infuraNetwork - The name of the Infura network being tested, - * assuming that `providerType` is "infura" (default: "mainnet"). - * @param options.customRpcUrl - The URL of the custom RPC endpoint, assuming - * that `providerType` is "custom". - * @param options.customChainId - The chain id belonging to the custom RPC - * endpoint, assuming that `providerType` is "custom" (default: "0x1"). - * @param fn - A function which will be called with an object that allows - * interaction with the network client. - * @returns The return value of the given function. - */ -export async function withNetworkClient( - { - providerType, - infuraNetwork = 'mainnet', - customRpcUrl = MOCK_RPC_URL, - customChainId = '0x1', - }: MockOptions, - fn: (client: MockNetworkClient) => Promise, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - // Faking timers ends up doing two things: - // 1. Halting the block tracker (which depends on `setTimeout` to periodically - // request the latest block) set up in `eth-json-rpc-middleware` - // 2. Halting the retry logic in `@metamask/eth-json-rpc-infura` (which also - // depends on `setTimeout`) - const clock = sinon.useFakeTimers(); - - // The JSON-RPC client wraps `eth_estimateGas` so that it takes 2 seconds longer - // than it usually would to complete. Or at least it should — this doesn't - // appear to be working correctly. Unset `IN_TEST` on `process.env` to prevent - // this behavior. - const inTest = process.env.IN_TEST; - delete process.env.IN_TEST; - const clientUnderTest = - providerType === 'infura' - ? createNetworkClient({ - network: infuraNetwork, - infuraProjectId: MOCK_INFURA_PROJECT_ID, - type: NetworkClientType.Infura, - }) - : createNetworkClient({ - chainId: customChainId, - rpcUrl: customRpcUrl, - type: NetworkClientType.Custom, - }); - process.env.IN_TEST = inTest; - - const { provider, blockTracker } = clientUnderTest; - - const ethQuery = new EthQuery(provider); - const curriedMakeRpcCall = (request: Request) => - makeRpcCall(ethQuery, request); - const makeRpcCallsInSeries = async (requests: Request[]) => { - const responses = []; - for (const request of requests) { - responses.push(await curriedMakeRpcCall(request)); - } - return responses; - }; - - const client = { - blockTracker, - clock, - makeRpcCall: curriedMakeRpcCall, - makeRpcCallsInSeries, - }; - - try { - return await fn(client); - } finally { - await blockTracker.destroy(); - - clock.restore(); - } -} - -type BuildMockParamsOptions = { - // The block parameter value to set. - blockParam: any; - // The index of the block parameter. - blockParamIndex: number; -}; - -/** - * Build mock parameters for a JSON-RPC call. - * - * The string 'some value' is used as the default value for each entry. The - * block parameter index determines the number of parameters to generate. - * - * The block parameter can be set to a custom value. If no value is given, it - * is set as undefined. - * - * @param args - Arguments. - * @param args.blockParamIndex - The index of the block parameter. - * @param args.blockParam - The block parameter value to set. - * @returns The mock params. - */ -export function buildMockParams({ - blockParam, - blockParamIndex, -}: BuildMockParamsOptions) { - const params = new Array(blockParamIndex).fill('some value'); - params[blockParamIndex] = blockParam; - - return params; -} - -/** - * Returns a partial JSON-RPC request object, with the "block" param replaced - * with the given value. - * - * @param request - The request object. - * @param request.method - The request method. - * @param request.params - The request params. - * @param blockParamIndex - The index within the `params` array of the block - * param. - * @param blockParam - The desired block param value. - * @returns The updated request object. - */ -export function buildRequestWithReplacedBlockParam( - { method, params = [] }: Request, - blockParamIndex: number, - blockParam: any, -) { - const updatedParams = params.slice(); - updatedParams[blockParamIndex] = blockParam; - return { method, params: updatedParams }; -} diff --git a/app/scripts/controllers/network/provider-api-tests/no-block-param.ts b/app/scripts/controllers/network/provider-api-tests/no-block-param.ts deleted file mode 100644 index 662c7fac9..000000000 --- a/app/scripts/controllers/network/provider-api-tests/no-block-param.ts +++ /dev/null @@ -1,977 +0,0 @@ -/* eslint-disable jest/require-top-level-describe, jest/no-export */ - -import { - ProviderType, - waitForPromiseToBeFulfilledAfterRunningAllTimers, - withMockedCommunications, - withNetworkClient, -} from './helpers'; -import { - buildFetchFailedErrorMessage, - buildInfuraClientRetriesExhaustedErrorMessage, - buildJsonRpcEngineEmptyResponseErrorMessage, -} from './shared-tests'; - -type TestsForRpcMethodAssumingNoBlockParamOptions = { - providerType: ProviderType; - numberOfParameters: number; -}; - -/** - * Defines tests which exercise the behavior exhibited by an RPC method which is - * assumed to not take a block parameter. Even if it does, the value of this - * parameter will not be used in determining how to cache the method. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.numberOfParameters - The number of parameters supported by the method under test. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom` (default: "infura"). - */ -export function testsForRpcMethodAssumingNoBlockParam( - method: string, - { - numberOfParameters, - providerType, - }: TestsForRpcMethodAssumingNoBlockParamOptions, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('does not hit the RPC endpoint more than once for identical requests', async () => { - const requests = [{ method }, { method }]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[0]]); - }); - }); - - for (const paramIndex of [...Array(numberOfParameters).keys()]) { - it(`does not reuse the result of a previous request if parameter at index "${paramIndex}" differs`, async () => { - const firstMockParams = [ - ...new Array(numberOfParameters).fill('some value'), - ]; - const secondMockParams = firstMockParams.slice(); - secondMockParams[paramIndex] = 'another value'; - const requests = [ - { - method, - params: firstMockParams, - }, - { method, params: secondMockParams }, - ]; - const mockResults = ['some result', 'another result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual([mockResults[0], mockResults[1]]); - }); - }); - } - - it('hits the RPC endpoint and does not reuse the result of a previous request if the latest block number was updated since', async () => { - const requests = [{ method }, { method }]; - const mockResults = ['first result', 'second result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // Note that we have to mock these requests in a specific order. The - // first block tracker request occurs because of the first RPC request. - // The second block tracker request, however, does not occur because of - // the second RPC request, but rather because we call `clock.runAll()` - // below. - comms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x2' }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const firstResult = await client.makeRpcCall(requests[0]); - // Proceed to the next iteration of the block tracker so that a new - // block is fetched and the current block is updated. - client.clock.runAll(); - const secondResult = await client.makeRpcCall(requests[1]); - return [firstResult, secondResult]; - }, - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - - for (const emptyValue of [null, undefined, '\u003cnil\u003e']) { - it(`does not retry an empty response of "${emptyValue}"`, async () => { - const request = { method }; - const mockResult = emptyValue; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: mockResult }, - }); - - const result = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(result).toStrictEqual(mockResult); - }); - }); - - it(`does not reuse the result of a previous request if it was "${emptyValue}"`, async () => { - const requests = [{ method }, { method }]; - const mockResults = [emptyValue, 'some result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - }); - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - const results = await withNetworkClient( - { providerType }, - ({ makeRpcCallsInSeries }) => makeRpcCallsInSeries(requests), - ); - - expect(results).toStrictEqual(mockResults); - }); - }); - } - - it('queues requests while a previous identical call is still pending, then runs the queue when it finishes, reusing the result from the first request', async () => { - const requests = [{ method }, { method }, { method }]; - const mockResults = ['first result', 'second result', 'third result']; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request: requests[0], - response: { result: mockResults[0] }, - delay: 100, - }); - - comms.mockRpcCall({ - request: requests[1], - response: { result: mockResults[1] }, - }); - - comms.mockRpcCall({ - request: requests[2], - response: { result: mockResults[2] }, - }); - - const results = await withNetworkClient( - { providerType }, - async (client) => { - const resultPromises = [ - client.makeRpcCall(requests[0]), - client.makeRpcCall(requests[1]), - client.makeRpcCall(requests[2]), - ]; - const firstResult = await resultPromises[0]; - // The inflight cache middleware uses setTimeout to run the handlers, - // so run them now - client.clock.runAll(); - const remainingResults = await Promise.all(resultPromises.slice(1)); - return [firstResult, ...remainingResults]; - }, - ); - - expect(results).toStrictEqual([ - mockResults[0], - mockResults[0], - mockResults[0], - ]); - }); - }); - - it('throws a custom error if the request to the RPC endpoint returns a 405 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 405, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'The method does not exist / is not available', - ); - }); - }); - - // There is a difference in how we are testing the Infura middleware vs. the - // custom RPC middleware (or, more specifically, the fetch middleware) because - // of what both middleware treat as rate limiting errors. In this case, the - // fetch middleware treats a 418 response from the RPC endpoint as such an - // error, whereas to the Infura middleware, it is a 429 response. - if (providerType === 'infura') { - it('throws an undescriptive error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { id: 123, method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - '{"id":123,"jsonrpc":"2.0"}', - ); - }); - }); - - it('throws an error with a custom message if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited', - ); - }); - }); - } else { - it('throws a custom error if the request to the RPC endpoint returns a 418 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 418, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - 'Request is being rate limited.', - ); - }); - }); - - it('throws an undescriptive error if the request to the RPC endpoint returns a 429 response', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - httpStatus: 429, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - "Non-200 status code: '429'", - ); - }); - }); - } - - it('throws a generic, undescriptive error if the request to the RPC endpoint returns a response that is not 405, 418, 429, 503, or 504', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - id: 12345, - jsonrpc: '2.0', - error: 'some error', - httpStatus: 420, - }, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - const errorMessage = - providerType === 'infura' - ? '{"id":12345,"jsonrpc":"2.0","error":"some error"}' - : "Non-200 status code: '420'"; - await expect(promiseForResult).rejects.toThrow(errorMessage); - }); - }); - - [503, 504].forEach((httpStatus) => { - it(`retries the request to the RPC endpoint up to 5 times if it returns a ${httpStatus} response, returning the successful result if there is one on the 5th try`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - response: { - error: 'Some error', - httpStatus, - }, - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it(`causes a request to fail with a custom error if the request to the RPC endpoint returns a ${httpStatus} response 5 times in a row`, async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { - error: 'Some error', - httpStatus, - }, - times: 5, - }); - comms.mockNextBlockTrackerRequest(); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - const err = - providerType === 'infura' - ? buildInfuraClientRetriesExhaustedErrorMessage('Gateway timeout') - : buildJsonRpcEngineEmptyResponseErrorMessage(method); - await expect(promiseForResult).rejects.toThrow(err); - }); - }); - }); - - it('retries the request to the RPC endpoint up to 5 times if an "ETIMEDOUT" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'ETIMEDOUT: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - // Both the Infura and fetch middleware detect ETIMEDOUT errors and will - // automatically retry the request to the RPC endpoint in question, but both - // produce a different error if the number of retries is exhausted. - if (providerType === 'infura') { - it('causes a request to fail with a custom error if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('returns an empty response if an "ETIMEDOUT" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ETIMEDOUT: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // The Infura middleware treats a response that contains an ECONNRESET message - // as an innocuous error that is likely to disappear on a retry. The custom - // RPC middleware, on the other hand, does not specially handle this error. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "ECONNRESET" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'ECONNRESET: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "ECONNRESET" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if an "ECONNRESET" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'ECONNRESET: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - } - - // Both the Infura and fetch middleware will attempt to parse the response - // body as JSON, and if this step produces an error, both middleware will also - // attempt to retry the request. However, this error handling code is slightly - // different between the two. As the error in this case is a SyntaxError, the - // Infura middleware will catch it immediately, whereas the custom RPC - // middleware will catch it and re-throw a separate error, which it then - // catches later. - if (providerType === 'infura') { - it('retries the request to the RPC endpoint up to 5 times if an "SyntaxError" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'SyntaxError: Some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('causes a request to fail with a custom error if an "SyntaxError" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildInfuraClientRetriesExhaustedErrorMessage(errorMessage), - ); - }); - }); - - it('does not retry the request to the RPC endpoint, but throws immediately, if a "failed to parse response body" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "SyntaxError" error is thrown while making the request', async () => { - const customRpcUrl = 'http://example.com'; - - await withMockedCommunications( - { providerType, customRpcUrl }, - async (comms) => { - const request = { method }; - const errorMessage = 'SyntaxError: Some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, customRpcUrl }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(customRpcUrl, errorMessage), - ); - }, - ); - }); - - it('retries the request to the RPC endpoint up to 5 times if a "failed to parse response body" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'failed to parse response body: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('returns an empty response if a "failed to parse response body" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'failed to parse response body: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } - - // Only the custom RPC middleware will detect a "Failed to fetch" error and - // attempt to retry the request to the RPC endpoint; the Infura middleware - // does not. - if (providerType === 'infura') { - it('does not retry the request to the RPC endpoint, but throws immediately, if a "Failed to fetch" error is thrown while making the request', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - }); - const promiseForResult = withNetworkClient( - { providerType, infuraNetwork: comms.infuraNetwork }, - async ({ makeRpcCall }) => makeRpcCall(request), - ); - - await expect(promiseForResult).rejects.toThrow( - buildFetchFailedErrorMessage(comms.rpcUrl, errorMessage), - ); - }); - }); - } else { - it('retries the request to the RPC endpoint up to 5 times if a "Failed to fetch" error is thrown while making the request, returning the successful result if there is one on the 5th try', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - // Here we have the request fail for the first 4 tries, then succeed - // on the 5th try. - comms.mockRpcCall({ - request, - error: 'Failed to fetch: some message', - times: 4, - }); - comms.mockRpcCall({ - request, - response: { - result: 'the result', - httpStatus: 200, - }, - }); - - const result = await withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - expect(result).toStrictEqual('the result'); - }); - }); - - it('returns an empty response if a "Failed to fetch" error is thrown while making the request to the RPC endpoint 5 times in a row', async () => { - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - const errorMessage = 'Failed to fetch: some message'; - - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - error: errorMessage, - times: 5, - }); - const promiseForResult = withNetworkClient( - { providerType }, - async ({ makeRpcCall, clock }) => { - return await waitForPromiseToBeFulfilledAfterRunningAllTimers( - makeRpcCall(request), - clock, - ); - }, - ); - - await expect(promiseForResult).rejects.toThrow( - buildJsonRpcEngineEmptyResponseErrorMessage(method), - ); - }); - }); - } -} diff --git a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts b/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts deleted file mode 100644 index ee92bb07f..000000000 --- a/app/scripts/controllers/network/provider-api-tests/not-handled-by-middleware.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable jest/require-top-level-describe, jest/no-export */ - -import { fill } from 'lodash'; -import { - ProviderType, - withMockedCommunications, - withNetworkClient, -} from './helpers'; - -type TestsForRpcMethodNotHandledByMiddlewareOptions = { - providerType: ProviderType; - numberOfParameters: number; -}; - -/** - * Defines tests which exercise the behavior exhibited by an RPC method that - * is not handled specially by the network client middleware. - * - * @param method - The name of the RPC method under test. - * @param additionalArgs - Additional arguments. - * @param additionalArgs.providerType - The type of provider being tested; - * either `infura` or `custom`. - * @param additionalArgs.numberOfParameters - The number of parameters that this - * RPC method takes. - */ -export function testsForRpcMethodNotHandledByMiddleware( - method: string, - { - providerType, - numberOfParameters, - }: TestsForRpcMethodNotHandledByMiddlewareOptions, -) { - if (providerType !== 'infura' && providerType !== 'custom') { - throw new Error( - `providerType must be either "infura" or "custom", was "${providerType}" instead`, - ); - } - - it('attempts to pass the request off to the RPC endpoint', async () => { - const request = { - method, - params: fill(Array(numberOfParameters), 'some value'), - }; - const expectedResult = 'the result'; - - await withMockedCommunications({ providerType }, async (comms) => { - // The first time a block-cacheable request is made, the latest block - // number is retrieved through the block tracker first. It doesn't - // matter what this is — it's just used as a cache key. - comms.mockNextBlockTrackerRequest(); - comms.mockRpcCall({ - request, - response: { result: expectedResult }, - }); - const actualResult = await withNetworkClient( - { providerType }, - ({ makeRpcCall }) => makeRpcCall(request), - ); - - expect(actualResult).toStrictEqual(expectedResult); - }); - }); -} diff --git a/app/scripts/controllers/network/provider-api-tests/shared-tests.ts b/app/scripts/controllers/network/provider-api-tests/shared-tests.ts deleted file mode 100644 index cf34302ce..000000000 --- a/app/scripts/controllers/network/provider-api-tests/shared-tests.ts +++ /dev/null @@ -1,372 +0,0 @@ -/* eslint-disable jest/require-top-level-describe, jest/no-export, jest/no-identical-title */ - -import { testsForRpcMethodsThatCheckForBlockHashInResponse } from './block-hash-in-response'; -import { testsForRpcMethodSupportingBlockParam } from './block-param'; -import { - ProviderType, - withMockedCommunications, - withNetworkClient, -} from './helpers'; -import { testsForRpcMethodAssumingNoBlockParam } from './no-block-param'; -import { testsForRpcMethodNotHandledByMiddleware } from './not-handled-by-middleware'; - -/** - * Constructs an error message that the Infura client would produce in the event - * that it has attempted to retry the request to Infura and has failed. - * - * @param reason - The exact reason for failure. - * @returns The error message. - */ -export function buildInfuraClientRetriesExhaustedErrorMessage(reason: string) { - return new RegExp( - `^InfuraProvider - cannot complete request. All retries exhausted\\..+${reason}`, - 'us', - ); -} - -/** - * Constructs an error message that JsonRpcEngine would produce in the event - * that the response object is empty as it leaves the middleware. - * - * @param method - The RPC method. - * @returns The error message. - */ -export function buildJsonRpcEngineEmptyResponseErrorMessage(method: string) { - return new RegExp( - `^JsonRpcEngine: Response has no error or result for request:.+"method": "${method}"`, - 'us', - ); -} - -/** - * Constructs an error message that `fetch` with throw if it cannot make a - * request. - * - * @param url - The URL being fetched - * @param reason - The reason. - * @returns The error message. - */ -export function buildFetchFailedErrorMessage(url: string, reason: string) { - return new RegExp( - `^request to ${url}(/[^/ ]*)+ failed, reason: ${reason}`, - 'us', - ); -} - -/** - * Defines tests that are common to both the Infura and JSON-RPC network client. - * - * @param providerType - The type of provider being tested, which determines - * which suite of middleware is being tested. If `infura`, then the middleware - * exposed by `createInfuraClient` is tested; if `custom`, then the middleware - * exposed by `createJsonRpcClient` will be tested. - */ -export function testsForProviderType(providerType: ProviderType) { - // Ethereum JSON-RPC spec: - // Infura documentation: - - describe('methods included in the Ethereum JSON-RPC spec', () => { - describe('methods not handled by middleware', () => { - const notHandledByMiddleware = [ - // This list is presented in the same order as in the network client - // tests on the core side. - - { name: 'eth_newFilter', numberOfParameters: 1 }, - { name: 'eth_getFilterChanges', numberOfParameters: 1 }, - { name: 'eth_newBlockFilter', numberOfParameters: 0 }, - { name: 'eth_newPendingTransactionFilter', numberOfParameters: 0 }, - { name: 'eth_uninstallFilter', numberOfParameters: 1 }, - - { name: 'eth_sendRawTransaction', numberOfParameters: 1 }, - { name: 'eth_sendTransaction', numberOfParameters: 1 }, - { name: 'eth_sign', numberOfParameters: 2 }, - - { name: 'eth_createAccessList', numberOfParameters: 2 }, - { name: 'eth_getLogs', numberOfParameters: 1 }, - { name: 'eth_getProof', numberOfParameters: 3 }, - { name: 'eth_getWork', numberOfParameters: 0 }, - { name: 'eth_maxPriorityFeePerGas', numberOfParameters: 0 }, - { name: 'eth_submitHashRate', numberOfParameters: 2 }, - { name: 'eth_submitWork', numberOfParameters: 3 }, - { name: 'eth_syncing', numberOfParameters: 0 }, - { name: 'eth_feeHistory', numberOfParameters: 3 }, - { name: 'debug_getRawHeader', numberOfParameters: 1 }, - { name: 'debug_getRawBlock', numberOfParameters: 1 }, - { name: 'debug_getRawTransaction', numberOfParameters: 1 }, - { name: 'debug_getRawReceipts', numberOfParameters: 1 }, - { name: 'debug_getBadBlocks', numberOfParameters: 0 }, - - { name: 'eth_accounts', numberOfParameters: 0 }, - { name: 'eth_coinbase', numberOfParameters: 0 }, - { name: 'eth_hashrate', numberOfParameters: 0 }, - { name: 'eth_mining', numberOfParameters: 0 }, - - { name: 'eth_signTransaction', numberOfParameters: 1 }, - ]; - notHandledByMiddleware.forEach(({ name, numberOfParameters }) => { - describe(`method name: ${name}`, () => { - testsForRpcMethodNotHandledByMiddleware(name, { - providerType, - numberOfParameters, - }); - }); - }); - }); - - describe('methods with block hashes in their result', () => { - const methodsWithBlockHashInResponse = [ - { name: 'eth_getTransactionByHash', numberOfParameters: 1 }, - { name: 'eth_getTransactionReceipt', numberOfParameters: 1 }, - ]; - methodsWithBlockHashInResponse.forEach(({ name, numberOfParameters }) => { - describe(`method name: ${name}`, () => { - testsForRpcMethodsThatCheckForBlockHashInResponse(name, { - numberOfParameters, - providerType, - }); - }); - }); - }); - - describe('methods that assume there is no block param', () => { - const assumingNoBlockParam = [ - { name: 'eth_getFilterLogs', numberOfParameters: 1 }, - { name: 'eth_blockNumber', numberOfParameters: 0 }, - { name: 'eth_estimateGas', numberOfParameters: 2 }, - { name: 'eth_gasPrice', numberOfParameters: 0 }, - { name: 'eth_getBlockByHash', numberOfParameters: 2 }, - { - name: 'eth_getBlockTransactionCountByHash', - numberOfParameters: 1, - }, - { - name: 'eth_getTransactionByBlockHashAndIndex', - numberOfParameters: 2, - }, - { name: 'eth_getUncleByBlockHashAndIndex', numberOfParameters: 2 }, - { name: 'eth_getUncleCountByBlockHash', numberOfParameters: 1 }, - ]; - const blockParamIgnored = [ - { name: 'eth_getUncleCountByBlockNumber', numberOfParameters: 1 }, - { name: 'eth_getUncleByBlockNumberAndIndex', numberOfParameters: 2 }, - { - name: 'eth_getTransactionByBlockNumberAndIndex', - numberOfParameters: 2, - }, - { - name: 'eth_getBlockTransactionCountByNumber', - numberOfParameters: 1, - }, - ]; - assumingNoBlockParam - .concat(blockParamIgnored) - .forEach(({ name, numberOfParameters }) => - describe(`method name: ${name}`, () => { - testsForRpcMethodAssumingNoBlockParam(name, { - providerType, - numberOfParameters, - }); - }), - ); - }); - - describe('methods that have a param to specify the block', () => { - const supportingBlockParam = [ - { - name: 'eth_call', - blockParamIndex: 1, - numberOfParameters: 2, - }, - { - name: 'eth_getBalance', - blockParamIndex: 1, - numberOfParameters: 2, - }, - { - name: 'eth_getBlockByNumber', - blockParamIndex: 0, - numberOfParameters: 2, - }, - { name: 'eth_getCode', blockParamIndex: 1, numberOfParameters: 2 }, - { - name: 'eth_getStorageAt', - blockParamIndex: 2, - numberOfParameters: 3, - }, - { - name: 'eth_getTransactionCount', - blockParamIndex: 1, - numberOfParameters: 2, - }, - ]; - supportingBlockParam.forEach( - ({ name, blockParamIndex, numberOfParameters }) => { - describe(`method name: ${name}`, () => { - testsForRpcMethodSupportingBlockParam(name, { - providerType, - blockParamIndex, - numberOfParameters, - }); - }); - }, - ); - }); - - describe('other methods', () => { - describe('eth_getTransactionByHash', () => { - it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => { - const method = 'eth_getTransactionByHash'; - - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // This is our request. - comms.mockRpcCall({ - request, - response: { - result: { - blockNumber: '0x200', - }, - }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' }); - - await withNetworkClient( - { providerType }, - async ({ makeRpcCall, blockTracker }) => { - await makeRpcCall(request); - expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300'); - }, - ); - }); - }); - }); - - describe('eth_getTransactionReceipt', () => { - it("refreshes the block tracker's current block if it is less than the block number that comes back in the response", async () => { - const method = 'eth_getTransactionReceipt'; - - await withMockedCommunications({ providerType }, async (comms) => { - const request = { method }; - - comms.mockNextBlockTrackerRequest({ blockNumber: '0x100' }); - // This is our request. - comms.mockRpcCall({ - request, - response: { - result: { - blockNumber: '0x200', - }, - }, - }); - comms.mockNextBlockTrackerRequest({ blockNumber: '0x300' }); - - await withNetworkClient( - { providerType }, - async ({ makeRpcCall, blockTracker }) => { - await makeRpcCall(request); - expect(blockTracker.getCurrentBlock()).toStrictEqual('0x300'); - }, - ); - }); - }); - }); - - describe('eth_chainId', () => { - it('does not hit the RPC endpoint, instead returning the configured chain id', async () => { - const networkId = await withNetworkClient( - { providerType: 'custom', customChainId: '0x1' }, - ({ makeRpcCall }) => { - return makeRpcCall({ method: 'eth_chainId' }); - }, - ); - - expect(networkId).toStrictEqual('0x1'); - }); - }); - }); - }); - - describe('methods not included in the Ethereum JSON-RPC spec', () => { - describe('methods not handled by middleware', () => { - const notHandledByMiddleware = [ - // This list is presented in the same order as in the network client - // tests on the core side. - - { name: 'net_listening', numberOfParameters: 0 }, - { name: 'eth_subscribe', numberOfParameters: 1 }, - { name: 'eth_unsubscribe', numberOfParameters: 1 }, - { name: 'custom_rpc_method', numberOfParameters: 1 }, - { name: 'net_peerCount', numberOfParameters: 0 }, - { name: 'parity_nextNonce', numberOfParameters: 1 }, - ]; - notHandledByMiddleware.forEach(({ name, numberOfParameters }) => { - describe(`method name: ${name}`, () => { - testsForRpcMethodNotHandledByMiddleware(name, { - providerType, - numberOfParameters, - }); - }); - }); - }); - - describe('methods that assume there is no block param', () => { - const assumingNoBlockParam = [ - { name: 'web3_clientVersion', numberOfParameters: 0 }, - { name: 'eth_protocolVersion', numberOfParameters: 0 }, - ]; - assumingNoBlockParam.forEach(({ name, numberOfParameters }) => - describe(`method name: ${name}`, () => { - testsForRpcMethodAssumingNoBlockParam(name, { - providerType, - numberOfParameters, - }); - }), - ); - }); - - describe('other methods', () => { - describe('net_version', () => { - // The Infura middleware includes `net_version` in its scaffold - // middleware, whereas the custom RPC middleware does not. - if (providerType === 'infura') { - it('does not hit Infura, instead returning the network ID that maps to the Infura network, as a decimal string', async () => { - const networkId = await withNetworkClient( - { providerType: 'infura', infuraNetwork: 'goerli' }, - ({ makeRpcCall }) => { - return makeRpcCall({ - method: 'net_version', - }); - }, - ); - expect(networkId).toStrictEqual('5'); - }); - } else { - it('hits the RPC endpoint', async () => { - await withMockedCommunications( - { providerType: 'custom' }, - async (comms) => { - comms.mockRpcCall({ - request: { method: 'net_version' }, - response: { result: '1' }, - }); - - const networkId = await withNetworkClient( - { providerType: 'custom' }, - ({ makeRpcCall }) => { - return makeRpcCall({ - method: 'net_version', - }); - }, - ); - - expect(networkId).toStrictEqual('1'); - }, - ); - }); - } - }); - }); - }); -} diff --git a/app/scripts/controllers/network/test/fake-block-tracker.ts b/app/scripts/controllers/network/test/fake-block-tracker.ts deleted file mode 100644 index 89f8309d1..000000000 --- a/app/scripts/controllers/network/test/fake-block-tracker.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { PollingBlockTracker } from 'eth-block-tracker'; - -/** - * Acts like a PollingBlockTracker, but doesn't start the polling loop or - * make any requests. - */ -export class FakeBlockTracker extends PollingBlockTracker { - async _start() { - // do nothing - } -} diff --git a/app/scripts/controllers/network/test/fake-provider.ts b/app/scripts/controllers/network/test/fake-provider.ts deleted file mode 100644 index a2820112d..000000000 --- a/app/scripts/controllers/network/test/fake-provider.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { inspect, isDeepStrictEqual } from 'util'; -import { - JsonRpcEngine, - JsonRpcRequest, - JsonRpcResponse, -} from 'json-rpc-engine'; -import { SafeEventEmitterProvider } from '@metamask/eth-json-rpc-provider/dist/safe-event-emitter-provider'; - -// Store this in case it gets stubbed later -const originalSetTimeout = global.setTimeout; - -/** - * An object that allows specifying the behavior of a specific invocation of - * `sendAsync`. The `method` always identifies the stub, but the behavior - * may be specified multiple ways: `sendAsync` can either return a promise or - * throw an error, and if it returns a promise, that promise can either be - * resolved with a response object or reject with an error. - * - * @property request - Looks for a request matching these specifications. - * @property request.method - The RPC method to which this stub will be matched. - * @property request.params - The params to which this stub will be matched. - * @property response - Instructs `sendAsync` to return a promise that resolves - * with a response object. - * @property response.result - Specifies a successful response, with this as the - * `result`. - * @property response.error - Specifies an error response, with this as the - * `error`. - * @property error - Instructs `sendAsync` to return a promise that rejects with - * this error. - * @property implementation - Allows overriding `sendAsync` entirely. Useful if - * you want it to throw an error. - * @property delay - The amount of time that will pass after the callback is - * called with the response. - * @property discardAfterMatching - Usually after the stub matches a request, it - * is discarded, but setting this to true prevents that from happening. True by - * default. - * @property beforeCompleting - Sometimes it is useful to do something after the - * request is kicked off but before it ends (or, in terms of a `fetch` promise, - * when the promise is initiated but before it is resolved). You can pass an - * (async) function for this option to do this. - */ -export type FakeProviderStub = { - request: { - method: string; - params?: any[]; - }; - delay?: number; - discardAfterMatching?: boolean; - beforeCompleting?: () => void | Promise; -} & ( - | { - response: { result: any } | { error: string }; - } - | { - error: unknown; - } - | { - implementation: () => void; - } -); - -/** - * The set of options that the FakeProviderEngine constructor takes. - * - * @property stubs - A set of objects that allow specifying the behavior - * of specific invocations of `sendAsync` matching a `method`. - */ -interface FakeProviderEngineOptions { - stubs?: FakeProviderStub[]; -} - -/** - * FakeProviderEngine is an implementation of the provider that - * NetworkController exposes, which is actually an instance of - * Web3ProviderEngine (from the `web3-provider-engine` package). Hence it - * supports the same interface as Web3ProviderEngine, except that fake responses - * for any RPC methods that are accessed can be supplied via an API that is more - * succinct than using Jest's mocking API. - */ -// NOTE: We shouldn't need to extend from the "real" provider here, but -// we'd need a `SafeEventEmitterProvider` _interface_ and that doesn't exist (at -// least not yet). -export class FakeProvider extends SafeEventEmitterProvider { - calledStubs: FakeProviderStub[]; - - #originalStubs: FakeProviderStub[]; - - #stubs: FakeProviderStub[]; - - /** - * Makes a new instance of the fake provider. - * - * @param options - The options. - * @param options.stubs - A set of objects that allow specifying the behavior - * of specific invocations of `sendAsync` matching a `method`. - */ - constructor({ stubs = [] }: FakeProviderEngineOptions) { - super({ engine: new JsonRpcEngine() }); - this.#originalStubs = stubs; - this.#stubs = this.#originalStubs.slice(); - this.calledStubs = []; - } - - send = ( - payload: JsonRpcRequest, - callback: (error: unknown, response?: JsonRpcResponse) => void, - ) => { - return this.#handleSend(payload, callback); - }; - - sendAsync = ( - payload: JsonRpcRequest, - callback: (error: unknown, response?: JsonRpcResponse) => void, - ) => { - return this.#handleSend(payload, callback); - }; - - #handleSend( - payload: JsonRpcRequest, - callback: (error: unknown, response?: JsonRpcResponse) => void, - ) { - if (Array.isArray(payload)) { - throw new Error("Arrays aren't supported"); - } - - const index = this.#stubs.findIndex((stub) => { - return ( - stub.request.method === payload.method && - (!('params' in stub.request) || - isDeepStrictEqual(stub.request.params, payload.params)) - ); - }); - - if (index === -1) { - const matchingCalledStubs = this.calledStubs.filter((stub) => { - return ( - stub.request.method === payload.method && - (!('params' in stub.request) || - isDeepStrictEqual(stub.request.params, payload.params)) - ); - }); - let message = `Could not find any stubs matching: ${inspect(payload, { - depth: null, - })}`; - if (matchingCalledStubs.length > 0) { - message += `\n\nIt appears the following stubs were defined, but have been called already:\n\n${inspect( - matchingCalledStubs, - { depth: null }, - )}`; - } - - throw new Error(message); - } else { - const stub = this.#stubs[index]; - - if (stub.discardAfterMatching !== false) { - this.#stubs.splice(index, 1); - } - - if (stub.delay) { - originalSetTimeout(() => { - this.#handleRequest(stub, callback); - }, stub.delay); - } else { - this.#handleRequest(stub, callback); - } - - this.calledStubs.push({ ...stub }); - } - } - - async #handleRequest( - stub: FakeProviderStub, - callback: (error: unknown, response?: JsonRpcResponse) => void, - ) { - if (stub.beforeCompleting) { - await stub.beforeCompleting(); - } - - if ('implementation' in stub) { - stub.implementation(); - return; - } - - if ('response' in stub) { - if ('result' in stub.response) { - callback(null, { - jsonrpc: '2.0', - id: 1, - result: stub.response.result, - }); - } else if ('error' in stub.response) { - callback(null, { - jsonrpc: '2.0', - id: 1, - error: { - code: -999, - message: stub.response.error, - }, - }); - } - } else if ('error' in stub) { - callback(stub.error); - } - } -} diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 505b696d7..295428813 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -147,9 +147,9 @@ export default class SwapsController { this.indexOfNewestCallInFlight = 0; this.ethersProvider = new Web3Provider(provider); - this._currentNetworkId = networkController.store.getState().networkId; + this._currentNetworkId = networkController.state.networkId; onNetworkStateChange(() => { - const { networkId, networkStatus } = networkController.store.getState(); + const { networkId, networkStatus } = networkController.state; if ( networkStatus === NetworkStatus.Available && networkId !== this._currentNetworkId diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index e3dbaafda..a17d131ca 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -100,11 +100,9 @@ const MOCK_GET_BUFFERED_GAS_LIMIT = async () => ({ function getMockNetworkController() { return { - store: { - getState: sinon.stub().returns({ - networkId: NETWORK_IDS.GOERLI, - networkStatus: NetworkStatus.Available, - }), + state: { + networkId: NETWORK_IDS.GOERLI, + networkStatus: NetworkStatus.Available, }, }; } @@ -208,7 +206,10 @@ describe('SwapsController', function () { it('should replace ethers instance when network changes', function () { const networkController = getMockNetworkController(); - const onNetworkStateChange = sinon.stub(); + let networkStateChangeListener; + const onNetworkStateChange = (listener) => { + networkStateChangeListener = listener; + }; const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, @@ -220,13 +221,12 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - networkController.store.getState.returns({ + networkController.state = { networkId: NETWORK_IDS.MAINNET, networkStatus: NetworkStatus.Available, - }); - changeNetwork(NETWORK_IDS.MAINNET); + }; + networkStateChangeListener(); const newEthersInstance = swapsController.ethersProvider; assert.notStrictEqual( @@ -238,7 +238,10 @@ describe('SwapsController', function () { it('should not replace ethers instance when network changes to loading', function () { const networkController = getMockNetworkController(); - const onNetworkStateChange = sinon.stub(); + let networkStateChangeListener; + const onNetworkStateChange = (listener) => { + networkStateChangeListener = listener; + }; const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, @@ -250,13 +253,12 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - networkController.store.getState.returns({ + networkController.state = { networkId: null, networkStatus: NetworkStatus.Unknown, - }); - changeNetwork('loading'); + }; + networkStateChangeListener(); const newEthersInstance = swapsController.ethersProvider; assert.strictEqual( @@ -268,7 +270,10 @@ describe('SwapsController', function () { it('should not replace ethers instance when network changes to the same network', function () { const networkController = getMockNetworkController(); - const onNetworkStateChange = sinon.stub(); + let networkStateChangeListener; + const onNetworkStateChange = (listener) => { + networkStateChangeListener = listener; + }; const swapsController = new SwapsController({ getBufferedGasLimit: MOCK_GET_BUFFERED_GAS_LIMIT, networkController, @@ -280,13 +285,12 @@ describe('SwapsController', function () { getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; - const changeNetwork = onNetworkStateChange.getCall(0).args[0]; - networkController.store.getState.returns({ + networkController.state = { networkId: NETWORK_IDS.GOERLI, networkStatus: NetworkStatus.Available, - }); - changeNetwork(NETWORK_IDS.GOERLI); + }; + networkStateChangeListener(); const newEthersInstance = swapsController.ethersProvider; assert.strictEqual( diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7b6b049d1..0ccdb519d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -39,6 +39,7 @@ import { } from '@metamask/assets-controllers'; import { PhishingController } from '@metamask/phishing-controller'; import { AnnouncementController } from '@metamask/announcement-controller'; +import { NetworkController } from '@metamask/network-controller'; import { GasFeeController } from '@metamask/gas-fee-controller'; import { PermissionController, @@ -105,6 +106,7 @@ import { import { CHAIN_IDS, NETWORK_TYPES, + TEST_NETWORK_TICKER_MAP, NetworkStatus, } from '../../shared/constants/network'; import { HardwareDeviceNames } from '../../shared/constants/hardware-wallets'; @@ -167,7 +169,6 @@ import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { setupMultiplex } from './lib/stream-utils'; import EnsController from './controllers/ens'; -import { NetworkController } from './controllers/network'; import PreferencesController from './controllers/preferences'; import AppStateController from './controllers/app-state'; import CachedBalancesController from './controllers/cached-balances'; @@ -205,6 +206,7 @@ import { } from './controllers/permissions'; import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware'; import { securityProviderCheck } from './lib/security-provider-helpers'; +import { updateCurrentLocale } from './translate'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) @@ -301,6 +303,7 @@ export default class MetamaskController extends EventEmitter { const networkControllerMessenger = this.controllerMessenger.getRestricted({ name: 'NetworkController', allowedEvents: [ + 'NetworkController:stateChange', 'NetworkController:networkWillChange', 'NetworkController:networkDidChange', 'NetworkController:infuraIsBlocked', @@ -308,9 +311,34 @@ export default class MetamaskController extends EventEmitter { ], }); + let initialProviderConfig; + if (process.env.IN_TEST) { + initialProviderConfig = { + type: NETWORK_TYPES.RPC, + rpcUrl: 'http://localhost:8545', + chainId: '0x539', + nickname: 'Localhost 8545', + ticker: 'ETH', + }; + } else if ( + process.env.METAMASK_DEBUG || + process.env.METAMASK_ENVIRONMENT === 'test' + ) { + initialProviderConfig = { + type: NETWORK_TYPES.GOERLI, + chainId: CHAIN_IDS.GOERLI, + ticker: TEST_NETWORK_TICKER_MAP[NETWORK_TYPES.GOERLI], + }; + } + const initialNetworkControllerState = initialProviderConfig + ? { + providerConfig: initialProviderConfig, + ...initState.NetworkController, + } + : initState.NetworkController; this.networkController = new NetworkController({ messenger: networkControllerMessenger, - state: initState.NetworkController, + state: initialNetworkControllerState, infuraProjectId: opts.infuraProjectId, trackMetaMetricsEvent: (...args) => this.metaMetricsController.trackEvent(...args), @@ -330,13 +358,10 @@ export default class MetamaskController extends EventEmitter { }); this.tokenListController = new TokenListController({ - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, preventPollingOnNetworkRestart: initState.TokenListController ? initState.TokenListController.preventPollingOnNetworkRestart : true, - onNetworkStateChange: this.networkController.store.subscribe.bind( - this.networkController.store, - ), messenger: tokenListMessenger, state: initState.TokenListController, }); @@ -360,25 +385,32 @@ export default class MetamaskController extends EventEmitter { ///: END:ONLY_INCLUDE_IN }); + this.preferencesController.store.subscribe(async ({ currentLocale }) => { + await updateCurrentLocale(currentLocale); + }); + + const tokensControllerMessenger = this.controllerMessenger.getRestricted({ + name: 'TokensController', + allowedActions: ['ApprovalController:addRequest'], + allowedEvents: ['NetworkController:stateChange'], + }); this.tokensController = new TokensController({ - chainId: this.networkController.store.getState().providerConfig.chainId, + messenger: tokensControllerMessenger, + chainId: this.networkController.state.providerConfig.chainId, onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), - onNetworkStateChange: this.networkController.store.subscribe.bind( - this.networkController.store, + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', ), config: { provider: this.provider }, state: initState.TokensController, - messenger: this.controllerMessenger.getRestricted({ - name: 'TokensController', - allowedActions: [`${this.approvalController.name}:addRequest`], - }), }); this.assetsContractController = new AssetsContractController( { - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, onPreferencesStateChange: (listener) => this.preferencesController.store.subscribe(listener), // This handler is misnamed, and is a known issue that will be resolved @@ -393,7 +425,7 @@ export default class MetamaskController extends EventEmitter { networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', () => { - const networkState = this.networkController.store.getState(); + const networkState = this.networkController.state; return cb(networkState); }, ), @@ -411,13 +443,14 @@ export default class MetamaskController extends EventEmitter { this.nftController = new NftController( { messenger: nftControllerMessenger, - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), - onNetworkStateChange: this.networkController.store.subscribe.bind( - this.networkController.store, + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', ), getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( @@ -464,13 +497,14 @@ export default class MetamaskController extends EventEmitter { this.nftController.setApiKey(process.env.OPENSEA_KEY); this.nftDetectionController = new NftDetectionController({ - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, onNftsStateChange: (listener) => this.nftController.subscribe(listener), onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), - onNetworkStateChange: this.networkController.store.subscribe.bind( - this.networkController.store, + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', ), getOpenSeaApiKey: () => this.nftController.openSeaApiKey, getBalancesInSingleCall: @@ -489,12 +523,11 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:networkDidChange', ), getNetworkIdentifier: () => { - const { type, rpcUrl } = - this.networkController.store.getState().providerConfig; + const { type, rpcUrl } = this.networkController.state.providerConfig; return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, version: this.platform.getVersion(), environment: process.env.METAMASK_ENVIRONMENT, extension: this.extension, @@ -526,7 +559,7 @@ export default class MetamaskController extends EventEmitter { onNetworkStateChange: (eventHandler) => { networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', - () => eventHandler(this.networkController.store.getState()), + () => eventHandler(this.networkController.state), ); }, getCurrentNetworkEIP1559Compatibility: @@ -538,12 +571,10 @@ export default class MetamaskController extends EventEmitter { legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, getCurrentNetworkLegacyGasAPICompatibility: () => { - const { chainId } = - this.networkController.store.getState().providerConfig; + const { chainId } = this.networkController.state.providerConfig; return process.env.IN_TEST || chainId === CHAIN_IDS.MAINNET; }, - getChainId: () => - this.networkController.store.getState().providerConfig.chainId, + getChainId: () => this.networkController.state.providerConfig.chainId, }); this.qrHardwareKeyring = new QRHardwareKeyring(); @@ -572,8 +603,7 @@ export default class MetamaskController extends EventEmitter { messenger: currencyRateMessenger, state: { ...initState.CurrencyController, - nativeCurrency: - this.networkController.store.getState().providerConfig.ticker, + nativeCurrency: this.networkController.state.providerConfig.ticker, }, }); @@ -601,7 +631,7 @@ export default class MetamaskController extends EventEmitter { // token exchange rate tracker this.tokenRatesController = new TokenRatesController( { - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, onTokensStateChange: (listener) => this.tokensController.subscribe(listener), onCurrencyRateStateChange: (listener) => @@ -609,8 +639,9 @@ export default class MetamaskController extends EventEmitter { `${this.currencyRateController.name}:stateChange`, listener, ), - onNetworkStateChange: this.networkController.store.subscribe.bind( - this.networkController.store, + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', ), }, { @@ -640,7 +671,7 @@ export default class MetamaskController extends EventEmitter { this.ensController = new EnsController({ provider: this.provider, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, onNetworkDidChange: networkControllerMessenger.subscribe.bind( networkControllerMessenger, 'NetworkController:networkDidChange', @@ -658,7 +689,7 @@ export default class MetamaskController extends EventEmitter { 'NetworkController:networkDidChange', ), getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, preferencesController: this.preferencesController, onboardingController: this.onboardingController, initState: initState.IncomingTransactionsController, @@ -669,10 +700,9 @@ export default class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, getNetworkIdentifier: () => { - const { type, rpcUrl } = - this.networkController.store.getState().providerConfig; + const { type, rpcUrl } = this.networkController.state.providerConfig; return type === NETWORK_TYPES.RPC ? rpcUrl : type; }, preferencesController: this.preferencesController, @@ -709,7 +739,7 @@ export default class MetamaskController extends EventEmitter { this.cachedBalancesController = new CachedBalancesController({ accountTracker: this.accountTracker, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, initState: initState.CachedBalancesController, }); @@ -983,7 +1013,13 @@ export default class MetamaskController extends EventEmitter { }); ///: END:ONLY_INCLUDE_IN + const detectTokensControllerMessenger = + this.controllerMessenger.getRestricted({ + name: 'DetectTokensController', + allowedEvents: ['NetworkController:stateChange'], + }); this.detectTokensController = new DetectTokensController({ + messenger: detectTokensControllerMessenger, preferences: this.preferencesController, tokensController: this.tokensController, assetsContractController: this.assetsContractController, @@ -1034,29 +1070,24 @@ export default class MetamaskController extends EventEmitter { initState: initState.TransactionController || initState.TransactionManager, getPermittedAccounts: this.getPermittedAccounts.bind(this), - getProviderConfig: () => - this.networkController.store.getState().providerConfig, + getProviderConfig: () => this.networkController.state.providerConfig, getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( this.networkController, ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind(this), - getNetworkId: () => this.networkController.store.getState().networkId, - getNetworkStatus: () => - this.networkController.store.getState().networkStatus, + getNetworkId: () => this.networkController.state.networkId, + getNetworkStatus: () => this.networkController.state.networkStatus, onNetworkStateChange: (listener) => { - let previousNetworkId = - this.networkController.store.getState().networkId; - this.networkController.store.subscribe((state) => { - if (previousNetworkId !== state.networkId) { - listener(); - previousNetworkId = state.networkId; - } - }); + networkControllerMessenger.subscribe( + 'NetworkController:stateChange', + () => listener(), + ({ networkId }) => networkId, + ); }, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, preferencesStore: this.preferencesController.store, txHistoryLimit: 60, signTransaction: this.keyringController.signTransaction.bind( @@ -1134,8 +1165,7 @@ export default class MetamaskController extends EventEmitter { const txMeta = this.txController.txStateManager.getTransaction(txId); let rpcPrefs = {}; if (txMeta.chainId) { - const { networkConfigurations } = - this.networkController.store.getState(); + const { networkConfigurations } = this.networkController.state; const matchingNetworkConfig = Object.values( networkConfigurations, ).find( @@ -1216,8 +1246,7 @@ export default class MetamaskController extends EventEmitter { networkControllerMessenger.subscribe( 'NetworkController:networkDidChange', async () => { - const { ticker } = - this.networkController.store.getState().providerConfig; + const { ticker } = this.networkController.state.providerConfig; try { await this.currencyRateController.setNativeCurrency(ticker); } catch (error) { @@ -1263,11 +1292,7 @@ export default class MetamaskController extends EventEmitter { this.signatureController = new SignatureController({ messenger: this.controllerMessenger.getRestricted({ name: 'SignatureController', - allowedActions: [ - `${this.approvalController.name}:addRequest`, - `${this.approvalController.name}:acceptRequest`, - `${this.approvalController.name}:rejectRequest`, - ], + allowedActions: [`${this.approvalController.name}:addRequest`], }), keyringController: this.keyringController, isEthSignEnabled: () => @@ -1276,7 +1301,7 @@ export default class MetamaskController extends EventEmitter { getAllState: this.getState.bind(this), securityProviderRequest: this.securityProviderRequest.bind(this), getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, }); this.signatureController.hub.on( @@ -1300,14 +1325,15 @@ export default class MetamaskController extends EventEmitter { this.txController.txGasUtil, ), networkController: this.networkController, - onNetworkStateChange: (listener) => - this.networkController.store.subscribe(listener), + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', + ), provider: this.provider, - getProviderConfig: () => - this.networkController.store.getState().providerConfig, + getProviderConfig: () => this.networkController.state.providerConfig, getTokenRatesState: () => this.tokenRatesController.state, getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind( this.gasFeeController, @@ -1317,19 +1343,11 @@ export default class MetamaskController extends EventEmitter { ); this.smartTransactionsController = new SmartTransactionsController( { - onNetworkStateChange: (cb) => { - this.networkController.store.subscribe((networkState) => { - const modifiedNetworkState = { - ...networkState, - providerConfig: { - ...networkState.providerConfig, - }, - }; - return cb(modifiedNetworkState); - }); - }, - getNetwork: () => - this.networkController.store.getState().networkId ?? 'loading', + onNetworkStateChange: networkControllerMessenger.subscribe.bind( + networkControllerMessenger, + 'NetworkController:stateChange', + ), + getNetwork: () => this.networkController.state.networkId ?? 'loading', getNonceLock: this.txController.nonceTracker.getNonceLock.bind( this.txController.nonceTracker, ), @@ -1481,7 +1499,7 @@ export default class MetamaskController extends EventEmitter { MetaMetricsController: this.metaMetricsController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, - NetworkController: this.networkController.store, + NetworkController: this.networkController, CachedBalancesController: this.cachedBalancesController.store, AlertController: this.alertController.store, OnboardingController: this.onboardingController.store, @@ -1519,7 +1537,7 @@ export default class MetamaskController extends EventEmitter { this.memStore = new ComposableObservableStore({ config: { AppStateController: this.appStateController.store, - NetworkController: this.networkController.store, + NetworkController: this.networkController, CachedBalancesController: this.cachedBalancesController.store, KeyringController: this.keyringController.memStore, PreferencesController: this.preferencesController.store, @@ -1905,7 +1923,7 @@ export default class MetamaskController extends EventEmitter { updatePublicConfigStore(this.getState()); function updatePublicConfigStore(memState) { - const { chainId } = networkController.store.getState().providerConfig; + const { chainId } = networkController.state.providerConfig; if (memState.networkStatus === NetworkStatus.Available) { publicConfigStore.putState(selectPublicState(chainId, memState)); } @@ -1946,7 +1964,7 @@ export default class MetamaskController extends EventEmitter { getProviderNetworkState(memState) { const { networkId } = memState || this.getState(); return { - chainId: this.networkController.store.getState().providerConfig.chainId, + chainId: this.networkController.state.providerConfig.chainId, networkVersion: networkId ?? 'loading', }; } @@ -2253,27 +2271,6 @@ export default class MetamaskController extends EventEmitter { updatePreviousGasParams: txController.updatePreviousGasParams.bind(txController), - // signatureController - signMessage: this.signatureController.signMessage.bind( - this.signatureController, - ), - cancelMessage: this.signatureController.cancelMessage.bind( - this.signatureController, - ), - signPersonalMessage: this.signatureController.signPersonalMessage.bind( - this.signatureController, - ), - cancelPersonalMessage: - this.signatureController.cancelPersonalMessage.bind( - this.signatureController, - ), - signTypedMessage: this.signatureController.signTypedMessage.bind( - this.signatureController, - ), - cancelTypedMessage: this.signatureController.cancelTypedMessage.bind( - this.signatureController, - ), - // decryptMessageController decryptMessage: this.decryptMessageController.decryptMessage.bind( this.decryptMessageController, @@ -2995,8 +2992,7 @@ export default class MetamaskController extends EventEmitter { this.appStateController.setTrezorModel(model); } - keyring.network = - this.networkController.store.getState().providerConfig.type; + keyring.network = this.networkController.state.providerConfig.type; return keyring; } @@ -3882,12 +3878,12 @@ export default class MetamaskController extends EventEmitter { ), getCurrentChainId: () => - this.networkController.store.getState().providerConfig.chainId, + this.networkController.state.providerConfig.chainId, getCurrentRpcUrl: () => - this.networkController.store.getState().providerConfig.rpcUrl, + this.networkController.state.providerConfig.rpcUrl, // network configuration-related getNetworkConfigurations: () => - this.networkController.store.getState().networkConfigurations, + this.networkController.state.networkConfigurations, upsertNetworkConfiguration: this.networkController.upsertNetworkConfiguration.bind( this.networkController, @@ -4306,7 +4302,7 @@ export default class MetamaskController extends EventEmitter { * @returns {object} rpcInfo found in the network configurations list */ findNetworkConfigurationBy(rpcInfo) { - const { networkConfigurations } = this.networkController.store.getState(); + const { networkConfigurations } = this.networkController.state; const networkConfiguration = Object.values(networkConfigurations).find( (configuration) => { return Object.keys(rpcInfo).some((key) => { @@ -4522,9 +4518,7 @@ export default class MetamaskController extends EventEmitter { if (transactionSecurityCheckEnabled) { const chainId = Number( - hexToDecimal( - this.networkController.store.getState().providerConfig.chainId, - ), + hexToDecimal(this.networkController.state.providerConfig.chainId), ); try { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index c4d64af8f..55f5a29e9 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -6,6 +6,7 @@ import { getEnvironmentType } from '../lib/util'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { TransactionStatus } from '../../../shared/constants/transaction'; import { getURLHostName } from '../../../ui/helpers/utils/util'; +import { t } from '../translate'; export default class ExtensionPlatform { // @@ -181,22 +182,30 @@ export default class ExtensionPlatform { toLower(getURLHostName(url).replace(/([.]\w+)$/u, '')), ); - const title = 'Confirmed transaction'; - const message = `Transaction ${nonce} confirmed! ${ - url.length ? `View on ${view}` : '' - }`; + const title = t('notificationTransactionSuccessTitle'); + let message = t('notificationTransactionSuccessMessage', nonce); + + if (url.length) { + message += ` ${t('notificationTransactionSuccessView', view)}`; + } + await this._showNotification(title, message, url); } async _showFailedTransaction(txMeta, errorMessage) { const nonce = parseInt(txMeta.txParams.nonce, 16); - const title = 'Failed transaction'; - let message = `Transaction ${nonce} failed! ${ - errorMessage || txMeta.err.message - }`; + const title = t('notificationTransactionFailedTitle'); + let message = t( + 'notificationTransactionFailedMessage', + nonce, + errorMessage || txMeta.err.message, + ); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) if (isNaN(nonce)) { - message = `Transaction failed! ${errorMessage || txMeta.err.message}`; + message = t( + 'notificationTransactionFailedMessageMMI', + errorMessage || txMeta.err.message, + ); } ///: END:ONLY_INCLUDE_IN await this._showNotification(title, message); diff --git a/app/scripts/translate.test.ts b/app/scripts/translate.test.ts new file mode 100644 index 000000000..5be2694df --- /dev/null +++ b/app/scripts/translate.test.ts @@ -0,0 +1,125 @@ +import { + getMessage, + fetchLocale, + FALLBACK_LOCALE, +} from '../../shared/modules/i18n'; +import { t, updateCurrentLocale } from './translate'; + +const localeCodeMock = 'te'; +const keyMock = 'testKey'; +const substitutionsMock = ['a1', 'b2']; +const messageMock = 'testMessage'; +const messageMock2 = 'testMessage2'; +const alternateLocaleDataMock = { [keyMock]: { message: messageMock2 } }; + +jest.mock('../../shared/modules/i18n'); +jest.mock('../_locales/en/messages.json', () => ({ + [keyMock]: { message: messageMock }, +})); + +describe('Translate', () => { + const getMessageMock = getMessage as jest.MockedFunction; + const fetchLocaleMock = fetchLocale as jest.MockedFunction< + typeof fetchLocale + >; + + beforeEach(async () => { + jest.resetAllMocks(); + await updateCurrentLocale(FALLBACK_LOCALE); + }); + + describe('updateCurrentLocale', () => { + it('retrieves locale data from shared module', async () => { + await updateCurrentLocale(localeCodeMock); + + expect(fetchLocale).toHaveBeenCalledTimes(1); + expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock); + }); + + it('does not retrieve locale data if same locale already set', async () => { + await updateCurrentLocale(localeCodeMock); + await updateCurrentLocale(localeCodeMock); + + expect(fetchLocale).toHaveBeenCalledTimes(1); + expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock); + }); + + it('does not retrieve locale data if fallback locale set', async () => { + await updateCurrentLocale(localeCodeMock); + await updateCurrentLocale(FALLBACK_LOCALE); + + expect(fetchLocale).toHaveBeenCalledTimes(1); + expect(fetchLocale).toHaveBeenCalledWith(localeCodeMock); + }); + }); + + describe('t', () => { + it('returns value from shared module', () => { + getMessageMock.mockReturnValue(messageMock); + + expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock); + }); + + it('uses en locale by default', () => { + getMessageMock.mockReturnValue(messageMock); + + t(keyMock, ...substitutionsMock); + + expect(getMessage).toHaveBeenCalledTimes(1); + expect(getMessage).toHaveBeenCalledWith( + FALLBACK_LOCALE, + { [keyMock]: { message: messageMock } }, + keyMock, + substitutionsMock, + ); + }); + + it('uses locale passed to updateCurrentLocale if called', async () => { + (getMessage as jest.MockedFunction).mockReturnValue( + messageMock, + ); + + fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock); + await updateCurrentLocale(localeCodeMock); + + t(keyMock, ...substitutionsMock); + + expect(getMessage).toHaveBeenCalledTimes(1); + expect(getMessage).toHaveBeenCalledWith( + localeCodeMock, + alternateLocaleDataMock, + keyMock, + substitutionsMock, + ); + }); + + it('returns value from en locale as fallback if current locale returns null', async () => { + ( + getMessage as jest.MockedFunction + ).mockReturnValueOnce(null); + + ( + getMessage as jest.MockedFunction + ).mockReturnValueOnce(messageMock2); + + fetchLocaleMock.mockResolvedValueOnce(alternateLocaleDataMock); + await updateCurrentLocale(localeCodeMock); + + expect(t(keyMock, ...substitutionsMock)).toStrictEqual(messageMock2); + + expect(getMessage).toHaveBeenCalledTimes(2); + expect(getMessage).toHaveBeenCalledWith( + FALLBACK_LOCALE, + { [keyMock]: { message: messageMock } }, + keyMock, + substitutionsMock, + ); + expect(getMessage).toHaveBeenCalledWith( + localeCodeMock, + alternateLocaleDataMock, + keyMock, + substitutionsMock, + ); + }); + }); +}); diff --git a/app/scripts/translate.ts b/app/scripts/translate.ts new file mode 100644 index 000000000..583c30ef5 --- /dev/null +++ b/app/scripts/translate.ts @@ -0,0 +1,31 @@ +import enTranslations from '../_locales/en/messages.json'; +import { + FALLBACK_LOCALE, + I18NMessageDict, + fetchLocale, + getMessage, +} from '../../shared/modules/i18n'; + +let currentLocale: string = FALLBACK_LOCALE; +let translations: I18NMessageDict = enTranslations; + +export async function updateCurrentLocale(locale: string): Promise { + if (currentLocale === locale) { + return; + } + + if (locale === FALLBACK_LOCALE) { + translations = enTranslations; + } else { + translations = await fetchLocale(locale); + } + + currentLocale = locale; +} + +export function t(key: string, ...substitutions: string[]): string | null { + return ( + getMessage(currentLocale, translations, key, substitutions) || + getMessage(FALLBACK_LOCALE, enTranslations, key, substitutions) + ); +} diff --git a/builds.yml b/builds.yml index 954ddd7f3..3ca68bd59 100644 --- a/builds.yml +++ b/builds.yml @@ -11,6 +11,7 @@ default: &default main # Declaration of build types # Each build type is composed of features, env variables and assets. # Also known as productFlavors in Android lingo +# Note: These build types should be kept in sync with the list in `.github/workflows/update-lavamoat-policies.yml` buildTypes: main: features: diff --git a/development/build/index.js b/development/build/index.js index 1ca565043..33051f5a3 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -81,6 +81,65 @@ async function defineAndRunBuildTasks() { // scuttle on production/tests environment only const shouldScuttle = entryTask !== BUILD_TARGETS.DEV; + let scuttleGlobalThisExceptions = [ + // globals used by different mm deps outside of lm compartment + 'toString', + 'getComputedStyle', + 'addEventListener', + 'removeEventListener', + 'ShadowRoot', + 'HTMLElement', + 'Element', + 'pageXOffset', + 'pageYOffset', + 'visualViewport', + 'Reflect', + 'Set', + 'Object', + 'navigator', + 'harden', + 'console', + 'Image', // Used by browser to generate notifications + // globals chromedriver needs to function + /cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu, + 'performance', + 'parseFloat', + 'innerWidth', + 'innerHeight', + 'Symbol', + 'Math', + 'DOMRect', + 'Number', + 'Array', + 'crypto', + 'Function', + 'Uint8Array', + 'String', + 'Promise', + 'JSON', + 'Date', + // globals sentry needs to function + '__SENTRY__', + 'appState', + 'extra', + 'stateHooks', + 'sentryHooks', + 'sentry', + ]; + + if ( + entryTask === BUILD_TARGETS.TEST || + entryTask === BUILD_TARGETS.TEST_DEV + ) { + scuttleGlobalThisExceptions = [ + ...scuttleGlobalThisExceptions, + // more globals chromedriver needs to function + // in the future, more of the globals above can be put in this list + 'Proxy', + 'ret_nodes', + ]; + } + console.log( `Building lavamoat runtime file`, `(scuttling is ${shouldScuttle ? 'on' : 'off'})`, @@ -89,52 +148,7 @@ async function defineAndRunBuildTasks() { // build lavamoat runtime file await lavapack.buildRuntime({ scuttleGlobalThis: applyLavaMoat && shouldScuttle, - scuttleGlobalThisExceptions: [ - // globals used by different mm deps outside of lm compartment - 'toString', - 'getComputedStyle', - 'addEventListener', - 'removeEventListener', - 'ShadowRoot', - 'HTMLElement', - 'Element', - 'pageXOffset', - 'pageYOffset', - 'visualViewport', - 'Reflect', - 'Set', - 'Object', - 'navigator', - 'harden', - 'console', - 'Image', // Used by browser to generate notifications - // globals chrome driver needs to function (test env) - /cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu, - 'performance', - 'parseFloat', - 'innerWidth', - 'innerHeight', - 'Symbol', - 'Math', - 'DOMRect', - 'Number', - 'Array', - 'crypto', - 'Function', - 'Uint8Array', - 'String', - 'Promise', - 'JSON', - 'Date', - 'Proxy', - // globals sentry needs to function - '__SENTRY__', - 'appState', - 'extra', - 'stateHooks', - 'sentryHooks', - 'sentry', - ], + scuttleGlobalThisExceptions, }); } diff --git a/development/get-next-semver-version.sh b/development/get-next-semver-version.sh new file mode 100755 index 000000000..bdf4b93dc --- /dev/null +++ b/development/get-next-semver-version.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +FORCE_NEXT_SEMVER_VERSION=$1 + +# If FORCE_NEXT_SEMVER_VERSION is defined and not empty, use its value and skip the next operations +if [ -n "$FORCE_NEXT_SEMVER_VERSION" ] +then + echo "NEXT_SEMVER_VERSION=${FORCE_NEXT_SEMVER_VERSION}" >> "$GITHUB_ENV" + exit 0 +fi + +# Get the highest version from release branches +VERSION_BRANCHES=$(git branch -r | grep -o 'Version-v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1) + +# Get the highest version from tags +VERSION_TAGS=$(git tag | grep -o 'v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1) + +# Get the version from package.json +VERSION_PACKAGE=$(node -p "require('./package.json').version") + +# Compare versions and keep the highest one +HIGHEST_VERSION=$(printf "%s\n%s\n%s" "$VERSION_BRANCHES" "$VERSION_TAGS" "$VERSION_PACKAGE" | sort --version-sort | tail -n 1) + +# Increment the minor version of the highest version found +NEXT_VERSION=$(echo "$HIGHEST_VERSION" | awk -F. -v OFS=. '{$2++; print}') + +echo "NEXT_SEMVER_VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV" diff --git a/development/states/navigate-txs.json b/development/states/navigate-txs.json deleted file mode 100644 index 8cd084b85..000000000 --- a/development/states/navigate-txs.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "appState": { - "menuOpen": false, - "modal": { - "open": false, - "modalState": { - "name": null, - "props": {} - }, - "previousModalState": { - "name": null - } - }, - "sidebar": { - "isOpen": false, - "transitionName": "", - "type": "" - }, - "alertOpen": false, - "alertMessage": null, - "qrCodeData": null, - "networkDropdownOpen": false, - "currentView": { - "name": "confTx", - "context": 0 - }, - "accountDetail": {}, - "transForward": false, - "isLoading": false, - "warning": null, - "buyView": {}, - "isMouseUser": true, - "gasIsLoading": false, - "networkNonce": "0x92", - "defaultHdPaths": { - "trezor": "m/44'/60'/0'/0", - "ledger": "m/44'/60'/0'/0/0", - "lattice": "m/44'/60'/0'/0" - } - }, - "confirmTransaction": { - "txData": { - "estimatedGas": "0x38f53", - "gasLimitSpecified": true, - "gasPriceSpecified": false, - "history": [], - "id": 2389644572638774, - "loadingDefaults": false, - "metamaskNetworkId": "4", - "origin": "remix.ethereum.org", - "status": "unapproved", - "time": 1538844223352, - "txParams": { - "data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029", - "from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "gas": "0x38f53", - "gasPrice": "0x3b9aca00", - "value": "0x0" - }, - "type": "standard" - }, - "tokenData": {}, - "methodData": {}, - "tokenProps": { - "tokenDecimals": "", - "tokenSymbol": "" - }, - "fiatTransactionAmount": "0", - "fiatTransactionFee": "0.05", - "fiatTransactionTotal": "0.05", - "ethTransactionAmount": "0", - "ethTransactionFee": "0.000233", - "ethTransactionTotal": "0.000233", - "hexGasTotal": "0xd42f28057e00", - "nonce": "", - "toSmartContract": false, - "fetchingData": false - }, - "localeMessages": {}, - "metamask": { - "isInitialized": true, - "isUnlocked": true, - "isAccountMenuOpen": false, - "isPopup": false, - "rpcTarget": "https://rawtestrpc.metamask.io/", - "identities": { - "0x8cf82b5aa41ff2282427be151dd328568684007a": { - "address": "0x8cf82b5aa41ff2282427be151dd328568684007a", - "name": "Account 3" - }, - "0xbe1a00e10ec68b154adb84e8119167146a71c9a2": { - "address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2", - "name": "Account 2" - }, - "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": { - "address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "name": "Account 1" - } - }, - "unapprovedTxs": { - "2389644572638771": { - "estimatedGas": "0x8544", - "gasLimitSpecified": true, - "gasPriceSpecified": true, - "history": [], - "id": 2389644572638771, - "loadingDefaults": false, - "metamaskNetworkId": "4", - "origin": "MetaMask", - "status": "unapproved", - "time": 1538844175144, - "txParams": { - "data": "0xa9059cbb000000000000000000000000be1a00e10ec68b154adb84e8119167146a71c9a20000000000000000000000000000000000000000000000000000000000000000", - "from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "gas": "0x8544", - "gasPrice": "0x3b9aca00", - "to": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", - "value": "0x0" - }, - "type": "standard" - }, - "2389644572638772": { - "estimatedGas": "0x5208", - "gasLimitSpecified": true, - "gasPriceSpecified": true, - "history": [], - "id": 2389644572638772, - "loadingDefaults": false, - "metamaskNetworkId": "4", - "origin": "MetaMask", - "status": "unapproved", - "time": 1538844178492, - "txParams": { - "from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "gas": "0x5208", - "gasPrice": "0x3b9aca00", - "to": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2", - "value": "0x0" - }, - "type": "standard" - }, - "2389644572638773": { - "estimatedGas": { - "length": 1, - "negative": 0, - "red": null, - "words": [34061, null] - }, - "gasLimitSpecified": false, - "gasPriceSpecified": true, - "history": [], - "id": 2389644572638773, - "loadingDefaults": false, - "metamaskNetworkId": "4", - "origin": "localhost", - "status": "unapproved", - "time": 1538844204724, - "txParams": { - "data": "0xdfb29935000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320746865206970667320686173680000000000000000000000", - "from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "gas": "0xc793", - "gasPrice": "0x3b9aca00", - "to": "0xb7ec370c889b3b48ec537e0b2c887faedceb254a", - "value": "0x0" - }, - "type": "standard" - }, - "2389644572638774": { - "estimatedGas": "0x38f53", - "gasLimitSpecified": true, - "gasPriceSpecified": false, - "history": [], - "id": 2389644572638774, - "loadingDefaults": false, - "metamaskNetworkId": "4", - "origin": "remix.ethereum.org", - "status": "unapproved", - "time": 1538844223352, - "txParams": { - "data": "0x608060405234801561001057600080fd5b506102a7806100206000396000f30060806040526004361061004b5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663d13319c48114610050578063dfb29935146100da575b600080fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f578181015183820152602001610087565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b60008054604080516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f10610196576101008083540402835291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b82800160010185558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675600a165627a7a72305820cf4282c534b8f2faad947d592afa109b907e4e6b2f52335b361b69c24fedb9580029", - "from": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "gas": "0x38f53", - "gasPrice": "0x3b9aca00", - "value": "0x0" - }, - "type": "standard" - } - }, - "noActiveNotices": true, - "frequentRpcList": [], - "addressBook": [], - "selectedTokenAddress": null, - "contractExchangeRates": {}, - "tokenExchangeRates": {}, - "tokens": [ - { - "address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", - "decimals": 9, - "symbol": "DGD" - } - ], - "pendingTokens": {}, - "send": { - "gasLimit": null, - "gasPrice": null, - "gasTotal": null, - "tokenBalance": null, - "from": "", - "to": "", - "amount": "0x0", - "memo": "", - "errors": {}, - "editingTransactionId": null, - "forceGasMin": null - }, - "coinOptions": {}, - "useBlockie": false, - "featureFlags": { - "betaUI": true, - "skipAnnounceBetaUI": true - }, - "isRevealingSeedWords": false, - "welcomeScreenSeen": false, - "currentLocale": "en", - "preferences": { - "useETHAsPrimaryCurrency": true - }, - "providerConfig": { - "type": "goerli" - }, - "network": "4", - "accounts": { - "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": { - "address": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "balance": "0x36aabfb2a0190c00" - }, - "0xbe1a00e10ec68b154adb84e8119167146a71c9a2": { - "address": "0xbe1a00e10ec68b154adb84e8119167146a71c9a2", - "balance": "0x7b3ef08c294a000" - }, - "0x8cf82b5aa41ff2282427be151dd328568684007a": { - "address": "0x8cf82b5aa41ff2282427be151dd328568684007a", - "balance": "0x0" - } - }, - "currentBlockGasLimit": "0x731e25", - "selectedAddressTxList": [], - "unapprovedMsgs": {}, - "unapprovedMsgCount": 0, - "unapprovedPersonalMsgs": {}, - "unapprovedPersonalMsgCount": 0, - "unapprovedTypedMessages": {}, - "unapprovedTypedMessagesCount": 0, - "keyringTypes": [ - "Simple Key Pair", - "HD Key Tree", - "Trezor Hardware", - "Ledger Hardware", - "Lattice Hardware" - ], - "keyrings": [ - { - "type": "HD Key Tree", - "accounts": [ - "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "0xbe1a00e10ec68b154adb84e8119167146a71c9a2", - "0x8cf82b5aa41ff2282427be151dd328568684007a" - ] - } - ], - "currentAccountTab": "history", - "accountTokens": { - "0x8cf82b5aa41ff2282427be151dd328568684007a": {}, - "0xbe1a00e10ec68b154adb84e8119167146a71c9a2": {}, - "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2": { - "goerli": [ - { - "address": "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", - "decimals": 9, - "symbol": "DGD" - }, - { - "address": "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359", - "decimals": 18, - "symbol": "DAI" - } - ] - } - }, - "assetImages": { - "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359": null, - "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a": null - }, - "suggestedTokens": {}, - "lostIdentities": {}, - "seedWords": null, - "forgottenPassword": false, - "selectedAddress": "0xe2f12a09ba1098312a7d1cad7581ed253ca5f4b2", - "recentBlocks": [], - "currentCurrency": "usd", - "conversionRate": 225.23, - "conversionDate": 1538859376, - "shapeShiftTxList": [], - "infuraNetworkStatus": { - "mainnet": "ok", - "goerli": "ok", - "sepolia": "ok", - "lineaGoerli": "ok", - "lineaMainnet": "ok" - } - }, - "send": { - "toDropdownOpen": false, - "errors": {}, - "warnings": {} - } -} diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js index c6942bedc..1bdc4339f 100755 --- a/development/verify-locale-strings.js +++ b/development/verify-locale-strings.js @@ -183,10 +183,13 @@ async function verifyEnglishLocale() { [ 'ui/**/*.js', 'ui/**/*.ts', + 'ui/**/*.tsx', 'shared/**/*.js', 'shared/**/*.ts', + 'shared/**/*.tsx', 'app/scripts/constants/**/*.js', 'app/scripts/constants/**/*.ts', + 'app/scripts/platforms/**/*.js', ], { ignore: [...globsToStrictSearch, testGlob], diff --git a/jest.config.js b/jest.config.js index db7b93bcd..d4cc55266 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,6 @@ module.exports = { collectCoverageFrom: [ '/app/scripts/constants/error-utils.js', - '/app/scripts/controllers/network/**/*.js', - '/app/scripts/controllers/network/**/*.ts', - '!/app/scripts/controllers/network/**/test/*.ts', '/app/scripts/controllers/permissions/**/*.js', '/app/scripts/controllers/sign.ts', '/app/scripts/controllers/decrypt-message.ts', @@ -40,8 +37,6 @@ module.exports = { '/app/scripts/constants/error-utils.test.js', '/app/scripts/controllers/app-state.test.js', '/app/scripts/controllers/mmi-controller.test.js', - '/app/scripts/controllers/network/**/*.test.js', - '/app/scripts/controllers/network/**/*.test.ts', '/app/scripts/controllers/permissions/**/*.test.js', '/app/scripts/controllers/sign.test.ts', '/app/scripts/controllers/decrypt-message.test.ts', @@ -51,6 +46,7 @@ module.exports = { '/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', '/app/scripts/migrations/*.test.(js|ts)', '/app/scripts/platforms/*.test.js', + '/app/scripts/translate.test.ts', '/shared/**/*.test.(js|ts)', '/ui/**/*.test.(js|ts|tsx)', '/development/fitness-functions/**/*.test.(js|ts|tsx)', diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index c43c1c97b..494ce3924 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -905,50 +905,6 @@ "fetch": true } }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, - "@metamask/safe-event-emitter": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -977,12 +933,6 @@ "superstruct": true } }, - "@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/safe-event-emitter": true, - "json-rpc-engine": true - } - }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/browser-passworder": true, @@ -1588,6 +1538,100 @@ "browserify>url": true } }, + "@metamask/network-controller": { + "globals": { + "URL": true, + "btoa": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-middleware": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/swappable-obj-proxy": true, + "@metamask/network-controller>eth-block-tracker": true, + "@metamask/utils": true, + "browserify>assert": true, + "eth-query": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "uuid": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": { + "globals": { + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "node-fetch": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { + "globals": { + "URL": true, + "btoa": true, + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/safe-event-emitter": true, + "browserify>browser-resolve": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "lavamoat>json-stable-stringify": true, + "vinyl>clone": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, + "@metamask/network-controller>eth-block-tracker": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": true, + "@metamask/network-controller>eth-block-tracker>pify": true, + "@metamask/utils": true, + "eth-query>json-rpc-random-id": true + } + }, + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1718,7 +1762,8 @@ "browserify>buffer": true, "browserify>events": true, "eth-rpc-errors": true, - "ethereumjs-util": true + "ethereumjs-util": true, + "lodash": true } }, "@metamask/smart-transactions-controller": { @@ -2971,27 +3016,6 @@ "postMessage": true } }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>@metamask/safe-event-emitter": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true - } - }, - "eth-block-tracker>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" diff --git a/lavamoat/browserify/desktop/policy.json b/lavamoat/browserify/desktop/policy.json index f80fbb328..fafcfcc40 100644 --- a/lavamoat/browserify/desktop/policy.json +++ b/lavamoat/browserify/desktop/policy.json @@ -976,50 +976,6 @@ "define": true } }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, - "@metamask/safe-event-emitter": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1048,12 +1004,6 @@ "superstruct": true } }, - "@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/safe-event-emitter": true, - "json-rpc-engine": true - } - }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/browser-passworder": true, @@ -1659,6 +1609,100 @@ "browserify>url": true } }, + "@metamask/network-controller": { + "globals": { + "URL": true, + "btoa": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-middleware": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/swappable-obj-proxy": true, + "@metamask/network-controller>eth-block-tracker": true, + "@metamask/utils": true, + "browserify>assert": true, + "eth-query": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "uuid": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": { + "globals": { + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "node-fetch": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { + "globals": { + "URL": true, + "btoa": true, + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/safe-event-emitter": true, + "browserify>browser-resolve": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "lavamoat>json-stable-stringify": true, + "vinyl>clone": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, + "@metamask/network-controller>eth-block-tracker": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": true, + "@metamask/network-controller>eth-block-tracker>pify": true, + "@metamask/utils": true, + "eth-query>json-rpc-random-id": true + } + }, + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/notification-controller": { "packages": { "@metamask/base-controller": true, @@ -1908,7 +1952,8 @@ "browserify>buffer": true, "browserify>events": true, "eth-rpc-errors": true, - "ethereumjs-util": true + "ethereumjs-util": true, + "lodash": true } }, "@metamask/smart-transactions-controller": { @@ -3497,27 +3542,6 @@ "postMessage": true } }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>@metamask/safe-event-emitter": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true - } - }, - "eth-block-tracker>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index f80fbb328..fafcfcc40 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -976,50 +976,6 @@ "define": true } }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, - "@metamask/safe-event-emitter": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1048,12 +1004,6 @@ "superstruct": true } }, - "@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/safe-event-emitter": true, - "json-rpc-engine": true - } - }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/browser-passworder": true, @@ -1659,6 +1609,100 @@ "browserify>url": true } }, + "@metamask/network-controller": { + "globals": { + "URL": true, + "btoa": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-middleware": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/swappable-obj-proxy": true, + "@metamask/network-controller>eth-block-tracker": true, + "@metamask/utils": true, + "browserify>assert": true, + "eth-query": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "uuid": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": { + "globals": { + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "node-fetch": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { + "globals": { + "URL": true, + "btoa": true, + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/safe-event-emitter": true, + "browserify>browser-resolve": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "lavamoat>json-stable-stringify": true, + "vinyl>clone": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, + "@metamask/network-controller>eth-block-tracker": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": true, + "@metamask/network-controller>eth-block-tracker>pify": true, + "@metamask/utils": true, + "eth-query>json-rpc-random-id": true + } + }, + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/notification-controller": { "packages": { "@metamask/base-controller": true, @@ -1908,7 +1952,8 @@ "browserify>buffer": true, "browserify>events": true, "eth-rpc-errors": true, - "ethereumjs-util": true + "ethereumjs-util": true, + "lodash": true } }, "@metamask/smart-transactions-controller": { @@ -3497,27 +3542,6 @@ "postMessage": true } }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>@metamask/safe-event-emitter": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true - } - }, - "eth-block-tracker>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index c43c1c97b..494ce3924 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -905,50 +905,6 @@ "fetch": true } }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, - "@metamask/safe-event-emitter": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -977,12 +933,6 @@ "superstruct": true } }, - "@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/safe-event-emitter": true, - "json-rpc-engine": true - } - }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/browser-passworder": true, @@ -1588,6 +1538,100 @@ "browserify>url": true } }, + "@metamask/network-controller": { + "globals": { + "URL": true, + "btoa": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-middleware": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/swappable-obj-proxy": true, + "@metamask/network-controller>eth-block-tracker": true, + "@metamask/utils": true, + "browserify>assert": true, + "eth-query": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "uuid": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": { + "globals": { + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "node-fetch": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { + "globals": { + "URL": true, + "btoa": true, + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/safe-event-emitter": true, + "browserify>browser-resolve": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "lavamoat>json-stable-stringify": true, + "vinyl>clone": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, + "@metamask/network-controller>eth-block-tracker": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": true, + "@metamask/network-controller>eth-block-tracker>pify": true, + "@metamask/utils": true, + "eth-query>json-rpc-random-id": true + } + }, + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1718,7 +1762,8 @@ "browserify>buffer": true, "browserify>events": true, "eth-rpc-errors": true, - "ethereumjs-util": true + "ethereumjs-util": true, + "lodash": true } }, "@metamask/smart-transactions-controller": { @@ -2971,27 +3016,6 @@ "postMessage": true } }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>@metamask/safe-event-emitter": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true - } - }, - "eth-block-tracker>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 9de49f001..6f388ca84 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1126,50 +1126,6 @@ "fetch": true } }, - "@metamask/eth-json-rpc-infura": { - "globals": { - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "node-fetch": true - } - }, - "@metamask/eth-json-rpc-infura>@metamask/utils": { - "globals": { - "TextDecoder": true, - "TextEncoder": true - }, - "packages": { - "browserify>buffer": true, - "nock>debug": true, - "semver": true, - "superstruct": true - } - }, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { - "globals": { - "URL": true, - "btoa": true, - "console.error": true, - "fetch": true, - "setTimeout": true - }, - "packages": { - "@metamask/eth-json-rpc-infura>@metamask/utils": true, - "@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, - "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, - "@metamask/safe-event-emitter": true, - "browserify>browser-resolve": true, - "eth-rpc-errors": true, - "json-rpc-engine": true, - "lavamoat>json-stable-stringify": true, - "vinyl>clone": true - } - }, "@metamask/eth-json-rpc-middleware": { "globals": { "URL": true, @@ -1198,12 +1154,6 @@ "superstruct": true } }, - "@metamask/eth-json-rpc-provider": { - "packages": { - "@metamask/safe-event-emitter": true, - "json-rpc-engine": true - } - }, "@metamask/eth-keyring-controller": { "packages": { "@metamask/browser-passworder": true, @@ -1809,6 +1759,100 @@ "browserify>url": true } }, + "@metamask/network-controller": { + "globals": { + "URL": true, + "btoa": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/controller-utils": true, + "@metamask/eth-json-rpc-middleware": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": true, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": true, + "@metamask/network-controller>@metamask/swappable-obj-proxy": true, + "@metamask/network-controller>eth-block-tracker": true, + "@metamask/utils": true, + "browserify>assert": true, + "eth-query": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "uuid": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura": { + "globals": { + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "node-fetch": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": { + "globals": { + "TextDecoder": true, + "TextEncoder": true + }, + "packages": { + "browserify>buffer": true, + "nock>debug": true, + "semver": true, + "superstruct": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware": { + "globals": { + "URL": true, + "btoa": true, + "console.error": true, + "fetch": true, + "setTimeout": true + }, + "packages": { + "@metamask/eth-trezor-keyring>@metamask/eth-sig-util": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>@metamask/utils": true, + "@metamask/network-controller>@metamask/eth-json-rpc-infura>eth-json-rpc-middleware>pify": true, + "@metamask/safe-event-emitter": true, + "browserify>browser-resolve": true, + "eth-rpc-errors": true, + "json-rpc-engine": true, + "lavamoat>json-stable-stringify": true, + "vinyl>clone": true + } + }, + "@metamask/network-controller>@metamask/eth-json-rpc-provider": { + "packages": { + "@metamask/safe-event-emitter": true, + "json-rpc-engine": true + } + }, + "@metamask/network-controller>eth-block-tracker": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": true, + "@metamask/network-controller>eth-block-tracker>pify": true, + "@metamask/utils": true, + "eth-query>json-rpc-random-id": true + } + }, + "@metamask/network-controller>eth-block-tracker>@metamask/safe-event-emitter": { + "globals": { + "setTimeout": true + }, + "packages": { + "browserify>events": true + } + }, "@metamask/notification-controller>nanoid": { "globals": { "crypto.getRandomValues": true @@ -1939,7 +1983,8 @@ "browserify>buffer": true, "browserify>events": true, "eth-rpc-errors": true, - "ethereumjs-util": true + "ethereumjs-util": true, + "lodash": true } }, "@metamask/smart-transactions-controller": { @@ -3192,27 +3237,6 @@ "postMessage": true } }, - "eth-block-tracker": { - "globals": { - "clearTimeout": true, - "console.error": true, - "setTimeout": true - }, - "packages": { - "@metamask/utils": true, - "eth-block-tracker>@metamask/safe-event-emitter": true, - "eth-block-tracker>pify": true, - "eth-query>json-rpc-random-id": true - } - }, - "eth-block-tracker>@metamask/safe-event-emitter": { - "globals": { - "setTimeout": true - }, - "packages": { - "browserify>events": true - } - }, "eth-ens-namehash": { "globals": { "name": "write" diff --git a/package.json b/package.json index 43e66d32a..fae1202fe 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "lavamoat:webapp:auto": "node ./development/generate-lavamoat-policies.js --devMode=true", "lavamoat:webapp:auto:ci": "node ./development/generate-lavamoat-policies.js --parallel=false", "lavamoat:auto": "yarn lavamoat:build:auto && yarn lavamoat:webapp:auto", - "lavamoat:auto:ci": "yarn lavamoat:build:auto && yarn lavamoat:webapp:auto:ci", "ts-migration:dashboard:build": "ts-node development/ts-migration-dashboard/scripts/build-app.ts", "ts-migration:dashboard:deploy": "gh-pages --dist development/ts-migration-dashboard/build/final --remote ts-migration-dashboard", "ts-migration:dashboard:watch": "yarn ts-migration:dashboard:build --watch", @@ -96,7 +95,8 @@ "fitness-functions": "ts-node development/fitness-functions/index.ts", "generate-beta-commit": "node ./development/generate-beta-commit.js", "validate-branch-name": "validate-branch-name", - "label-prs": "ts-node ./.github/scripts/label-prs.ts" + "label-prs": "ts-node ./.github/scripts/label-prs.ts", + "add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts" }, "resolutions": { "@babel/core": "patch:@babel/core@npm%3A7.21.5#./.yarn/patches/@babel-core-npm-7.21.5-c72c337956.patch", @@ -196,12 +196,9 @@ "fast-json-patch@^3.1.1": "patch:fast-json-patch@npm%3A3.1.1#./.yarn/patches/fast-json-patch-npm-3.1.1-7e8bb70a45.patch", "request@^2.83.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", "request@^2.88.2": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", - "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch", - "@metamask/signature-controller@^3.0.0": "patch:@metamask/signature-controller@npm%3A3.0.0#./.yarn/patches/@metamask-signature-controller-npm-3.0.0-8771b6885e.patch" + "request@^2.85.0": "patch:request@npm%3A2.88.2#./.yarn/patches/request-npm-2.88.2-f4a57c72c4.patch" }, "dependencies": { - "@actions/core": "^1.10.0", - "@actions/github": "^5.1.1", "@babel/runtime": "^7.18.9", "@download/blockies": "^1.0.3", "@ensdomains/content-hash": "^2.5.6", @@ -229,16 +226,14 @@ "@metamask/address-book-controller": "^3.0.0", "@metamask/announcement-controller": "^4.0.0", "@metamask/approval-controller": "^3.1.0", - "@metamask/assets-controllers": "^9.0.0", + "@metamask/assets-controllers": "^9.1.0", "@metamask/base-controller": "^3.0.0", "@metamask/browser-passworder": "^4.1.0", "@metamask/contract-metadata": "^2.3.1", "@metamask/controller-utils": "^4.0.0", "@metamask/design-tokens": "^1.9.0", "@metamask/desktop": "^0.3.0", - "@metamask/eth-json-rpc-infura": "^8.1.0", "@metamask/eth-json-rpc-middleware": "^11.0.0", - "@metamask/eth-json-rpc-provider": "^1.0.0", "@metamask/eth-keyring-controller": "^10.0.1", "@metamask/eth-ledger-bridge-keyring": "^0.15.0", "@metamask/eth-token-tracker": "^4.0.0", @@ -248,8 +243,9 @@ "@metamask/jazzicon": "^2.0.0", "@metamask/key-tree": "^7.0.0", "@metamask/logo": "^3.1.1", - "@metamask/message-manager": "^6.0.0", + "@metamask/message-manager": "^7.0.0", "@metamask/metamask-eth-abis": "^3.0.0", + "@metamask/network-controller": "^10.1.0", "@metamask/notification-controller": "^3.0.0", "@metamask/obs-store": "^8.1.0", "@metamask/permission-controller": "^4.0.0", @@ -261,7 +257,7 @@ "@metamask/rpc-methods-flask": "npm:@metamask/rpc-methods@0.34.0-flask.1", "@metamask/safe-event-emitter": "^2.0.0", "@metamask/scure-bip39": "^2.0.3", - "@metamask/signature-controller": "^3.0.0", + "@metamask/signature-controller": "^4.0.1", "@metamask/slip44": "^3.0.0", "@metamask/smart-transactions-controller": "^3.1.0", "@metamask/snaps-controllers": "^0.32.2", @@ -271,7 +267,6 @@ "@metamask/snaps-utils": "^0.32.2", "@metamask/snaps-utils-flask": "npm:@metamask/snaps-utils@0.34.0-flask.1", "@metamask/subject-metadata-controller": "^2.0.0", - "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/utils": "^5.0.0", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", @@ -297,7 +292,6 @@ "debounce-stream": "^2.0.0", "deep-freeze-strict": "1.1.1", "end-of-stream": "^1.4.4", - "eth-block-tracker": "^7.0.0", "eth-ens-namehash": "^2.0.8", "eth-json-rpc-filters": "^6.0.0", "eth-lattice-keyring": "^0.12.4", @@ -317,7 +311,6 @@ "fuse.js": "^3.2.0", "globalthis": "^1.0.1", "human-standard-token-abi": "^2.0.0", - "husky": "^8.0.3", "immer": "^9.0.6", "is-retry-allowed": "^2.2.0", "jest-junit": "^14.0.1", @@ -366,6 +359,8 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^5.1.1", "@babel/code-frame": "^7.12.13", "@babel/core": "^7.21.5", "@babel/eslint-parser": "^7.13.14", @@ -375,7 +370,6 @@ "@babel/preset-typescript": "^7.16.7", "@babel/register": "^7.5.5", "@ethersproject/bignumber": "^5.7.0", - "@json-rpc-specification/meta-schema": "^1.0.6", "@lavamoat/allow-scripts": "^2.0.3", "@lavamoat/lavapack": "^5.0.0", "@metamask/auto-changelog": "^2.1.0", @@ -418,7 +412,6 @@ "@types/gulp-sass": "^5.0.0", "@types/gulp-sourcemaps": "^0.0.35", "@types/jest": "^29.1.2", - "@types/jest-when": "^3.5.2", "@types/madge": "^5.0.0", "@types/node": "^17.0.21", "@types/pify": "^5.0.1", @@ -426,6 +419,7 @@ "@types/react": "^16.9.53", "@types/react-dom": "^17.0.11", "@types/react-redux": "^7.1.25", + "@types/react-router-dom": "^5.3.3", "@types/remote-redux-devtools": "^0.5.5", "@types/sass": "^1.43.1", "@types/sinon": "^10.0.13", @@ -444,7 +438,7 @@ "browserify": "^16.5.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", - "chromedriver": "^111.0.0", + "chromedriver": "^114.0.0", "concurrently": "^7.6.0", "copy-webpack-plugin": "^6.0.3", "cross-spawn": "^7.0.3", @@ -496,7 +490,6 @@ "jest": "^29.1.2", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^29.1.2", - "jest-when": "^3.5.2", "js-yaml": "^4.1.0", "jsdom": "^11.2.0", "junit-report-merger": "^4.0.0", diff --git a/shared/constants/preferences.ts b/shared/constants/preferences.ts index fab436736..9a37b0281 100644 --- a/shared/constants/preferences.ts +++ b/shared/constants/preferences.ts @@ -3,3 +3,5 @@ export enum ThemeType { dark = 'dark', os = 'os', } + +export const DEFAULT_AUTO_LOCK_TIME_LIMIT = 0; diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index d7a7d284d..731fe93b4 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -3,10 +3,7 @@ import browser from 'webextension-polyfill'; ///: END:ONLY_INCLUDE_IN import { memoize } from 'lodash'; import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred-lang-code'; -import { - fetchLocale, - loadRelativeTimeFormatLocaleData, -} from '../../ui/helpers/utils/i18n-helper'; +import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n'; ///: BEGIN:ONLY_INCLUDE_IN(desktop) import { renderDesktopError } from '../../ui/pages/desktop-error/render-desktop-error'; import { EXTENSION_ERROR_PAGE_TYPES } from '../constants/desktop'; diff --git a/shared/lib/error-utils.test.js b/shared/lib/error-utils.test.js index f761acd10..9874d9b24 100644 --- a/shared/lib/error-utils.test.js +++ b/shared/lib/error-utils.test.js @@ -1,5 +1,5 @@ import browser from 'webextension-polyfill'; -import { fetchLocale } from '../../ui/helpers/utils/i18n-helper'; +import { fetchLocale } from '../modules/i18n'; import { SUPPORT_LINK } from './ui-utils'; import { downloadDesktopApp, @@ -12,7 +12,7 @@ import { } from './error-utils'; import { openCustomProtocol } from './deep-linking'; -jest.mock('../../ui/helpers/utils/i18n-helper', () => ({ +jest.mock('../modules/i18n', () => ({ fetchLocale: jest.fn(), loadRelativeTimeFormatLocaleData: jest.fn(), })); diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts new file mode 100644 index 000000000..3353e4732 --- /dev/null +++ b/shared/modules/i18n.test.ts @@ -0,0 +1,335 @@ +import log from 'loglevel'; +import { + FALLBACK_LOCALE, + I18NMessageDict, + clearCaches, + fetchLocale, + getMessage, + loadRelativeTimeFormatLocaleData, +} from './i18n'; + +const localeCodeMock = 'te'; +const keyMock = 'testKey'; +const errorLocaleMock = 'testLocaleError'; +const errorMock = 'TestError'; + +jest.mock('loglevel'); + +jest.mock('./fetch-with-timeout', () => + jest.fn(() => (url: string) => { + return Promise.resolve({ + json: () => { + if (url.includes(errorLocaleMock)) { + throw new Error(errorMock); + } + + return { url }; + }, + }); + }), +); + +describe('I18N Module', () => { + beforeEach(() => { + jest.resetAllMocks(); + clearCaches(); + process.env.IN_TEST = 'true'; + }); + + describe('getMessage', () => { + describe('on error', () => { + it('returns null if no messages', () => { + expect( + getMessage( + localeCodeMock, + null as unknown as I18NMessageDict, + keyMock, + ), + ).toBeNull(); + }); + + describe('if missing key', () => { + describe('if not using fallback locale', () => { + it('logs warning', () => { + expect( + getMessage( + localeCodeMock, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toBeNull(); + + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn).toHaveBeenCalledWith( + `Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`, + ); + }); + + it('does not log warning if warning already created', () => { + expect( + getMessage( + localeCodeMock, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toBeNull(); + + expect( + getMessage( + localeCodeMock, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toBeNull(); + + expect(log.warn).toHaveBeenCalledTimes(1); + expect(log.warn).toHaveBeenCalledWith( + `Translator - Unable to find value of key "${keyMock}" for locale "${localeCodeMock}"`, + ); + }); + }); + + describe('if using fallback locale', () => { + it('logs error', () => { + delete process.env.IN_TEST; + + expect( + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toBeNull(); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + new Error( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ), + ); + }); + + it('throws if test env set', () => { + expect(() => + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toThrow( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ); + }); + + it('calls onError callback', () => { + const onErrorMock = jest.fn(); + + try { + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + [], + onErrorMock, + ); + } catch { + // Expected + } + + expect(onErrorMock).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledWith( + new Error( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ), + ); + }); + + it('does nothing if error already created', () => { + const onErrorMock = jest.fn(); + + try { + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + [], + onErrorMock, + ); + } catch { + // Expected + } + + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + [], + onErrorMock, + ); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('if missing substitution', () => { + it('logs error', () => { + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1'], + ), + ).toStrictEqual('test1 a1 test2 test3'); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + new Error( + `Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`, + ), + ); + }); + + it('calls onError callback', () => { + const onErrorMock = jest.fn(); + + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1'], + onErrorMock, + ), + ).toStrictEqual('test1 a1 test2 test3'); + + expect(onErrorMock).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledWith( + new Error( + `Insufficient number of substitutions for key "${keyMock}" with locale "${localeCodeMock}"`, + ), + ); + }); + + it('does nothing if error already created', () => { + const onErrorMock = jest.fn(); + + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1'], + onErrorMock, + ), + ).toStrictEqual('test1 a1 test2 test3'); + + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1'], + onErrorMock, + ), + ).toStrictEqual('test1 a1 test2 test3'); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(onErrorMock).toHaveBeenCalledTimes(1); + }); + }); + }); + + it('returns text only if no substitutions', () => { + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'testValue' } }, + keyMock, + ), + ).toStrictEqual('testValue'); + }); + + it('returns text including substitutions', () => { + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1', 'b2'], + ), + ).toStrictEqual('test1 a1 test2 b2 test3'); + }); + + it('returns text including substitutions using custom join', () => { + expect( + getMessage( + localeCodeMock, + { [keyMock]: { message: 'test1 $1 test2 $2 test3' } }, + keyMock, + ['a1', 'b2'], + undefined, + (substitutions) => substitutions.join(','), + ), + ).toStrictEqual('test1 ,a1, test2 ,b2, test3'); + }); + }); + + describe('fetchLocale', () => { + it('returns json from locale file', async () => { + const result = await fetchLocale(localeCodeMock); + expect(result).toStrictEqual({ + url: `./_locales/${localeCodeMock}/messages.json`, + }); + }); + + it('logs if fetch fails', async () => { + await fetchLocale(errorLocaleMock); + + expect(log.error).toHaveBeenCalledTimes(1); + expect(log.error).toHaveBeenCalledWith( + `failed to fetch testLocaleError locale because of Error: ${errorMock}`, + ); + }); + + it('returns empty object if fetch fails', async () => { + expect(await fetchLocale(errorLocaleMock)).toStrictEqual({}); + }); + }); + + describe('loadRelativeTimeFormatLocaleData', () => { + it('adds locale data if function exists', async () => { + const addMock = jest.fn(); + + global.Intl = { + RelativeTimeFormat: { + __addLocaleData: addMock, + }, + } as any; + + await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`); + + expect(addMock).toHaveBeenCalledTimes(1); + expect(addMock).toHaveBeenCalledWith({ + url: `./intl/${localeCodeMock}/relative-time-format-data.json`, + }); + }); + + it('does not add locale data if language tag already processed', async () => { + const addMock = jest.fn(); + + global.Intl = { + RelativeTimeFormat: { + __addLocaleData: addMock, + }, + } as any; + + await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`); + await loadRelativeTimeFormatLocaleData(`${localeCodeMock}_test`); + + expect(addMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts new file mode 100644 index 000000000..601343704 --- /dev/null +++ b/shared/modules/i18n.ts @@ -0,0 +1,223 @@ +import log from 'loglevel'; +import { Json } from '@metamask/utils'; +import getFetchWithTimeout from './fetch-with-timeout'; + +const fetchWithTimeout = getFetchWithTimeout(); + +// From app/_locales folders there is a messages.json file such as app/_locales/en, comes with key and translated results +// and we use as t('reject') to get the translated message in the codebase +// and in i18n lib, the translated message is an object (I18NMessage) with message & description - +// message is the string that will replace the translationKey, and that message may contain replacement variables such as $1, $2, etc. +// Description is key describing the usage of the message. +export interface I18NMessage { + message: string; + description?: string; +} + +// The overall translation file is made of same entries +// translationKey (string) and the I18NMessage as the value. +export interface I18NMessageDict { + [translationKey: string]: I18NMessage; +} + +export type I18NSubstitution = string | (() => any) | object; + +// A parameterized type (or generic type) of maps that use the same structure (translationKey) key +interface I18NMessageDictMap { + [translationKey: string]: R; +} + +export const FALLBACK_LOCALE = 'en'; + +const warned: { [localeCode: string]: I18NMessageDictMap } = {}; + +const missingMessageErrors: I18NMessageDictMap = {}; + +const missingSubstitutionErrors: { + [localeCode: string]: I18NMessageDictMap; +} = {}; + +const relativeTimeFormatLocaleData = new Set(); + +/** + * Returns a localized message for the given key + * + * @param localeCode - The code for the current locale + * @param localeMessages - The map of messages for the current locale + * @param key - The message key + * @param substitutions - A list of message substitution replacements can replace $n in given message + * @param onError - An optional callback to provide additional processing on any errors + * @param join - An optional callback to join the substituted parts using custom logic + * @returns The localized message + */ +export const getMessage = ( + localeCode: string, + localeMessages: I18NMessageDict, + key: string, + substitutions?: I18NSubstitution[], + onError?: (error: Error) => void, + join?: (substitutedParts: I18NSubstitution[]) => T, +): T | string | null => { + if (!localeMessages) { + return null; + } + + const message = localeMessages[key]; + + if (!message) { + missingKeyError(key, localeCode, onError); + return null; + } + + const text = message.message; + + const parts = hasSubstitutions(substitutions) + ? applySubstitutions( + text, + substitutions as I18NSubstitution[], + key, + localeCode, + onError, + ) + : [text]; + + return join ? join(parts) : parts.join(''); +}; + +export async function fetchLocale( + localeCode: string, +): Promise { + try { + const response = await fetchWithTimeout( + `./_locales/${localeCode}/messages.json`, + ); + return await response.json(); + } catch (error) { + log.error(`failed to fetch ${localeCode} locale because of ${error}`); + return {}; + } +} + +export async function loadRelativeTimeFormatLocaleData( + localeCode: string, +): Promise { + const languageTag = localeCode.split('_')[0]; + if ( + Intl.RelativeTimeFormat && + typeof (Intl.RelativeTimeFormat as any).__addLocaleData === 'function' && + !relativeTimeFormatLocaleData.has(languageTag) + ) { + const localeData = await fetchRelativeTimeFormatData(languageTag); + (Intl.RelativeTimeFormat as any).__addLocaleData(localeData); + relativeTimeFormatLocaleData.add(languageTag); + } +} + +export function clearCaches() { + Object.keys(warned).forEach((key) => { + delete warned[key]; + }); + + Object.keys(missingMessageErrors).forEach((key) => { + delete missingMessageErrors[key]; + }); + + Object.keys(missingSubstitutionErrors).forEach((key) => { + delete missingSubstitutionErrors[key]; + }); + + relativeTimeFormatLocaleData.clear(); +} + +function applySubstitutions( + message: string, + substitutions: I18NSubstitution[], + key: string, + localeCode: string, + onError?: (error: Error) => void, +): I18NSubstitution[] { + const parts = message.split(/(\$\d)/gu); + + return parts.map((part: string) => { + const subMatch = part.match(/\$(\d)/u); + + if (!subMatch) { + return part; + } + + const substituteIndex = Number(subMatch[1]) - 1; + const substitution = substitutions[substituteIndex]; + + if (substitution === null || substitution === undefined) { + missingSubstitutionError(key, localeCode, onError); + } + + return substitutions?.[substituteIndex]; + }); +} + +function missingKeyError( + key: string, + localeCode: string, + onError?: (error: Error) => void, +) { + if (localeCode === FALLBACK_LOCALE && !missingMessageErrors[key]) { + const error = new Error( + `Unable to find value of key "${key}" for locale "${localeCode}"`, + ); + + missingMessageErrors[key] = error; + + onError?.(error); + log.error(error); + + if (process.env.IN_TEST) { + throw error; + } + } + + if (localeCode === FALLBACK_LOCALE || warned[localeCode]?.[key]) { + return; + } + + warned[localeCode] = warned[localeCode] ?? {}; + warned[localeCode][key] = true; + + log.warn( + `Translator - Unable to find value of key "${key}" for locale "${localeCode}"`, + ); +} + +function missingSubstitutionError( + key: string, + localeCode: string, + onError?: (error: Error) => void, +) { + if (missingSubstitutionErrors[localeCode]?.[key]) { + return; + } + + missingSubstitutionErrors[localeCode] = + missingSubstitutionErrors[localeCode] ?? {}; + + missingSubstitutionErrors[localeCode][key] = true; + + const error = new Error( + `Insufficient number of substitutions for key "${key}" with locale "${localeCode}"`, + ); + + log.error(error); + + onError?.(error); +} + +function hasSubstitutions(substitutions?: I18NSubstitution[]) { + return (substitutions?.length ?? 0) > 0; +} + +async function fetchRelativeTimeFormatData(languageTag: string): Promise { + const response = await fetchWithTimeout( + `./intl/${languageTag}/relative-time-format-data.json`, + ); + return await response.json(); +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..4b696125f --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +sonar.projectKey=metamask-extension +sonar.organization=consensys + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=MetaMask Extension +#sonar.projectVersion=1.0 + +# Root for sonar analysis. +sonar.sources=app/ + +# Excluded project files from analysis. +#sonar.exclusions= + +# Inclusions for test files. +sonar.test.inclusions=**.test.** + +# Encoding of the source code. Default is default system encoding +sonar.sourceEncoding=UTF-8 \ No newline at end of file diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 51d333062..040ed2166 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -4,6 +4,7 @@ const { promises: fs } = require('fs'); const BigNumber = require('bignumber.js'); const mockttp = require('mockttp'); const createStaticServer = require('../../development/create-static-server'); +const { tEn } = require('../lib/i18n-helpers'); const { setupMocking } = require('./mock-e2e'); const Ganache = require('./ganache'); const FixtureServer = require('./fixture-server'); @@ -384,6 +385,56 @@ const testSRPDropdownIterations = async (options, driver, iterations) => { } }; +const passwordUnlockOpenSRPRevealQuiz = async (driver) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // navigate settings to reveal SRP + await driver.clickElement('[data-testid="account-options-menu-button"]'); + await driver.clickElement({ text: 'Settings', tag: 'div' }); + await driver.clickElement({ text: 'Security & privacy', tag: 'div' }); + await driver.clickElement('[data-testid="reveal-seed-words"]'); +}; + +const completeSRPRevealQuiz = async (driver) => { + // start quiz + await driver.clickElement('[data-testid="srp-quiz-get-started"]'); + + // tap correct answer 1 + await driver.clickElement('[data-testid="srp-quiz-right-answer"]'); + + // tap Continue 1 + await driver.clickElement('[data-testid="srp-quiz-continue"]'); + + // tap correct answer 2 + await driver.clickElement('[data-testid="srp-quiz-right-answer"]'); + + // tap Continue 2 + await driver.clickElement('[data-testid="srp-quiz-continue"]'); +}; + +const tapAndHoldToRevealSRP = async (driver) => { + await driver.holdMouseDownOnElement( + { + text: tEn('holdToRevealSRP'), + tag: 'span', + }, + 2000, + ); +}; + +const closeSRPReveal = async (driver) => { + await driver.clickElement({ + text: tEn('close'), + tag: 'button', + }); + await driver.findVisibleElement({ + text: tEn('tokens'), + tag: 'button', + }); +}; + const DAPP_URL = 'http://127.0.0.1:8080'; const DAPP_ONE_URL = 'http://127.0.0.1:8081'; @@ -500,10 +551,12 @@ const findAnotherAccountFromAccountList = async ( ) => { await driver.clickElement('[data-testid="account-menu-icon"]'); const accountMenuItemSelector = `.multichain-account-list-item:nth-child(${itemNumber})`; - const acctName = await driver.findElement( - `${accountMenuItemSelector} .multichain-account-list-item__account-name__button`, - ); - assert.equal(await acctName.getText(), accountName); + + await driver.findElement({ + css: `${accountMenuItemSelector} .multichain-account-list-item__account-name__button`, + text: accountName, + }); + return accountMenuItemSelector; }; @@ -639,6 +692,10 @@ module.exports = { completeImportSRPOnboardingFlow, completeImportSRPOnboardingFlowWordByWord, completeCreateNewWalletOnboardingFlow, + passwordUnlockOpenSRPRevealQuiz, + completeSRPRevealQuiz, + closeSRPReveal, + tapAndHoldToRevealSRP, createDownloadFolder, importWrongSRPOnboardingFlow, testSRPDropdownIterations, diff --git a/test/e2e/tests/add-account.spec.js b/test/e2e/tests/add-account.spec.js index cf544bb38..93b0696b3 100644 --- a/test/e2e/tests/add-account.spec.js +++ b/test/e2e/tests/add-account.spec.js @@ -8,6 +8,7 @@ const { waitForAccountRendered, convertToHexValue, regularDelayMs, + unlockWallet, } = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); @@ -37,21 +38,19 @@ describe('Add account', function () { }, async ({ driver }) => { await driver.navigate(); - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); + await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( - '[data-testid="multichain-account-menu-add-account"]', + '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); - const accountName = await driver.waitForSelector({ + await driver.findElement({ css: '[data-testid="account-menu-icon"]', - text: '2nd', + text: '2nd account', }); - assert.equal(await accountName.getText(), '2nd account'); }, ); }); @@ -76,28 +75,26 @@ describe('Add account', function () { // Check address of 1st account await waitForAccountRendered(driver); - const firstAccountPublicAddress = await retrieveShortenAccountAddress( - driver, - ); - assert.equal(firstAccountPublicAddress, shortenAddress(firstAccount)); + await driver.findElement({ + css: '.multichain-address-copy-button', + text: shortenAddress(firstAccount), + }); // Create 2nd account await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( - '[data-testid="multichain-account-menu-add-account"]', + '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); await waitForAccountRendered(driver); // Check address of 2nd account - const secondAccountPublicAddress = await retrieveShortenAccountAddress( - driver, - ); - assert.strictEqual( - secondAccountPublicAddress, - shortenAddress(secondAccount), - ); + await waitForAccountRendered(driver); + await driver.findElement({ + css: '.multichain-address-copy-button', + text: shortenAddress(secondAccount), + }); // Log into the account with balance(account 1) // and transfer some balance to 2nd account @@ -143,12 +140,10 @@ describe('Add account', function () { await waitForAccountRendered(driver); // Check address of 1st account - const restoredFirstAccountPublicAddress = - await retrieveShortenAccountAddress(driver); - assert.equal( - restoredFirstAccountPublicAddress, - shortenAddress(firstAccount), - ); + await driver.findElement({ + css: '.multichain-address-copy-button', + text: shortenAddress(firstAccount), + }); // Check address of 2nd account const accountTwoSelector = await findAnotherAccountFromAccountList( @@ -158,17 +153,15 @@ describe('Add account', function () { ); await driver.clickElement(accountTwoSelector); - const restoredSecondAccountPublicAddress = - await retrieveShortenAccountAddress(driver); - assert.equal( - restoredSecondAccountPublicAddress, - shortenAddress(secondAccount), - ); + await driver.findElement({ + css: '.multichain-address-copy-button', + text: shortenAddress(secondAccount), + }); }, ); }); - it('It should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding', async function () { + it('should be possible to remove an account imported with a private key, but should not be possible to remove an account generated from the SRP imported in onboarding', async function () { const testPrivateKey = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'; @@ -180,25 +173,22 @@ describe('Add account', function () { }, async ({ driver }) => { await driver.navigate(); - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); - - await waitForAccountRendered(driver); + await unlockWallet(driver); await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( - '[data-testid="multichain-account-menu-add-account"]', + '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); // Wait for 2nd account to be created await waitForAccountRendered(driver); - const secondAccountCreated = await driver.findElement( - '[data-testid="account-menu-icon"]', - ); - assert.equal(await secondAccountCreated.getText(), '2nd account'); + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: '2nd account', + }); await driver.clickElement('[data-testid="account-menu-icon"]'); @@ -226,10 +216,10 @@ describe('Add account', function () { // Wait for 3rd account to be created await waitForAccountRendered(driver); - const thirdAccountCreated = await driver.findElement( - '[data-testid="account-menu-icon"]', - ); - assert.equal(await thirdAccountCreated.getText(), 'Account 3'); + await driver.findElement({ + css: '[data-testid="account-menu-icon"]', + text: 'Account 3', + }); // User can delete 3rd account imported with a private key await driver.clickElement('[data-testid="account-menu-icon"]'); @@ -245,11 +235,3 @@ describe('Add account', function () { ); }); }); - -async function retrieveShortenAccountAddress(driver) { - // get the shorten public address for account - const accountDOM = await driver.waitForSelector( - '.multichain-address-copy-button', - ); - return await accountDOM.getText(); -} diff --git a/test/e2e/tests/auto-lock.spec.js b/test/e2e/tests/auto-lock.spec.js index 71a25966b..40a1009f2 100644 --- a/test/e2e/tests/auto-lock.spec.js +++ b/test/e2e/tests/auto-lock.spec.js @@ -38,7 +38,7 @@ describe('Auto-Lock Timer', function () { await autoLockTimerInput.fill(10081); await driver.waitForSelector({ css: '#autoTimeout-helper-text', - text: 'Lock time is too great', + text: 'Lock time must be a number between 0 and 10080', }); await autoLockTimerInput.fill(sixSecsInMins); await driver.assertElementNotPresent('#autoTimeout-helper-text'); diff --git a/test/e2e/tests/multiple-transactions.spec.js b/test/e2e/tests/multiple-transactions.spec.js index bc29c3308..d18120575 100644 --- a/test/e2e/tests/multiple-transactions.spec.js +++ b/test/e2e/tests/multiple-transactions.spec.js @@ -1,7 +1,13 @@ -const { convertToHexValue, withFixtures } = require('../helpers'); +const assert = require('assert'); +const { + convertToHexValue, + withFixtures, + openDapp, + regularDelayMs, +} = require('../helpers'); const FixtureBuilder = require('../fixture-builder'); -describe('Confirm transactions', function () { +describe('Multiple transactions', function () { const ganacheOptions = { hardfork: 'london', accounts: [ @@ -12,11 +18,13 @@ describe('Confirm transactions', function () { }, ], }; - it('should be able to confirm multiple transactions', async function () { + + it('creates multiple queued transactions, then confirms', async function () { await withFixtures( { + dapp: true, fixtures: new FixtureBuilder() - .withTransactionControllerMultipleTransactions() + .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions, title: this.test.title, @@ -26,42 +34,58 @@ describe('Confirm transactions', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - // confirm multiple transactions - await driver.waitForSelector({ - text: 'Reject 4 transactions', - tag: 'a', + // initiates a transaction from the dapp + await openDapp(driver); + // creates first transaction + await driver.clickElement({ + text: 'Send EIP 1559 Transaction', + tag: 'button', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitForSelector({ - text: 'Reject 3 transactions', - tag: 'a', + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extensionTab = windowHandles[0]; + const dApp = windowHandles[1]; + const confirmation = windowHandles[2]; + + await driver.switchToWindow(dApp); + + // creates second transaction + await driver.clickElement({ + text: 'Send EIP 1559 Transaction', + tag: 'button', }); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.switchToWindow(confirmation); + + // confirms second transaction await driver.waitForSelector({ text: 'Reject 2 transactions', tag: 'a', }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.waitForElementNotPresent('.loading-overlay__spinner'); + // confirms first transaction await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extensionTab); + await driver.delay(regularDelayMs); await driver.clickElement('[data-testid="home__activity-tab"]'); - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 4; - }, 10000); + + const confirmedTxes = await driver.findElements( + '.transaction-list__completed-transactions .transaction-list-item', + ); + + assert.equal(confirmedTxes.length, 2); }, ); }); - it('should be able to reject multiple transactions', async function () { + it('creates multiple queued transactions, then rejects', async function () { await withFixtures( { + dapp: true, fixtures: new FixtureBuilder() - .withTransactionControllerMultipleTransactions() + .withPermissionControllerConnectedToTestDapp() .build(), ganacheOptions, title: this.test.title, @@ -71,27 +95,50 @@ describe('Confirm transactions', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - // confirm multiple transactions - await driver.waitForSelector({ - text: 'Reject 4 transactions', - tag: 'a', + // initiates a transaction from the dapp + await openDapp(driver); + // creates first transaction + await driver.clickElement({ + text: 'Send EIP 1559 Transaction', + tag: 'button', }); - await driver.clickElement({ text: 'Reject', tag: 'button' }); - await driver.waitForSelector({ - text: 'Reject 3 transactions', - tag: 'a', + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + const confirmation = windowHandles[2]; + await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); + + // creates second transaction + await driver.clickElement({ + text: 'Send EIP 1559 Transaction', + tag: 'button', }); - await driver.clickElement({ text: 'Reject', tag: 'button' }); + await driver.switchToWindow(confirmation); + + // rejects second transaction await driver.waitForSelector({ text: 'Reject 2 transactions', tag: 'a', }); await driver.clickElement({ text: 'Reject', tag: 'button' }); - await driver.waitForElementNotPresent('.loading-overlay__spinner'); + // rejects first transaction await driver.clickElement({ text: 'Reject', tag: 'button' }); - await driver.waitForSelector('[data-testid="home__activity-tab"]'); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + await driver.delay(regularDelayMs); + await driver.clickElement('[data-testid="home__activity-tab"]'); + + const isTransactionListEmpty = await driver.isElementPresentAndVisible( + '.transaction-list__empty-text', + ); + assert.equal(isTransactionListEmpty, true); + + // should not be present + await driver.assertElementNotPresent( + '.transaction-list__completed-transactions .transaction-list-item', + ); }, ); }); diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index 81a330085..64bdaace0 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -412,8 +412,7 @@ describe('Send ETH from inside MetaMask to a Multisig Address', function () { smartContract, ); await driver.navigate(); - await driver.fill('#password', 'correct horse battery staple'); - await driver.press('#password', driver.Key.ENTER); + await logInWithBalanceValidation(driver, ganacheServer); await driver.clickElement('[data-testid="eth-overview-send"]'); @@ -426,10 +425,7 @@ describe('Send ETH from inside MetaMask to a Multisig Address', function () { await inputAmount.fill('1'); // Continue to next screen - await driver.findClickableElement({ text: 'Next', tag: 'button' }); await driver.clickElement({ text: 'Next', tag: 'button' }); - - await driver.findClickableElement({ text: 'Confirm', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); // Go back to home screen to check txn diff --git a/test/e2e/tests/settings-security-reveal-srp.spec.js b/test/e2e/tests/settings-security-reveal-srp.spec.js new file mode 100644 index 000000000..1a7cc14c0 --- /dev/null +++ b/test/e2e/tests/settings-security-reveal-srp.spec.js @@ -0,0 +1,135 @@ +const { strict: assert } = require('assert'); +const { + withFixtures, + passwordUnlockOpenSRPRevealQuiz, + completeSRPRevealQuiz, + tapAndHoldToRevealSRP, + closeSRPReveal, +} = require('../helpers'); +const FixtureBuilder = require('../fixture-builder'); +const { tEn } = require('../../lib/i18n-helpers'); + +describe('Reveal SRP through settings', function () { + const testPassword = 'correct horse battery staple'; + const wrongTestPassword = 'test test test test'; + const seedPhraseWords = + 'spread raise short crane omit tent fringe mandate neglect detail suspect cradle'; + + it('should not reveal SRP text with incorrect password', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test.title, + failOnConsoleError: false, + }, + async ({ driver }) => { + await passwordUnlockOpenSRPRevealQuiz(driver); + await completeSRPRevealQuiz(driver); + await driver.fill('#password-box', wrongTestPassword); + await driver.press('#password-box', driver.Key.ENTER); + await driver.isElementPresent( + { + css: '.mm-help-text', + text: 'Incorrect password', + }, + true, + ); + }, + ); + }); + + it('completes quiz and reveals SRP text', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test.title, + }, + async ({ driver }) => { + await passwordUnlockOpenSRPRevealQuiz(driver); + await completeSRPRevealQuiz(driver); + + // enter password + await driver.fill('#password-box', testPassword); + await driver.press('#password-box', driver.Key.ENTER); + + await tapAndHoldToRevealSRP(driver); + + // confirm SRP text matches expected + const displayedSRP = await driver.findVisibleElement( + '[data-testid="srp_text"]', + ); + assert.equal(await displayedSRP.getText(), seedPhraseWords); + + // copy SRP text to clipboard + await driver.clickElement({ + text: tEn('copyToClipboard'), + tag: 'button', + }); + await driver.findVisibleElement({ + text: tEn('copiedExclamation'), + tag: 'button', + }); + + // confirm that CTA returns user to wallet view + await closeSRPReveal(driver); + }, + ); + }); + + it('completes quiz and reveals SRP QR after wrong answers', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test.title, + }, + async ({ driver }) => { + await passwordUnlockOpenSRPRevealQuiz(driver); + + // start quiz + await driver.clickElement('[data-testid="srp-quiz-get-started"]'); + + // tap incorrect answer 1 + await driver.clickElement('[data-testid="srp-quiz-wrong-answer"]'); + + // try again + await driver.clickElement('[data-testid="srp-quiz-try-again"]'); + + // tap correct answer 1 + await driver.clickElement('[data-testid="srp-quiz-right-answer"]'); + + // tap Continue 1 + await driver.clickElement('[data-testid="srp-quiz-continue"]'); + + // tap incorrect answer 2 + await driver.clickElement('[data-testid="srp-quiz-wrong-answer"]'); + + // try again + await driver.clickElement('[data-testid="srp-quiz-try-again"]'); + + // tap correct answer 1 + await driver.clickElement('[data-testid="srp-quiz-right-answer"]'); + + // tap Continue 2 + await driver.clickElement('[data-testid="srp-quiz-continue"]'); + + // enter password + await driver.fill('#password-box', testPassword); + await driver.press('#password-box', driver.Key.ENTER); + + // tap and hold to reveal + await tapAndHoldToRevealSRP(driver); + + // confirm SRP QR is displayed + await driver.clickElement({ + text: 'QR', + tag: 'button', + }); + const qrCode = await driver.findElement('[data-testid="qr-srp"]'); + assert.equal(await qrCode.isDisplayed(), true); + + // confirm that CTA returns user to wallet view + await closeSRPReveal(driver); + }, + ); + }); +}); diff --git a/test/e2e/user-actions-benchmark.js b/test/e2e/user-actions-benchmark.js index 00a53c8d0..07843ecdf 100644 --- a/test/e2e/user-actions-benchmark.js +++ b/test/e2e/user-actions-benchmark.js @@ -36,7 +36,7 @@ async function loadNewAccount() { await driver.clickElement('[data-testid="account-menu-icon"]'); const timestampBeforeAction = new Date(); await driver.clickElement( - '[data-testid="multichain-account-menu-add-account"]', + '[data-testid="multichain-account-menu-popover-add-account"]', ); await driver.fill('[placeholder="Account 2"]', '2nd account'); await driver.clickElement({ text: 'Create', tag: 'button' }); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 243337c59..2c222ae95 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -261,6 +261,18 @@ class Driver { .perform(); } + async holdMouseDownOnElement(rawLocator, ms) { + const locator = this.buildLocator(rawLocator); + const element = await this.findClickableElement(locator); + await this.driver + .actions() + .move({ origin: element, x: 1, y: 1 }) + .press() + .pause(ms) + .release() + .perform(); + } + async scrollToElement(element) { await this.driver.executeScript( 'arguments[0].scrollIntoView(true)', diff --git a/test/lib/i18n-helpers.js b/test/lib/i18n-helpers.js new file mode 100644 index 000000000..2c73a5cce --- /dev/null +++ b/test/lib/i18n-helpers.js @@ -0,0 +1,6 @@ +import { getMessage } from '../../ui/helpers/utils/i18n-helper'; +import * as en from '../../app/_locales/en/messages.json'; + +export function tEn(key) { + return getMessage('en', en, key); +} diff --git a/tsconfig.json b/tsconfig.json index 0418262ab..aaea30b2c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "outDir": "tsout", "rootDir": ".", "sourceMap": true, - "strict": true + "strict": true, + "resolveJsonModule": true }, "exclude": [ "**/jest-coverage/**/*", diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js index c4a57667d..033145d8a 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js @@ -6,8 +6,8 @@ import { EditGasModes } from '../../../../../shared/constants/gas'; import Box from '../../../ui/box'; import CheckBox from '../../../ui/check-box'; import { - DISPLAY, - FLEX_DIRECTION, + Display, + FlexDirection, TextColor, TextVariant, } from '../../../../helpers/constants/design-system'; @@ -72,8 +72,8 @@ const AdvancedGasFeeDefaults = () => { return ( { variant={TextVariant.bodySm} as="h6" color={TextColor.textAlternative} - margin={0} > {isDefaultSettingsSelected ? t('advancedGasFeeDefaultOptOut') diff --git a/ui/components/app/cancel-button/cancel-buitton.stories.js b/ui/components/app/cancel-button/cancel-buitton.stories.js new file mode 100644 index 000000000..dad8cd782 --- /dev/null +++ b/ui/components/app/cancel-button/cancel-buitton.stories.js @@ -0,0 +1,29 @@ +import React from 'react'; +import CancelButton from './cancel-button'; + +export default { + title: 'Components/App/CancelButton', + component: CancelButton, + argTypes: { + transaction: { + control: 'object', + }, + cancelTransaction: { + control: 'cancelTransaction', + }, + detailsModal: { + control: 'boolean', + }, + }, + args: { + detailsModal: true, + transaction: { + id: '12345', + status: 'pending', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js index b12443ccd..832e8a770 100644 --- a/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js +++ b/ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js @@ -89,7 +89,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) { setHasTriggeredUnlock(true); preventPropogation(e); }, - [onLongPressed], + [onLongPressed, trackEvent], ); /** diff --git a/ui/components/app/modals/contract-details-modal/contract-details-modal.js b/ui/components/app/modals/contract-details-modal/contract-details-modal.js index e6250a280..db14cb8bc 100644 --- a/ui/components/app/modals/contract-details-modal/contract-details-modal.js +++ b/ui/components/app/modals/contract-details-modal/contract-details-modal.js @@ -12,7 +12,7 @@ import Popover from '../../../ui/popover'; import { FontWeight, TextVariant, - DISPLAY, + Display, Size, BorderStyle, BorderColor, @@ -65,17 +65,16 @@ export default function ContractDetailsModal({ fontWeight={FontWeight.Bold} variant={TextVariant.bodyMd} as="h5" - display={DISPLAY.FLEX} - boxProps={{ marginTop: 0, marginBottom: 0 }} + display={Display.Flex} > {t('contractTitle')} {t('contractDescription')} @@ -84,14 +83,14 @@ export default function ContractDetailsModal({ {nft ? t('contractNFT') : t('contractToken')} {ellipsify(tokenAddress)} @@ -148,7 +146,7 @@ export default function ContractDetailsModal({ } > { @@ -189,7 +187,7 @@ export default function ContractDetailsModal({ @@ -200,7 +198,7 @@ export default function ContractDetailsModal({ t('contractRequestingSpendingCap')} {ellipsify(toAddress)} @@ -246,7 +243,7 @@ export default function ContractDetailsModal({ } > { @@ -284,7 +281,7 @@ export default function ContractDetailsModal({ ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/network-account-balance-header/network-account-balance-header.js b/ui/components/app/network-account-balance-header/network-account-balance-header.js index 79953f3d6..98c91acfe 100644 --- a/ui/components/app/network-account-balance-header/network-account-balance-header.js +++ b/ui/components/app/network-account-balance-header/network-account-balance-header.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import IconWithFallback from '../../ui/icon-with-fallback'; import Identicon from '../../ui/identicon'; import { - DISPLAY, - FLEX_DIRECTION, + Display, + FlexDirection, TextVariant, FontWeight, AlignItems, @@ -33,22 +33,22 @@ export default function NetworkAccountBalanceHeader({ return ( @@ -60,15 +60,14 @@ export default function NetworkAccountBalanceHeader({ /> {networkName} @@ -78,22 +77,20 @@ export default function NetworkAccountBalanceHeader({ as="h6" color={TextColor.textDefault} fontWeight={FontWeight.Bold} - marginTop={0} > {accountName} {t('balance')} @@ -103,7 +100,6 @@ export default function NetworkAccountBalanceHeader({ as="h6" color={TextColor.textDefault} fontWeight={FontWeight.Bold} - marginTop={0} align={TextAlign.End} > {accountBalance} {tokenName} diff --git a/ui/components/app/nft-options/nft-options.stories.js b/ui/components/app/nft-options/nft-options.stories.js new file mode 100644 index 000000000..9b31de436 --- /dev/null +++ b/ui/components/app/nft-options/nft-options.stories.js @@ -0,0 +1,19 @@ +import React from 'react'; +import NftOptions from './nft-options'; + +export default { + title: 'Components/App/NftOptions', + component: NftOptions, + argTypes: { + onRemove: { + action: 'onRemove', + }, + onViewOnOpensea: { + action: 'onViewOnOpensea', + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js index c3875c4e8..40d577380 100644 --- a/ui/components/app/qr-hardware-popover/qr-hardware-popover.js +++ b/ui/components/app/qr-hardware-popover/qr-hardware-popover.js @@ -1,5 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { ethErrors, serializeError } from 'eth-rpc-errors'; import { getCurrentQRHardwareState } from '../../../selectors'; import Popover from '../../ui/popover'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -7,11 +8,8 @@ import { cancelSyncQRHardware as cancelSyncQRHardwareAction, cancelQRHardwareSignRequest as cancelQRHardwareSignRequestAction, cancelTx, - cancelPersonalMsg, - cancelMsg, - cancelTypedMsg, + rejectPendingApproval, } from '../../../store/actions'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import QRHardwareWalletImporter from './qr-hardware-wallet-importer'; import QRHardwareSignRequest from './qr-hardware-sign-request'; @@ -43,25 +41,13 @@ const QRHardwarePopover = () => { ); const signRequestCancel = useCallback(() => { - let action = cancelTx; - switch (_txData.type) { - case MESSAGE_TYPE.PERSONAL_SIGN: { - action = cancelPersonalMsg; - break; - } - case MESSAGE_TYPE.ETH_SIGN: { - action = cancelMsg; - break; - } - case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: { - action = cancelTypedMsg; - break; - } - default: { - action = cancelTx; - } - } - dispatch(action(_txData)); + dispatch( + rejectPendingApproval( + _txData.id, + serializeError(ethErrors.provider.userRejectedRequest()), + ), + ); + dispatch(cancelTx(_txData)); dispatch(cancelQRHardwareSignRequestAction()); }, [dispatch, _txData]); diff --git a/ui/components/app/signature-request-original/signature-request-original-warning/signature-request-original-warning.js b/ui/components/app/signature-request-original/signature-request-original-warning/signature-request-original-warning.js index 6de7757e9..13ac14b76 100644 --- a/ui/components/app/signature-request-original/signature-request-original-warning/signature-request-original-warning.js +++ b/ui/components/app/signature-request-original/signature-request-original-warning/signature-request-original-warning.js @@ -96,6 +96,7 @@ const SignatureRequestOriginalWarning = ({ + ))} + + ); +} diff --git a/ui/components/app/srp-quiz-modal/QuizContent/index.ts b/ui/components/app/srp-quiz-modal/QuizContent/index.ts new file mode 100644 index 000000000..537995246 --- /dev/null +++ b/ui/components/app/srp-quiz-modal/QuizContent/index.ts @@ -0,0 +1 @@ +export { default } from './QuizContent'; diff --git a/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.stories.tsx b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.stories.tsx new file mode 100644 index 000000000..495c4e14a --- /dev/null +++ b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.stories.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; +import { useArgs } from '@storybook/client-api'; +import { Button } from '../../../component-library'; +import SRPQuiz from '.'; + +export default { + title: 'Components/App/SRPQuizModal', + component: SRPQuiz, + argTypes: { + isShowingModal: { + control: 'boolean', + }, + }, +} as Meta; + +export const DefaultStory: StoryFn = () => { + const [{ isShowingModal }, updateArgs] = useArgs(); + + return ( + <> + + {isShowingModal && ( + updateArgs({ isShowingModal: false })} + /> + )} + + ); +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.test.js b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.test.js new file mode 100644 index 000000000..d1e7ae395 --- /dev/null +++ b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.test.js @@ -0,0 +1,95 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import React from 'react'; +import mockState from '../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../test/jest'; +import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; +import configureStore from '../../../../store/store'; +import { QuizStage } from '../types'; +import SRPQuiz from './SRPQuiz'; + +const store = configureStore({ + metamask: { + ...mockState.metamask, + }, +}); + +let openTabSpy; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + return { + ...original, + useHistory: () => ({ + push: jest.fn(), + }), + }; +}); + +async function waitForStage(stage) { + return await waitFor(() => { + expect(screen.getByTestId(`srp_stage_${stage}`)).toBeInTheDocument(); + }); +} + +function clickButton(id) { + fireEvent.click(screen.getByTestId(id)); +} + +describe('srp-reveal-quiz', () => { + beforeAll(() => { + global.platform = { openTab: jest.fn() }; + openTabSpy = jest.spyOn(global.platform, 'openTab'); + }); + + it('should go through the full sequence of steps', async () => { + renderWithProvider(, store); + + expect(screen.queryByTestId('srp-quiz-get-started')).toBeInTheDocument(); + + expect( + screen.queryByTestId('srp-quiz-right-answer'), + ).not.toBeInTheDocument(); + + clickButton('srp-quiz-learn-more'); + + await waitFor(() => + expect(openTabSpy).toHaveBeenCalledWith({ + url: expect.stringMatching(ZENDESK_URLS.PASSWORD_AND_SRP_ARTICLE), + }), + ); + + clickButton('srp-quiz-get-started'); + + await waitForStage(QuizStage.questionOne); + + clickButton('srp-quiz-wrong-answer'); + + await waitForStage(QuizStage.wrongAnswerQuestionOne); + + clickButton('srp-quiz-try-again'); + + await waitForStage(QuizStage.questionOne); + + clickButton('srp-quiz-right-answer'); + + await waitForStage(QuizStage.rightAnswerQuestionOne); + + clickButton('srp-quiz-continue'); + + await waitForStage(QuizStage.questionTwo); + + clickButton('srp-quiz-wrong-answer'); + + await waitForStage(QuizStage.wrongAnswerQuestionTwo); + + clickButton('srp-quiz-try-again'); + + await waitForStage(QuizStage.questionTwo); + + clickButton('srp-quiz-right-answer'); + + await waitForStage(QuizStage.rightAnswerQuestionTwo); + + clickButton('srp-quiz-continue'); + }); +}); diff --git a/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.tsx b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.tsx new file mode 100644 index 000000000..44f3fd29b --- /dev/null +++ b/ui/components/app/srp-quiz-modal/SRPQuiz/SRPQuiz.tsx @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, import/no-commonjs */ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + MetaMetricsEventCategory, + MetaMetricsEventKeyType, + MetaMetricsEventName, +} from '../../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../../contexts/metametrics'; +import { + BlockSize, + Display, + FlexDirection, + IconColor, + TextAlign, +} from '../../../../helpers/constants/design-system'; +import { REVEAL_SEED_ROUTE } from '../../../../helpers/constants/routes'; +import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + BUTTON_SIZES, + BUTTON_VARIANT, + Icon, + IconName, + IconSize, + Modal, + ModalContent, + ModalHeader, + ModalOverlay, +} from '../../../component-library'; +import QuizContent from '../QuizContent'; +import { JSXDict, QuizStage } from '../types'; + +const wrongAnswerIcon = ( + +); + +const rightAnswerIcon = ( + +); + +const openSupportArticle = (): void => { + global.platform.openTab({ + url: ZENDESK_URLS.PASSWORD_AND_SRP_ARTICLE, + }); +}; + +export default function SRPQuiz(props: any) { + const [stage, setStage] = useState(QuizStage.introduction); + + const trackEvent = useContext(MetaMetricsContext); + const history = useHistory(); + const t = useI18nContext(); + + // This should not be a state variable, because it's derivable from the state variable `stage` + // (Making it a state variable forces the component to render twice) + let title = ''; + + // Using a dictionary of JSX elements eliminates the need for a switch statement + const stages: JSXDict = {}; + + stages[QuizStage.introduction] = () => { + title = t('srpSecurityQuizTitle'); + return ( + setStage(QuizStage.questionOne), + variant: BUTTON_VARIANT.PRIMARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-get-started', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + 'data-testid': 'srp-quiz-learn-more', + }, + ]} + /> + ); + }; + + stages[QuizStage.questionOne] = () => { + title = `1 ${t('ofTextNofM')} 2`; + return ( + setStage(QuizStage.wrongAnswerQuestionOne), + variant: BUTTON_VARIANT.SECONDARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-wrong-answer', + }, + { + label: t('srpSecurityQuizQuestionOneRightAnswer'), + onClick: () => setStage(QuizStage.rightAnswerQuestionOne), + variant: BUTTON_VARIANT.SECONDARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-right-answer', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + stages[QuizStage.rightAnswerQuestionOne] = () => { + title = `1 ${t('ofTextNofM')} 2`; + return ( + setStage(QuizStage.questionTwo), + variant: BUTTON_VARIANT.PRIMARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-continue', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + stages[QuizStage.wrongAnswerQuestionOne] = () => { + title = `1 ${t('ofTextNofM')} 2`; + return ( + setStage(QuizStage.questionOne), + variant: BUTTON_VARIANT.PRIMARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-try-again', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + stages[QuizStage.questionTwo] = () => { + title = `2 ${t('ofTextNofM')} 2`; + return ( + setStage(QuizStage.rightAnswerQuestionTwo), + variant: BUTTON_VARIANT.SECONDARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-right-answer', + }, + { + label: t('srpSecurityQuizQuestionTwoWrongAnswer'), + onClick: () => setStage(QuizStage.wrongAnswerQuestionTwo), + variant: BUTTON_VARIANT.SECONDARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-wrong-answer', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + stages[QuizStage.rightAnswerQuestionTwo] = () => { + title = `2 ${t('ofTextNofM')} 2`; + return ( + history.push(REVEAL_SEED_ROUTE), + variant: BUTTON_VARIANT.PRIMARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-continue', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + stages[QuizStage.wrongAnswerQuestionTwo] = () => { + title = `2 ${t('ofTextNofM')} 2`; + return ( + setStage(QuizStage.questionTwo), + variant: BUTTON_VARIANT.PRIMARY, + size: BUTTON_SIZES.LG, + 'data-testid': 'srp-quiz-try-again', + }, + { + label: t('learnMoreUpperCase'), + onClick: openSupportArticle, + variant: BUTTON_VARIANT.LINK, + }, + ]} + /> + ); + }; + + // trackEvent shortcut specific to the SRP quiz + const trackEventSrp = useCallback((location) => { + trackEvent( + { + category: MetaMetricsEventCategory.Keys, + event: MetaMetricsEventName.KeyExportSelected, + properties: { + key_type: MetaMetricsEventKeyType.Srp, + location, + }, + }, + {}, + ); + }, []); + + useEffect(() => { + trackEventSrp(`stage_${stage}`); // Call MetaMetrics based on the current stage + }, [stage]); // Only call this when the stage changes + + const quizContent = stages[stage](); // Pick the content using the right stage from the JSXDict + + return ( + + + + + {title} + + + {quizContent} + + + ); +} diff --git a/ui/components/app/srp-quiz-modal/SRPQuiz/index.ts b/ui/components/app/srp-quiz-modal/SRPQuiz/index.ts new file mode 100644 index 000000000..8fd4c830a --- /dev/null +++ b/ui/components/app/srp-quiz-modal/SRPQuiz/index.ts @@ -0,0 +1 @@ +export { default } from './SRPQuiz'; diff --git a/ui/components/app/srp-quiz-modal/index.ts b/ui/components/app/srp-quiz-modal/index.ts new file mode 100644 index 000000000..8fd4c830a --- /dev/null +++ b/ui/components/app/srp-quiz-modal/index.ts @@ -0,0 +1 @@ +export { default } from './SRPQuiz'; diff --git a/ui/components/app/srp-quiz-modal/types.ts b/ui/components/app/srp-quiz-modal/types.ts new file mode 100644 index 000000000..7c83dee67 --- /dev/null +++ b/ui/components/app/srp-quiz-modal/types.ts @@ -0,0 +1,40 @@ +export enum QuizStage { + introduction = 'introduction', + questionOne = 'question_one', + wrongAnswerQuestionOne = 'wrong_answer_question_one', + rightAnswerQuestionOne = 'right_answer_question_one', + questionTwo = 'question_two', + wrongAnswerQuestionTwo = 'wrong_answer_question_two', + rightAnswerQuestionTwo = 'right_answer_question_two', +} + +export interface IQuizInformationProps { + /** + * The icon to display in the modal should use component + */ + icon?: any; + /** + * The image to display in the modal + */ + image?: string; + /** + * The text content to go inside of the component + */ + content: string; + /** + * More text content to go inside of the component + */ + moreContent?: string; + /** + * Array of - - - + Reveal Secret Recovery Phrase +