mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal
This commit is contained in:
commit
87857adcae
@ -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
|
||||
|
11
.circleci/scripts/check-working-tree.sh
Executable file
11
.circleci/scripts/check-working-tree.sh
Executable file
@ -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
|
@ -7,12 +7,12 @@ set -o pipefail
|
||||
sudo apt-get update
|
||||
|
||||
# To get the latest version, see <https://www.ubuntuupdates.org/ppa/google_chrome?dist=stable>
|
||||
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}"
|
||||
|
||||
|
@ -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
|
@ -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
|
@ -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',
|
||||
|
328
.github/scripts/add-release-label-to-pr-and-linked-issues.ts
vendored
Normal file
328
.github/scripts/add-release-label-to-pr-and-linked-issues.ts
vendored
Normal file
@ -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<void> {
|
||||
// "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<typeof GitHub> = 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<typeof GitHub>, repoOwner: string, repoName: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoOwner: string, repoName: string, labelName: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoId: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
|
||||
|
||||
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<typeof GitHub>, repoOwner: string, repoName: string, labelName: string, labelColor: string, labelDescription: string): Promise<string> {
|
||||
|
||||
// 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<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable> {
|
||||
|
||||
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<typeof GitHub>, repoOwner: string, repoName: string, prNumber: number): Promise<Labelable[]> {
|
||||
|
||||
// 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<typeof GitHub>, labelable: Labelable, labelName: string, labelColor: string, labelDescription: string): Promise<void> {
|
||||
|
||||
// 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],
|
||||
});
|
||||
|
||||
}
|
39
.github/workflows/add-release-label.yml
vendored
Normal file
39
.github/workflows/add-release-label.yml
vendored
Normal file
@ -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
|
6
.github/workflows/fitness-functions.yml
vendored
6
.github/workflows/fitness-functions.yml
vendored
@ -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"
|
||||
npm run fitness-functions -- "ci" "./diff"
|
||||
|
45
.github/workflows/main.yml
vendored
Normal file
45
.github/workflows/main.yml
vendored
Normal file
@ -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
|
40
.github/workflows/remove-labels-after-pr-closed.yml
vendored
Normal file
40
.github/workflows/remove-labels-after-pr-closed.yml
vendored
Normal file
@ -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
|
22
.github/workflows/sonar.yml
vendored
Normal file
22
.github/workflows/sonar.yml
vendored
Normal file
@ -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 }}
|
172
.github/workflows/update-lavamoat-policies.yml
vendored
Normal file
172
.github/workflows/update-lavamoat-policies.yml
vendored
Normal file
@ -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 }}
|
@ -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',
|
||||
|
@ -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 };
|
||||
|
@ -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) {
|
29
README.md
29
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
|
||||
|
||||
|
62
app/_locales/de/messages.json
generated
62
app/_locales/de/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/el/messages.json
generated
62
app/_locales/el/messages.json
generated
@ -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"
|
||||
|
84
app/_locales/en/messages.json
generated
84
app/_locales/en/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/es/messages.json
generated
62
app/_locales/es/messages.json
generated
@ -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"
|
||||
|
3
app/_locales/es_419/messages.json
generated
3
app/_locales/es_419/messages.json
generated
@ -1269,9 +1269,6 @@
|
||||
"lock": {
|
||||
"message": "Bloquear"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "El tiempo de bloqueo es demasiado largo"
|
||||
},
|
||||
"low": {
|
||||
"message": "Baja"
|
||||
},
|
||||
|
62
app/_locales/fr/messages.json
generated
62
app/_locales/fr/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/hi/messages.json
generated
62
app/_locales/hi/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/id/messages.json
generated
62
app/_locales/id/messages.json
generated
@ -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"
|
||||
|
3
app/_locales/it/messages.json
generated
3
app/_locales/it/messages.json
generated
@ -1117,9 +1117,6 @@
|
||||
"lock": {
|
||||
"message": "Disconnetti"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "Tempo di inattività troppo lungo"
|
||||
},
|
||||
"mainnet": {
|
||||
"message": "Rete Ethereum Principale"
|
||||
},
|
||||
|
62
app/_locales/ja/messages.json
generated
62
app/_locales/ja/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/ko/messages.json
generated
62
app/_locales/ko/messages.json
generated
@ -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"
|
||||
|
3
app/_locales/ph/messages.json
generated
3
app/_locales/ph/messages.json
generated
@ -816,9 +816,6 @@
|
||||
"lock": {
|
||||
"message": "I-lock"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "Masyadong matagal ang oras ng pag-lock"
|
||||
},
|
||||
"mainnet": {
|
||||
"message": "Ethereum Mainnet"
|
||||
},
|
||||
|
62
app/_locales/pt/messages.json
generated
62
app/_locales/pt/messages.json
generated
@ -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"
|
||||
|
3
app/_locales/pt_BR/messages.json
generated
3
app/_locales/pt_BR/messages.json
generated
@ -1269,9 +1269,6 @@
|
||||
"lock": {
|
||||
"message": "Bloquear"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "O tempo de bloqueio é longo demais"
|
||||
},
|
||||
"low": {
|
||||
"message": "Baixa"
|
||||
},
|
||||
|
62
app/_locales/ru/messages.json
generated
62
app/_locales/ru/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/tl/messages.json
generated
62
app/_locales/tl/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/tr/messages.json
generated
62
app/_locales/tr/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/vi/messages.json
generated
62
app/_locales/vi/messages.json
generated
@ -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"
|
||||
|
62
app/_locales/zh_CN/messages.json
generated
62
app/_locales/zh_CN/messages.json
generated
@ -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"
|
||||
|
3
app/_locales/zh_TW/messages.json
generated
3
app/_locales/zh_TW/messages.json
generated
@ -821,9 +821,6 @@
|
||||
"lock": {
|
||||
"message": "鎖定"
|
||||
},
|
||||
"lockTimeTooGreat": {
|
||||
"message": "鎖定時間過長"
|
||||
},
|
||||
"mainnet": {
|
||||
"message": "以太坊 主網路"
|
||||
},
|
||||
|
BIN
app/images/reveal-srp.png
Normal file
BIN
app/images/reveal-srp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -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,
|
||||
),
|
||||
|
@ -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: {},
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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',
|
||||
|
@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & {
|
||||
};
|
||||
|
||||
export type StateMessage = Required<
|
||||
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata'>
|
||||
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata' | 'error'>
|
||||
>;
|
||||
|
||||
export type DecryptMessageControllerState = {
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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,
|
||||
|
@ -45,7 +45,7 @@ export type CoreMessage = AbstractMessage & {
|
||||
};
|
||||
|
||||
export type StateMessage = Required<
|
||||
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata'>
|
||||
Omit<AbstractMessage, 'securityProviderResponse' | 'metadata' | 'error'>
|
||||
> & {
|
||||
msgParams: string;
|
||||
};
|
||||
|
@ -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 () {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
@ -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<unknown, unknown>;
|
||||
}) {
|
||||
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<unknown, unknown> => {
|
||||
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();
|
||||
});
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './network-controller';
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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<void>,
|
||||
) {
|
||||
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<any>;
|
||||
makeRpcCallsInSeries: (requests: Request[]) => Promise<any[]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<any>,
|
||||
) {
|
||||
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 };
|
||||
}
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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: <https://ethereum.github.io/execution-apis/api-documentation/>
|
||||
// Infura documentation: <https://docs.infura.io/infura/networks/ethereum/json-rpc-methods>
|
||||
|
||||
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');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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<void>;
|
||||
} & (
|
||||
| {
|
||||
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<any>,
|
||||
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
|
||||
) => {
|
||||
return this.#handleSend(payload, callback);
|
||||
};
|
||||
|
||||
sendAsync = (
|
||||
payload: JsonRpcRequest<any>,
|
||||
callback: (error: unknown, response?: JsonRpcResponse<any>) => void,
|
||||
) => {
|
||||
return this.#handleSend(payload, callback);
|
||||
};
|
||||
|
||||
#handleSend(
|
||||
payload: JsonRpcRequest<any>,
|
||||
callback: (error: unknown, response?: JsonRpcResponse<any>) => 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<any>) => 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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/<chain_id>/gasPrices`,
|
||||
EIP1559APIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/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 {
|
||||
|
@ -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);
|
||||
|
125
app/scripts/translate.test.ts
Normal file
125
app/scripts/translate.test.ts
Normal file
@ -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<typeof getMessage>;
|
||||
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<typeof getMessage>).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<typeof getMessage>
|
||||
).mockReturnValueOnce(null);
|
||||
|
||||
(
|
||||
getMessage as jest.MockedFunction<typeof getMessage>
|
||||
).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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
31
app/scripts/translate.ts
Normal file
31
app/scripts/translate.ts
Normal file
@ -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<void> {
|
||||
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)
|
||||
);
|
||||
}
|
@ -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:
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
27
development/get-next-semver-version.sh
Executable file
27
development/get-next-semver-version.sh
Executable file
@ -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"
|
@ -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": {}
|
||||
}
|
||||
}
|
@ -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],
|
||||
|
@ -1,9 +1,6 @@
|
||||
module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/app/scripts/constants/error-utils.js',
|
||||
'<rootDir>/app/scripts/controllers/network/**/*.js',
|
||||
'<rootDir>/app/scripts/controllers/network/**/*.ts',
|
||||
'!<rootDir>/app/scripts/controllers/network/**/test/*.ts',
|
||||
'<rootDir>/app/scripts/controllers/permissions/**/*.js',
|
||||
'<rootDir>/app/scripts/controllers/sign.ts',
|
||||
'<rootDir>/app/scripts/controllers/decrypt-message.ts',
|
||||
@ -40,8 +37,6 @@ module.exports = {
|
||||
'<rootDir>/app/scripts/constants/error-utils.test.js',
|
||||
'<rootDir>/app/scripts/controllers/app-state.test.js',
|
||||
'<rootDir>/app/scripts/controllers/mmi-controller.test.js',
|
||||
'<rootDir>/app/scripts/controllers/network/**/*.test.js',
|
||||
'<rootDir>/app/scripts/controllers/network/**/*.test.ts',
|
||||
'<rootDir>/app/scripts/controllers/permissions/**/*.test.js',
|
||||
'<rootDir>/app/scripts/controllers/sign.test.ts',
|
||||
'<rootDir>/app/scripts/controllers/decrypt-message.test.ts',
|
||||
@ -51,6 +46,7 @@ module.exports = {
|
||||
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js',
|
||||
'<rootDir>/app/scripts/migrations/*.test.(js|ts)',
|
||||
'<rootDir>/app/scripts/platforms/*.test.js',
|
||||
'<rootDir>/app/scripts/translate.test.ts',
|
||||
'<rootDir>/shared/**/*.test.(js|ts)',
|
||||
'<rootDir>/ui/**/*.test.(js|ts|tsx)',
|
||||
'<rootDir>/development/fitness-functions/**/*.test.(js|ts|tsx)',
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
29
package.json
29
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",
|
||||
|
@ -3,3 +3,5 @@ export enum ThemeType {
|
||||
dark = 'dark',
|
||||
os = 'os',
|
||||
}
|
||||
|
||||
export const DEFAULT_AUTO_LOCK_TIME_LIMIT = 0;
|
||||
|
@ -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';
|
||||
|
@ -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(),
|
||||
}));
|
||||
|
335
shared/modules/i18n.test.ts
Normal file
335
shared/modules/i18n.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
223
shared/modules/i18n.ts
Normal file
223
shared/modules/i18n.ts
Normal file
@ -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<R> {
|
||||
[translationKey: string]: R;
|
||||
}
|
||||
|
||||
export const FALLBACK_LOCALE = 'en';
|
||||
|
||||
const warned: { [localeCode: string]: I18NMessageDictMap<boolean> } = {};
|
||||
|
||||
const missingMessageErrors: I18NMessageDictMap<Error> = {};
|
||||
|
||||
const missingSubstitutionErrors: {
|
||||
[localeCode: string]: I18NMessageDictMap<boolean>;
|
||||
} = {};
|
||||
|
||||
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 = <T>(
|
||||
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<I18NMessageDict> {
|
||||
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<void> {
|
||||
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<Json> {
|
||||
const response = await fetchWithTimeout(
|
||||
`./intl/${languageTag}/relative-time-format-data.json`,
|
||||
);
|
||||
return await response.json();
|
||||
}
|
18
sonar-project.properties
Normal file
18
sonar-project.properties
Normal file
@ -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
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -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
|
||||
|
135
test/e2e/tests/settings-security-reveal-srp.spec.js
Normal file
135
test/e2e/tests/settings-security-reveal-srp.spec.js
Normal file
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -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' });
|
||||
|
@ -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)',
|
||||
|
6
test/lib/i18n-helpers.js
Normal file
6
test/lib/i18n-helpers.js
Normal file
@ -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);
|
||||
}
|
@ -14,7 +14,8 @@
|
||||
"outDir": "tsout",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"**/jest-coverage/**/*",
|
||||
|
@ -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 (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.ROW}
|
||||
display={Display.Flex}
|
||||
flexDirection={FlexDirection.Row}
|
||||
marginTop={4}
|
||||
marginLeft={2}
|
||||
marginRight={2}
|
||||
@ -90,7 +90,6 @@ const AdvancedGasFeeDefaults = () => {
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
color={TextColor.textAlternative}
|
||||
margin={0}
|
||||
>
|
||||
{isDefaultSettingsSelected
|
||||
? t('advancedGasFeeDefaultOptOut')
|
||||
|
29
ui/components/app/cancel-button/cancel-buitton.stories.js
Normal file
29
ui/components/app/cancel-button/cancel-buitton.stories.js
Normal file
@ -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) => <CancelButton {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -89,7 +89,7 @@ export default function HoldToRevealButton({ buttonText, onLongPressed }) {
|
||||
setHasTriggeredUnlock(true);
|
||||
preventPropogation(e);
|
||||
},
|
||||
[onLongPressed],
|
||||
[onLongPressed, trackEvent],
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -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')}
|
||||
</Text>
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
color={TextColor.textAlternative}
|
||||
boxProps={{ marginTop: 2, marginBottom: 0 }}
|
||||
marginTop={2}
|
||||
>
|
||||
{t('contractDescription')}
|
||||
</Text>
|
||||
@ -84,14 +83,14 @@ export default function ContractDetailsModal({
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
marginTop={4}
|
||||
marginBottom={2}
|
||||
>
|
||||
{nft ? t('contractNFT') : t('contractToken')}
|
||||
</Text>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
borderRadius={Size.SM}
|
||||
borderStyle={BorderStyle.solid}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
@ -124,9 +123,8 @@ export default function ContractDetailsModal({
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
color={TextColor.textAlternative}
|
||||
marginTop={0}
|
||||
marginBottom={4}
|
||||
>
|
||||
{ellipsify(tokenAddress)}
|
||||
@ -148,7 +146,7 @@ export default function ContractDetailsModal({
|
||||
}
|
||||
>
|
||||
<ButtonIcon
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
iconName={
|
||||
copiedTokenAddress ? IconName.CopySuccess : IconName.Copy
|
||||
}
|
||||
@ -163,7 +161,7 @@ export default function ContractDetailsModal({
|
||||
</Tooltip>
|
||||
<Tooltip position="top" title={t('openInBlockExplorer')}>
|
||||
<ButtonIcon
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
iconName={IconName.Export}
|
||||
color={Color.iconMuted}
|
||||
onClick={() => {
|
||||
@ -189,7 +187,7 @@ export default function ContractDetailsModal({
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
marginTop={4}
|
||||
marginBottom={2}
|
||||
>
|
||||
@ -200,7 +198,7 @@ export default function ContractDetailsModal({
|
||||
t('contractRequestingSpendingCap')}
|
||||
</Text>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
borderRadius={Size.SM}
|
||||
borderStyle={BorderStyle.solid}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
@ -224,9 +222,8 @@ export default function ContractDetailsModal({
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
color={TextColor.textAlternative}
|
||||
marginTop={0}
|
||||
marginBottom={4}
|
||||
>
|
||||
{ellipsify(toAddress)}
|
||||
@ -246,7 +243,7 @@ export default function ContractDetailsModal({
|
||||
}
|
||||
>
|
||||
<ButtonIcon
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
iconName={
|
||||
copiedToAddress ? IconName.CopySuccess : IconName.Copy
|
||||
}
|
||||
@ -261,7 +258,7 @@ export default function ContractDetailsModal({
|
||||
</Tooltip>
|
||||
<Tooltip position="top" title={t('openInBlockExplorer')}>
|
||||
<ButtonIcon
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
iconName={IconName.Export}
|
||||
color={Color.iconMuted}
|
||||
onClick={() => {
|
||||
@ -284,7 +281,7 @@ export default function ContractDetailsModal({
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
display={Display.Flex}
|
||||
paddingTop={6}
|
||||
paddingRight={4}
|
||||
paddingBottom={6}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user