mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 11:01:41 +01:00
Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal
This commit is contained in:
commit
a2839ce7aa
@ -152,8 +152,14 @@ workflows:
|
|||||||
- prep-build-test
|
- prep-build-test
|
||||||
- test-e2e-chrome-snaps:
|
- test-e2e-chrome-snaps:
|
||||||
requires:
|
requires:
|
||||||
- prep-build-test-flask
|
- prep-build-test
|
||||||
- test-e2e-firefox-snaps:
|
- test-e2e-firefox-snaps:
|
||||||
|
requires:
|
||||||
|
- prep-build-test
|
||||||
|
- test-e2e-chrome-snaps-flask:
|
||||||
|
requires:
|
||||||
|
- prep-build-test-flask
|
||||||
|
- test-e2e-firefox-snaps-flask:
|
||||||
requires:
|
requires:
|
||||||
- prep-build-test-flask
|
- prep-build-test-flask
|
||||||
- test-e2e-chrome-mv3:
|
- test-e2e-chrome-mv3:
|
||||||
@ -215,6 +221,7 @@ workflows:
|
|||||||
- prep-build-flask
|
- prep-build-flask
|
||||||
- all-tests-pass:
|
- all-tests-pass:
|
||||||
requires:
|
requires:
|
||||||
|
- test-deps-depcheck
|
||||||
- validate-lavamoat-allow-scripts
|
- validate-lavamoat-allow-scripts
|
||||||
- validate-lavamoat-policy-build
|
- validate-lavamoat-policy-build
|
||||||
- validate-lavamoat-policy-webapp
|
- validate-lavamoat-policy-webapp
|
||||||
@ -847,6 +854,80 @@ jobs:
|
|||||||
path: test/test-results/e2e.xml
|
path: test/test-results/e2e.xml
|
||||||
|
|
||||||
test-e2e-firefox-snaps:
|
test-e2e-firefox-snaps:
|
||||||
|
executor: node-browsers
|
||||||
|
parallelism: 4
|
||||||
|
steps:
|
||||||
|
- run: *shallow-git-clone
|
||||||
|
- run:
|
||||||
|
name: Install Firefox
|
||||||
|
command: ./.circleci/scripts/firefox-install.sh
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run:
|
||||||
|
name: Move test build to dist
|
||||||
|
command: mv ./dist-test ./dist
|
||||||
|
- run:
|
||||||
|
name: Move test zips to builds
|
||||||
|
command: mv ./builds-test ./builds
|
||||||
|
- run:
|
||||||
|
name: test:e2e:firefox:snaps
|
||||||
|
command: |
|
||||||
|
if .circleci/scripts/test-run-e2e.sh
|
||||||
|
then
|
||||||
|
yarn test:e2e:firefox:snaps --retries 2 --debug --build-type=main
|
||||||
|
fi
|
||||||
|
no_output_timeout: 20m
|
||||||
|
- run:
|
||||||
|
name: Merge JUnit report
|
||||||
|
command: |
|
||||||
|
if [ "$(ls -A test/test-results/e2e)" ]; then
|
||||||
|
yarn test:e2e:report
|
||||||
|
fi
|
||||||
|
when: always
|
||||||
|
- store_artifacts:
|
||||||
|
path: test-artifacts
|
||||||
|
destination: test-artifacts
|
||||||
|
- store_test_results:
|
||||||
|
path: test/test-results/e2e.xml
|
||||||
|
|
||||||
|
test-e2e-chrome-snaps:
|
||||||
|
executor: node-browsers
|
||||||
|
parallelism: 4
|
||||||
|
steps:
|
||||||
|
- run: *shallow-git-clone
|
||||||
|
- run:
|
||||||
|
name: Re-Install Chrome
|
||||||
|
command: ./.circleci/scripts/chrome-install.sh
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- run:
|
||||||
|
name: Move test build to dist
|
||||||
|
command: mv ./dist-test ./dist
|
||||||
|
- run:
|
||||||
|
name: Move test zips to builds
|
||||||
|
command: mv ./builds-test ./builds
|
||||||
|
- run:
|
||||||
|
name: test:e2e:chrome:snaps
|
||||||
|
command: |
|
||||||
|
if .circleci/scripts/test-run-e2e.sh
|
||||||
|
then
|
||||||
|
yarn test:e2e:chrome:snaps --retries 2 --debug --build-type=main
|
||||||
|
fi
|
||||||
|
no_output_timeout: 20m
|
||||||
|
- run:
|
||||||
|
name: Merge JUnit report
|
||||||
|
command: |
|
||||||
|
if [ "$(ls -A test/test-results/e2e)" ]; then
|
||||||
|
yarn test:e2e:report
|
||||||
|
fi
|
||||||
|
when: always
|
||||||
|
- store_artifacts:
|
||||||
|
path: test-artifacts
|
||||||
|
destination: test-artifacts
|
||||||
|
- store_test_results:
|
||||||
|
path: test/test-results/e2e.xml
|
||||||
|
|
||||||
|
test-e2e-firefox-snaps-flask:
|
||||||
executor: node-browsers
|
executor: node-browsers
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
steps:
|
steps:
|
||||||
@ -883,7 +964,7 @@ jobs:
|
|||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: test/test-results/e2e.xml
|
path: test/test-results/e2e.xml
|
||||||
|
|
||||||
test-e2e-chrome-snaps:
|
test-e2e-chrome-snaps-flask:
|
||||||
executor: node-browsers
|
executor: node-browsers
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
steps:
|
steps:
|
||||||
|
@ -239,6 +239,7 @@ module.exports = {
|
|||||||
'app/scripts/controllers/app-state.test.js',
|
'app/scripts/controllers/app-state.test.js',
|
||||||
'app/scripts/controllers/mmi-controller.test.js',
|
'app/scripts/controllers/mmi-controller.test.js',
|
||||||
'app/scripts/controllers/permissions/**/*.test.js',
|
'app/scripts/controllers/permissions/**/*.test.js',
|
||||||
|
'app/scripts/controllers/preferences.test.js',
|
||||||
'app/scripts/lib/**/*.test.js',
|
'app/scripts/lib/**/*.test.js',
|
||||||
'app/scripts/migrations/*.test.js',
|
'app/scripts/migrations/*.test.js',
|
||||||
'app/scripts/platforms/*.test.js',
|
'app/scripts/platforms/*.test.js',
|
||||||
@ -268,6 +269,7 @@ module.exports = {
|
|||||||
'app/scripts/controllers/app-state.test.js',
|
'app/scripts/controllers/app-state.test.js',
|
||||||
'app/scripts/controllers/mmi-controller.test.js',
|
'app/scripts/controllers/mmi-controller.test.js',
|
||||||
'app/scripts/controllers/permissions/**/*.test.js',
|
'app/scripts/controllers/permissions/**/*.test.js',
|
||||||
|
'app/scripts/controllers/preferences.test.js',
|
||||||
'app/scripts/lib/**/*.test.js',
|
'app/scripts/lib/**/*.test.js',
|
||||||
'app/scripts/migrations/*.test.js',
|
'app/scripts/migrations/*.test.js',
|
||||||
'app/scripts/platforms/*.test.js',
|
'app/scripts/platforms/*.test.js',
|
||||||
|
120
.github/scripts/close-release-bug-report-issue.ts
vendored
Normal file
120
.github/scripts/close-release-bug-report-issue.ts
vendored
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import * as core from '@actions/core';
|
||||||
|
import { context, getOctokit } from '@actions/github';
|
||||||
|
import { GitHub } from '@actions/github/lib/utils';
|
||||||
|
|
||||||
|
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 bug report issues which are not located in the same repository,
|
||||||
|
// we need to create our own "BUG_REPORT_TOKEN" with "repo" permissions.
|
||||||
|
// Such a token allows to access other repositories of the MetaMask organisation.
|
||||||
|
const personalAccessToken = process.env.BUG_REPORT_TOKEN;
|
||||||
|
if (!personalAccessToken) {
|
||||||
|
core.setFailed('BUG_REPORT_TOKEN not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoOwner = context.repo.owner; // MetaMask
|
||||||
|
|
||||||
|
const bugReportRepo = process.env.BUG_REPORT_REPO;
|
||||||
|
if (!bugReportRepo) {
|
||||||
|
core.setFailed('BUG_REPORT_REPO not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract branch name from the context
|
||||||
|
const branchName: string = context.payload.pull_request?.head.ref || "";
|
||||||
|
|
||||||
|
// Extract semver version number from the branch name
|
||||||
|
const releaseVersionNumberMatch = branchName.match(/^Version-v(\d+\.\d+\.\d+)$/);
|
||||||
|
|
||||||
|
if (!releaseVersionNumberMatch) {
|
||||||
|
core.setFailed(`Failed to extract version number from branch name: ${branchName}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseVersionNumber = releaseVersionNumberMatch[1];
|
||||||
|
|
||||||
|
// Initialise octokit, required to call Github GraphQL API
|
||||||
|
const octokit: InstanceType<typeof GitHub> = getOctokit(personalAccessToken);
|
||||||
|
|
||||||
|
const bugReportIssue = await retrieveOpenBugReportIssue(octokit, repoOwner, bugReportRepo, releaseVersionNumber);
|
||||||
|
|
||||||
|
if (!bugReportIssue) {
|
||||||
|
throw new Error(`No open bug report issue was found for release ${releaseVersionNumber} on ${repoOwner}/${bugReportRepo} repo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bugReportIssue.title?.toLocaleLowerCase() !== `v${releaseVersionNumber} Bug Report`.toLocaleLowerCase()) {
|
||||||
|
throw new Error(`Unexpected bug report title: "${bugReportIssue.title}" instead of "v${releaseVersionNumber} Bug Report"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Closing bug report issue with title "${bugReportIssue.title}" and id: ${bugReportIssue.id}`);
|
||||||
|
|
||||||
|
await closeIssue(octokit, bugReportIssue.id);
|
||||||
|
|
||||||
|
console.log(`Issue with id: ${bugReportIssue.id} successfully closed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function retrieves the issue titled "vx.y.z Bug Report" on a specific repo
|
||||||
|
async function retrieveOpenBugReportIssue(octokit: InstanceType<typeof GitHub>, repoOwner: string, repoName: string, releaseVersionNumber: string): Promise<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
} | undefined> {
|
||||||
|
|
||||||
|
const retrieveOpenBugReportIssueQuery = `
|
||||||
|
query RetrieveOpenBugReportIssue {
|
||||||
|
search(query: "repo:${repoOwner}/${repoName} type:issue is:open in:title v${releaseVersionNumber} Bug Report", type: ISSUE, first: 1) {
|
||||||
|
nodes {
|
||||||
|
... on Issue {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const retrieveOpenBugReportIssueQueryResult: {
|
||||||
|
search: {
|
||||||
|
nodes: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
} = await octokit.graphql(retrieveOpenBugReportIssueQuery);
|
||||||
|
|
||||||
|
const bugReportIssues = retrieveOpenBugReportIssueQueryResult?.search?.nodes;
|
||||||
|
|
||||||
|
return bugReportIssues?.length > 0 ? bugReportIssues[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function closes a Github issue, based on its ID
|
||||||
|
async function closeIssue(octokit: InstanceType<typeof GitHub>, issueId: string): Promise<string> {
|
||||||
|
|
||||||
|
const closeIssueMutation = `
|
||||||
|
mutation CloseIssue($issueId: ID!) {
|
||||||
|
updateIssue(input: {id: $issueId, state: CLOSED}) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const closeIssueMutationResult: {
|
||||||
|
updateIssue: {
|
||||||
|
clientMutationId: string;
|
||||||
|
};
|
||||||
|
} = await octokit.graphql(closeIssueMutation, {
|
||||||
|
issueId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientMutationId = closeIssueMutationResult?.updateIssue?.clientMutationId;
|
||||||
|
|
||||||
|
return clientMutationId;
|
||||||
|
}
|
2
.github/workflows/add-release-label.yml
vendored
2
.github/workflows/add-release-label.yml
vendored
@ -37,4 +37,4 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
|
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
|
||||||
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
|
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
|
||||||
run: npm run add-release-label-to-pr-and-linked-issues
|
run: yarn run add-release-label-to-pr-and-linked-issues
|
||||||
|
2
.github/workflows/check-pr-labels.yml
vendored
2
.github/workflows/check-pr-labels.yml
vendored
@ -35,4 +35,4 @@ jobs:
|
|||||||
id: check-pr-has-required-labels
|
id: check-pr-has-required-labels
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npm run check-pr-has-required-labels
|
run: yarn run check-pr-has-required-labels
|
||||||
|
34
.github/workflows/close-bug-report.yml
vendored
Normal file
34
.github/workflows/close-bug-report.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Close release bug report issue when release branch gets merged
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-bug-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'Version-v')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 1 # This retrieves only the latest commit.
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --immutable
|
||||||
|
|
||||||
|
- name: Close release bug report issue
|
||||||
|
id: close-release-bug-report-issue
|
||||||
|
env:
|
||||||
|
BUG_REPORT_REPO: MetaMask-planning
|
||||||
|
BUG_REPORT_TOKEN: ${{ secrets.BUG_REPORT_TOKEN }}
|
||||||
|
run: yarn run close-release-bug-report-issue
|
35
.github/workflows/create-bug-report.yml
vendored
Normal file
35
.github/workflows/create-bug-report.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Create release bug report issue when release branch gets created
|
||||||
|
|
||||||
|
on: create
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-bug-report:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract version from branch name if release branch
|
||||||
|
id: extract_version
|
||||||
|
run: |
|
||||||
|
if [[ "$GITHUB_REF" == "refs/heads/Version-v"* ]]; then
|
||||||
|
version="${GITHUB_REF#refs/heads/Version-v}"
|
||||||
|
echo "New release branch($version), continue next steps"
|
||||||
|
echo "version=$version" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "Not a release branch, skip next steps"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create bug report issue on planning repo
|
||||||
|
if: env.version
|
||||||
|
run: |
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"title": "v${{ env.version }} Bug Report",
|
||||||
|
"body": "This bug report was automatically created by a GitHub action upon the creation of release branch \`Version-v${{ env.version }}\` (release cut).\n\n**Expected actions for release engineers:**\n\n1. Convert this issue into a Zenhub epic and link all bugs identified during the release regression testing phase to this epic.\n\n2. After completing the first regression run, move this epic to \"Regression Completed\" on the [Extension Release Regression board](https://app.zenhub.com/workspaces/extension-release-regression-6478c62d937eaa15e95c33c5/board?filterLogic=any&labels=release-${{ env.version }},release-task).\n\nNote that once the release is prepared for store submission, meaning the \`Version-v${{ env.version }}\` branch merges into \`master\`, another GitHub action will automatically close this epic.",
|
||||||
|
"labels": ["type-bug", "regression-RC", "release-${{ env.version }}"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token ${{ secrets.BUG_REPORT_TOKEN }}" \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
https://api.github.com/repos/MetaMask/MetaMask-planning/issues \
|
||||||
|
-d "$payload"
|
@ -8,6 +8,7 @@ module.exports = {
|
|||||||
'./app/scripts/controllers/app-state.test.js',
|
'./app/scripts/controllers/app-state.test.js',
|
||||||
'./app/scripts/controllers/permissions/**/*.test.js',
|
'./app/scripts/controllers/permissions/**/*.test.js',
|
||||||
'./app/scripts/controllers/mmi-controller.test.js',
|
'./app/scripts/controllers/mmi-controller.test.js',
|
||||||
|
'./app/scripts/controllers/preferences.test.js',
|
||||||
'./app/scripts/constants/error-utils.test.js',
|
'./app/scripts/constants/error-utils.test.js',
|
||||||
'./development/fitness-functions/**/*.test.ts',
|
'./development/fitness-functions/**/*.test.ts',
|
||||||
'./test/e2e/helpers.test.js',
|
'./test/e2e/helpers.test.js',
|
||||||
|
@ -2,6 +2,7 @@ import { draftTransactionInitialState } from '../ui/ducks/send';
|
|||||||
import { KeyringType } from '../shared/constants/keyring';
|
import { KeyringType } from '../shared/constants/keyring';
|
||||||
import { NetworkType } from '@metamask/controller-utils';
|
import { NetworkType } from '@metamask/controller-utils';
|
||||||
import { NetworkStatus } from '@metamask/network-controller';
|
import { NetworkStatus } from '@metamask/network-controller';
|
||||||
|
import { CHAIN_IDS } from '../shared/constants/network';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
invalidCustomNetwork: {
|
invalidCustomNetwork: {
|
||||||
@ -529,6 +530,12 @@ const state = {
|
|||||||
preferences: {
|
preferences: {
|
||||||
useNativeCurrencyAsPrimaryCurrency: true,
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
},
|
},
|
||||||
|
incomingTransactionsPreferences: {
|
||||||
|
[CHAIN_IDS.MAINNET]: true,
|
||||||
|
[CHAIN_IDS.GOERLI]: false,
|
||||||
|
[CHAIN_IDS.OPTIMISM_TESTNET]: false,
|
||||||
|
[CHAIN_IDS.AVALANCHE_TESTNET]: true,
|
||||||
|
},
|
||||||
firstTimeFlowType: 'create',
|
firstTimeFlowType: 'create',
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
knownMethodData: {
|
knownMethodData: {
|
||||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [10.34.5]
|
||||||
|
### Changed
|
||||||
|
- Improve error diagnostic information
|
||||||
|
- Add additional logging for state migrations ([#20424](https://github.com/MetaMask/metamask-extension/pull/20424), [#20517](https://github.com/MetaMask/metamask-extension/pull/20517), [#20521](https://github.com/MetaMask/metamask-extension/pull/20521))
|
||||||
|
- Improve diagnostic state snapshot ([#20457](https://github.com/MetaMask/metamask-extension/pull/20457), [#20491](https://github.com/MetaMask/metamask-extension/pull/20491), [#20428](https://github.com/MetaMask/metamask-extension/pull/20428), [#20458](https://github.com/MetaMask/metamask-extension/pull/20458))
|
||||||
|
- Capture additional errors when state migrations fail ([#20427](https://github.com/MetaMask/metamask-extension/pull/20427))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix bug where state was temporarily incomplete upon initial load ([#20468](https://github.com/MetaMask/metamask-extension/pull/20468))
|
||||||
|
- In rare circumstances, this bug may have resulted in data loss (of preferences, permissions, or tracked NFTs/tokens) or UI crashes.
|
||||||
|
|
||||||
## [10.34.4]
|
## [10.34.4]
|
||||||
### Changed
|
### Changed
|
||||||
- Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420))
|
- Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420))
|
||||||
@ -3885,7 +3896,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Uncategorized
|
### Uncategorized
|
||||||
- Added the ability to restore accounts from seed words.
|
- Added the ability to restore accounts from seed words.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...HEAD
|
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.5...HEAD
|
||||||
|
[10.34.5]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...v10.34.5
|
||||||
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4
|
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4
|
||||||
[10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3
|
[10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3
|
||||||
[10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2
|
[10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2
|
||||||
|
20
README.md
20
README.md
@ -124,15 +124,17 @@ These test scripts all support additional options, which might be helpful for de
|
|||||||
Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
|
Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
--browser Set the browser used; either 'chrome' or 'firefox'.
|
--browser Set the browser used; either 'chrome' or 'firefox'.
|
||||||
[string] [choices: "chrome", "firefox"]
|
[string] [choices: "chrome", "firefox"]
|
||||||
--debug Run tests in debug mode, logging each driver interaction
|
--debug Run tests in debug mode, logging each driver interaction
|
||||||
[boolean] [default: false]
|
[boolean] [default: false]
|
||||||
--retries Set how many times the test should be retried upon failure.
|
--retries Set how many times the test should be retried upon failure.
|
||||||
[number] [default: 0]
|
[number] [default: 0]
|
||||||
--leave-running Leaves the browser running after a test fails, along with
|
--leave-running Leaves the browser running after a test fails, along with
|
||||||
anything else that the test used (ganache, the test dapp,
|
anything else that the test used (ganache, the test dapp,
|
||||||
etc.) [boolean] [default: false]
|
etc.) [boolean] [default: false]
|
||||||
|
--update-snapshot Update E2E test snapshots
|
||||||
|
[alias: -u] [boolean] [default: false]
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, to run the `account-details` tests using Chrome, with debug logging and with the browser set to remain open upon failure, you would use:
|
For example, to run the `account-details` tests using Chrome, with debug logging and with the browser set to remain open upon failure, you would use:
|
||||||
|
15
app/_locales/am/messages.json
generated
15
app/_locales/am/messages.json
generated
@ -169,9 +169,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "አድራሻን ወደ ቅንጥብ ሰሌዳ ቅዳ"
|
"message": "አድራሻን ወደ ቅንጥብ ሰሌዳ ቅዳ"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "የግል ቁልፍዎ ይህ ነው (ለመቅዳት ጠቅ ያድርጉ)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "ወደ ቅንጥብ ሰሌዳ ገልብጥ"
|
"message": "ወደ ቅንጥብ ሰሌዳ ገልብጥ"
|
||||||
},
|
},
|
||||||
@ -232,9 +229,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "በ ENS የስም ምዝገባ ላይ የተፈጠረ ስህተት"
|
"message": "በ ENS የስም ምዝገባ ላይ የተፈጠረ ስህተት"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "የይለፍ ቃል ያስገቡ"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "ለመቀጠል የይለፍ ቃል ያስገቡ"
|
"message": "ለመቀጠል የይለፍ ቃል ያስገቡ"
|
||||||
},
|
},
|
||||||
@ -247,9 +241,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "እይታን ዘርጋ"
|
"message": "እይታን ዘርጋ"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "የግል ቁልፍን ላክ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "አልተሳካም"
|
"message": "አልተሳካም"
|
||||||
},
|
},
|
||||||
@ -648,9 +639,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "በመላኪያ ማያ ላይ የ hex ውሂብ መስክን ለማሳየት ይህን ይምረጡ"
|
"message": "በመላኪያ ማያ ላይ የ hex ውሂብ መስክን ለማሳየት ይህን ይምረጡ"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "የግል ቁልፎችን አሳይ"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "የፊርማ ጥያቄ"
|
"message": "የፊርማ ጥያቄ"
|
||||||
},
|
},
|
||||||
@ -771,9 +759,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "እንደገና ሞክር"
|
"message": "እንደገና ሞክር"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "የ MetaMask የይለፍ ቃልዎን ይጻፉ"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "ያልተፈቀደ"
|
"message": "ያልተፈቀደ"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ar/messages.json
generated
15
app/_locales/ar/messages.json
generated
@ -179,9 +179,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "نسخ العنوان إلى الحافظة"
|
"message": "نسخ العنوان إلى الحافظة"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "هذا هو مفتاحك الخاص (انقر للنسخ)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "نسخ إلى الحافظة"
|
"message": "نسخ إلى الحافظة"
|
||||||
},
|
},
|
||||||
@ -245,9 +242,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "خطأ في تسجيل اسم ENS"
|
"message": "خطأ في تسجيل اسم ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "أدخل كلمة مرور"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "أدخل كلمة المرور للمتابعة"
|
"message": "أدخل كلمة المرور للمتابعة"
|
||||||
},
|
},
|
||||||
@ -260,9 +254,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "توسيع العرض"
|
"message": "توسيع العرض"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "تصدير المفتاح الخاص"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "فشل"
|
"message": "فشل"
|
||||||
},
|
},
|
||||||
@ -660,9 +651,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "حدد هذا لإظهار حقل بيانات سداسي عشرية على شاشة الإرسال"
|
"message": "حدد هذا لإظهار حقل بيانات سداسي عشرية على شاشة الإرسال"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "عرض المفاتيح الخاصة"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "طلب التوقيع"
|
"message": "طلب التوقيع"
|
||||||
},
|
},
|
||||||
@ -783,9 +771,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "إعادة المحاولة"
|
"message": "إعادة المحاولة"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "أدخل كلمة مرور MetaMask الخاصة بك"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "تم الرفض"
|
"message": "تم الرفض"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/bg/messages.json
generated
15
app/_locales/bg/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Копирайте адреса в клипборда"
|
"message": "Копирайте адреса в клипборда"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Това е Вашият личен ключ (кликнете, за да го копирате)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Копиране в буферната памет"
|
"message": "Копиране в буферната памет"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Грешка при регистрацията на име на ENS"
|
"message": "Грешка при регистрацията на име на ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Въведете парола"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Въведете парола, за да продължите"
|
"message": "Въведете парола, за да продължите"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Разгъване на изглед"
|
"message": "Разгъване на изглед"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Експортиране на частен ключ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Неуспешно"
|
"message": "Неуспешно"
|
||||||
},
|
},
|
||||||
@ -659,9 +650,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Изберете това, за да се покаже полето с шестнадесетични данни на екрана за изпращане"
|
"message": "Изберете това, за да се покаже полето с шестнадесетични данни на екрана за изпращане"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Показване на частни ключове"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Заявка за подпис"
|
"message": "Заявка за подпис"
|
||||||
},
|
},
|
||||||
@ -782,9 +770,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Нов опит"
|
"message": "Нов опит"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Въведете паролата си за MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Неодобрено"
|
"message": "Неодобрено"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/bn/messages.json
generated
15
app/_locales/bn/messages.json
generated
@ -172,9 +172,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "ক্লিপবোর্ডে ঠিকানা কপি করুন"
|
"message": "ক্লিপবোর্ডে ঠিকানা কপি করুন"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "এটি হল আপনার গোপন কী (কপি করতে ক্লিক করুন)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "ক্লিপবোর্ডে কপি করুন"
|
"message": "ক্লিপবোর্ডে কপি করুন"
|
||||||
},
|
},
|
||||||
@ -238,9 +235,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "ENS নাম নিবন্ধীকরণে ত্রুটি হয়েছে"
|
"message": "ENS নাম নিবন্ধীকরণে ত্রুটি হয়েছে"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "পাসওয়ার্ড লিখুন"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "অবিরত রাখতে পাসওয়ার্ড লিখুন"
|
"message": "অবিরত রাখতে পাসওয়ার্ড লিখুন"
|
||||||
},
|
},
|
||||||
@ -253,9 +247,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "ভিউ সম্প্রসারিত করুন"
|
"message": "ভিউ সম্প্রসারিত করুন"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "ব্যক্তিগত কী রপ্তানি করুন"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "ব্যর্থ হয়েছে"
|
"message": "ব্যর্থ হয়েছে"
|
||||||
},
|
},
|
||||||
@ -657,9 +648,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "পাঠাবার স্ক্রিনে হেক্স ডেটা ফিল্ডটি দেখাবার জন্য এটি নির্বাচন করুন"
|
"message": "পাঠাবার স্ক্রিনে হেক্স ডেটা ফিল্ডটি দেখাবার জন্য এটি নির্বাচন করুন"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "গোপনীয় কীগুলি দেখান"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "স্বাক্ষরের অনুরোধ"
|
"message": "স্বাক্ষরের অনুরোধ"
|
||||||
},
|
},
|
||||||
@ -780,9 +768,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "আবার করুন"
|
"message": "আবার করুন"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "আপনার MetaMask পাসওয়ার্ড টাইপ করুন"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "অননুমোদিত"
|
"message": "অননুমোদিত"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ca/messages.json
generated
15
app/_locales/ca/messages.json
generated
@ -172,9 +172,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copiar adreça al porta-retalls"
|
"message": "Copiar adreça al porta-retalls"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Aquesta és la teva clau privada (fes clic per a copiar)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Copia al porta-retalls"
|
"message": "Copia al porta-retalls"
|
||||||
},
|
},
|
||||||
@ -238,9 +235,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Error al registre de nom ENS"
|
"message": "Error al registre de nom ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Introdueix contrasenya"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Introdueix la contrasenya per continuar"
|
"message": "Introdueix la contrasenya per continuar"
|
||||||
},
|
},
|
||||||
@ -253,9 +247,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Eixamplar Vista"
|
"message": "Eixamplar Vista"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportar Clau Privada."
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Fallit"
|
"message": "Fallit"
|
||||||
},
|
},
|
||||||
@ -644,9 +635,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Selecciona això per a mostrar el camp de dades Hex a la pantalla d'enviament"
|
"message": "Selecciona això per a mostrar el camp de dades Hex a la pantalla d'enviament"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Mostrar Claus Privades"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Sol·licitud de Signatura"
|
"message": "Sol·licitud de Signatura"
|
||||||
},
|
},
|
||||||
@ -761,9 +749,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Torna-ho a provar"
|
"message": "Torna-ho a provar"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Tecleja la teva contrasenya de MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Pendent d'aprovació"
|
"message": "Pendent d'aprovació"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/cs/messages.json
generated
15
app/_locales/cs/messages.json
generated
@ -72,9 +72,6 @@
|
|||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "Zkopírováno!"
|
"message": "Zkopírováno!"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopírovat do schránky"
|
"message": "Kopírovat do schránky"
|
||||||
},
|
},
|
||||||
@ -105,15 +102,9 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "Upravit"
|
"message": "Upravit"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Zadejte heslo"
|
|
||||||
},
|
|
||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "Prohlédněte si účet na Etherscan"
|
"message": "Prohlédněte si účet na Etherscan"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportovat privátní klíč"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Neúspěšné"
|
"message": "Neúspěšné"
|
||||||
},
|
},
|
||||||
@ -304,9 +295,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"message": "Nastavení"
|
"message": "Nastavení"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Zobrazit privátní klíče"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Požadavek podpisu"
|
"message": "Požadavek podpisu"
|
||||||
},
|
},
|
||||||
@ -355,9 +343,6 @@
|
|||||||
"transactionError": {
|
"transactionError": {
|
||||||
"message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu."
|
"message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu."
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Zadejte své heslo"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neschváleno"
|
"message": "Neschváleno"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/da/messages.json
generated
15
app/_locales/da/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopier adresse til udklipsholder"
|
"message": "Kopier adresse til udklipsholder"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Dette er din private nøgle (klik for at kopiere)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopiér til udklipsholderen"
|
"message": "Kopiér til udklipsholderen"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Fejl i ENS-navneregistrering"
|
"message": "Fejl i ENS-navneregistrering"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Indtast kodeord"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Indtast adgangskode for at fortsætte"
|
"message": "Indtast adgangskode for at fortsætte"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Udvis Visning"
|
"message": "Udvis Visning"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksporter privat nøgle"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Mislykkedes"
|
"message": "Mislykkedes"
|
||||||
},
|
},
|
||||||
@ -641,9 +632,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Vælg dette for at vise hex-datafeltet på send-skærmen"
|
"message": "Vælg dette for at vise hex-datafeltet på send-skærmen"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Vis private nøgler"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Signaturforespørgsel"
|
"message": "Signaturforespørgsel"
|
||||||
},
|
},
|
||||||
@ -755,9 +743,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Prøv igen"
|
"message": "Prøv igen"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Skriv din MetaMask-adgangskode"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Ikke godkendt"
|
"message": "Ikke godkendt"
|
||||||
},
|
},
|
||||||
|
1136
app/_locales/de/messages.json
generated
1136
app/_locales/de/messages.json
generated
File diff suppressed because it is too large
Load Diff
1169
app/_locales/el/messages.json
generated
1169
app/_locales/el/messages.json
generated
File diff suppressed because it is too large
Load Diff
77
app/_locales/en/messages.json
generated
77
app/_locales/en/messages.json
generated
@ -287,6 +287,9 @@
|
|||||||
"address": {
|
"address": {
|
||||||
"message": "Address"
|
"message": "Address"
|
||||||
},
|
},
|
||||||
|
"addressCopied": {
|
||||||
|
"message": "Address copied!"
|
||||||
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"message": "Advanced"
|
"message": "Advanced"
|
||||||
},
|
},
|
||||||
@ -834,6 +837,9 @@
|
|||||||
"connectionRequest": {
|
"connectionRequest": {
|
||||||
"message": "Connection request"
|
"message": "Connection request"
|
||||||
},
|
},
|
||||||
|
"connections": {
|
||||||
|
"message": "Connections"
|
||||||
|
},
|
||||||
"contactUs": {
|
"contactUs": {
|
||||||
"message": "Contact us"
|
"message": "Contact us"
|
||||||
},
|
},
|
||||||
@ -898,9 +904,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copy address to clipboard"
|
"message": "Copy address to clipboard"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "This is your private key (click to copy)"
|
|
||||||
},
|
|
||||||
"copyRawTransactionData": {
|
"copyRawTransactionData": {
|
||||||
"message": "Copy raw transaction data"
|
"message": "Copy raw transaction data"
|
||||||
},
|
},
|
||||||
@ -1482,9 +1485,6 @@
|
|||||||
"enterOptionalPassword": {
|
"enterOptionalPassword": {
|
||||||
"message": "Enter optional password"
|
"message": "Enter optional password"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Enter password"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Enter password to continue"
|
"message": "Enter password to continue"
|
||||||
},
|
},
|
||||||
@ -1561,11 +1561,8 @@
|
|||||||
"exploreMetaMaskSnaps": {
|
"exploreMetaMaskSnaps": {
|
||||||
"message": "Explore MetaMask Snaps"
|
"message": "Explore MetaMask Snaps"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Export private key"
|
|
||||||
},
|
|
||||||
"extendWalletWithSnaps": {
|
"extendWalletWithSnaps": {
|
||||||
"message": "Extend the wallet experience."
|
"message": "Customize your wallet experience."
|
||||||
},
|
},
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "External extension"
|
"message": "External extension"
|
||||||
@ -2870,7 +2867,7 @@
|
|||||||
"message": "👓 We are making transactions easier to read."
|
"message": "👓 We are making transactions easier to read."
|
||||||
},
|
},
|
||||||
"notificationsEmptyText": {
|
"notificationsEmptyText": {
|
||||||
"message": "Nothing to see here."
|
"message": "This is where you can find notifications from your installed snaps."
|
||||||
},
|
},
|
||||||
"notificationsHeader": {
|
"notificationsHeader": {
|
||||||
"message": "Notifications"
|
"message": "Notifications"
|
||||||
@ -3019,10 +3016,6 @@
|
|||||||
"onboardingPinExtensionTitle": {
|
"onboardingPinExtensionTitle": {
|
||||||
"message": "Your MetaMask install is complete!"
|
"message": "Your MetaMask install is complete!"
|
||||||
},
|
},
|
||||||
"onboardingShowIncomingTransactionsDescription": {
|
|
||||||
"message": "Showing incoming transactions in your wallet relies on communication with $1. Etherscan will have access to your Ethereum address and your IP address. View $2.",
|
|
||||||
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
|
|
||||||
},
|
|
||||||
"onboardingUsePhishingDetectionDescription": {
|
"onboardingUsePhishingDetectionDescription": {
|
||||||
"message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.",
|
"message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.",
|
||||||
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
|
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
|
||||||
@ -3872,9 +3865,6 @@
|
|||||||
"showPrivateKey": {
|
"showPrivateKey": {
|
||||||
"message": "Show private key"
|
"message": "Show private key"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Show Private Keys"
|
|
||||||
},
|
|
||||||
"showTestnetNetworks": {
|
"showTestnetNetworks": {
|
||||||
"message": "Show test networks"
|
"message": "Show test networks"
|
||||||
},
|
},
|
||||||
@ -4377,6 +4367,9 @@
|
|||||||
"swap": {
|
"swap": {
|
||||||
"message": "Swap"
|
"message": "Swap"
|
||||||
},
|
},
|
||||||
|
"swapAdjustSlippage": {
|
||||||
|
"message": "Adjust slippage"
|
||||||
|
},
|
||||||
"swapAggregator": {
|
"swapAggregator": {
|
||||||
"message": "Aggregator"
|
"message": "Aggregator"
|
||||||
},
|
},
|
||||||
@ -4435,9 +4428,6 @@
|
|||||||
"swapEditLimit": {
|
"swapEditLimit": {
|
||||||
"message": "Edit limit"
|
"message": "Edit limit"
|
||||||
},
|
},
|
||||||
"swapEditTransactionSettings": {
|
|
||||||
"message": "Edit transaction settings"
|
|
||||||
},
|
|
||||||
"swapEnableDescription": {
|
"swapEnableDescription": {
|
||||||
"message": "This is required and gives MetaMask permission to swap your $1.",
|
"message": "This is required and gives MetaMask permission to swap your $1.",
|
||||||
"description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps."
|
"description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps."
|
||||||
@ -4498,6 +4488,9 @@
|
|||||||
"message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.",
|
"message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.",
|
||||||
"description": "$1 is the selected network, e.g. Ethereum or BSC"
|
"description": "$1 is the selected network, e.g. Ethereum or BSC"
|
||||||
},
|
},
|
||||||
|
"swapHighSlippage": {
|
||||||
|
"message": "High slippage"
|
||||||
|
},
|
||||||
"swapHighSlippageWarning": {
|
"swapHighSlippageWarning": {
|
||||||
"message": "Slippage amount is very high."
|
"message": "Slippage amount is very high."
|
||||||
},
|
},
|
||||||
@ -4512,6 +4505,9 @@
|
|||||||
"swapLearnMore": {
|
"swapLearnMore": {
|
||||||
"message": "Learn more about Swaps"
|
"message": "Learn more about Swaps"
|
||||||
},
|
},
|
||||||
|
"swapLowSlippage": {
|
||||||
|
"message": "Low slippage"
|
||||||
|
},
|
||||||
"swapLowSlippageError": {
|
"swapLowSlippageError": {
|
||||||
"message": "Transaction may fail, max slippage too low."
|
"message": "Transaction may fail, max slippage too low."
|
||||||
},
|
},
|
||||||
@ -4622,6 +4618,20 @@
|
|||||||
"swapShowLatestQuotes": {
|
"swapShowLatestQuotes": {
|
||||||
"message": "Show latest quotes"
|
"message": "Show latest quotes"
|
||||||
},
|
},
|
||||||
|
"swapSlippageHighDescription": {
|
||||||
|
"message": "The slippage entered ($1%) is considered very high and may result in a bad rate",
|
||||||
|
"description": "$1 is the amount of % for slippage"
|
||||||
|
},
|
||||||
|
"swapSlippageHighTitle": {
|
||||||
|
"message": "High slippage"
|
||||||
|
},
|
||||||
|
"swapSlippageLowDescription": {
|
||||||
|
"message": "A value this low ($1%) may result in a failed swap",
|
||||||
|
"description": "$1 is the amount of % for slippage"
|
||||||
|
},
|
||||||
|
"swapSlippageLowTitle": {
|
||||||
|
"message": "Low slippage"
|
||||||
|
},
|
||||||
"swapSlippageNegative": {
|
"swapSlippageNegative": {
|
||||||
"message": "Slippage must be greater or equal to zero"
|
"message": "Slippage must be greater or equal to zero"
|
||||||
},
|
},
|
||||||
@ -4635,27 +4645,15 @@
|
|||||||
"message": "Slippage tolerance must be 15% or less. Anything higher will result in a bad rate."
|
"message": "Slippage tolerance must be 15% or less. Anything higher will result in a bad rate."
|
||||||
},
|
},
|
||||||
"swapSlippageOverLimitTitle": {
|
"swapSlippageOverLimitTitle": {
|
||||||
"message": "Reduce slippage to continue"
|
"message": "Very high slippage"
|
||||||
},
|
},
|
||||||
"swapSlippagePercent": {
|
"swapSlippagePercent": {
|
||||||
"message": "$1%",
|
"message": "$1%",
|
||||||
"description": "$1 is the amount of % for slippage"
|
"description": "$1 is the amount of % for slippage"
|
||||||
},
|
},
|
||||||
"swapSlippageTooLowDescription": {
|
|
||||||
"message": "Max slippage is too low which may cause your transaction to fail."
|
|
||||||
},
|
|
||||||
"swapSlippageTooLowTitle": {
|
|
||||||
"message": "Increase slippage to avoid transaction failure"
|
|
||||||
},
|
|
||||||
"swapSlippageTooltip": {
|
"swapSlippageTooltip": {
|
||||||
"message": "If the price changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “slippage tolerance” setting."
|
"message": "If the price changes between the time your order is placed and confirmed it’s called “slippage”. Your swap will automatically cancel if slippage exceeds your “slippage tolerance” setting."
|
||||||
},
|
},
|
||||||
"swapSlippageVeryHighDescription": {
|
|
||||||
"message": "The slippage entered is considered very high and may result in a bad rate"
|
|
||||||
},
|
|
||||||
"swapSlippageVeryHighTitle": {
|
|
||||||
"message": "Very high slippage"
|
|
||||||
},
|
|
||||||
"swapSlippageZeroDescription": {
|
"swapSlippageZeroDescription": {
|
||||||
"message": "There are fewer zero-slippage quote providers which will result in a less competitive quote."
|
"message": "There are fewer zero-slippage quote providers which will result in a less competitive quote."
|
||||||
},
|
},
|
||||||
@ -5134,9 +5132,6 @@
|
|||||||
"txInsightsNotSupported": {
|
"txInsightsNotSupported": {
|
||||||
"message": "Transaction insights not supported for this contract at this time."
|
"message": "Transaction insights not supported for this contract at this time."
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Type your MetaMask password"
|
|
||||||
},
|
|
||||||
"typeYourSRP": {
|
"typeYourSRP": {
|
||||||
"message": "Type your Secret Recovery Phrase"
|
"message": "Type your Secret Recovery Phrase"
|
||||||
},
|
},
|
||||||
@ -5211,7 +5206,7 @@
|
|||||||
"message": "Decode smart contracts"
|
"message": "Decode smart contracts"
|
||||||
},
|
},
|
||||||
"use4ByteResolutionDescription": {
|
"use4ByteResolutionDescription": {
|
||||||
"message": "To improve user experience, we customize the activity tab with messages based on the smart contracts you interact with. MetaMask uses a service called 4byte.directory to decode data and show you a version of a smart contact that's easier to read. This helps reduce your chances of approving malicious smart contract actions, but can result in your IP address being shared."
|
"message": "To improve user experience, we customize the activity tab with messages based on the smart contracts you interact with. MetaMask uses a service called 4byte.directory to decode data and show you a version of a smart contract that's easier to read. This helps reduce your chances of approving malicious smart contract actions, but can result in your IP address being shared."
|
||||||
},
|
},
|
||||||
"useMultiAccountBalanceChecker": {
|
"useMultiAccountBalanceChecker": {
|
||||||
"message": "Batch account balance requests"
|
"message": "Batch account balance requests"
|
||||||
@ -5324,6 +5319,9 @@
|
|||||||
"visitWebSite": {
|
"visitWebSite": {
|
||||||
"message": "Visit our website"
|
"message": "Visit our website"
|
||||||
},
|
},
|
||||||
|
"wallet": {
|
||||||
|
"message": "Wallet"
|
||||||
|
},
|
||||||
"walletConnectionGuide": {
|
"walletConnectionGuide": {
|
||||||
"message": "our hardware wallet connection guide"
|
"message": "our hardware wallet connection guide"
|
||||||
},
|
},
|
||||||
@ -5351,8 +5349,7 @@
|
|||||||
"message": "Want to add this network?"
|
"message": "Want to add this network?"
|
||||||
},
|
},
|
||||||
"wantsToAddThisAsset": {
|
"wantsToAddThisAsset": {
|
||||||
"message": "$1 wants to add this asset to your wallet",
|
"message": "This allows the following asset to be added to your wallet."
|
||||||
"description": "$1 is the name of the website that wants to add an asset to your wallet"
|
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
"message": "Warning"
|
"message": "Warning"
|
||||||
|
1176
app/_locales/es/messages.json
generated
1176
app/_locales/es/messages.json
generated
File diff suppressed because it is too large
Load Diff
23
app/_locales/es_419/messages.json
generated
23
app/_locales/es_419/messages.json
generated
@ -475,9 +475,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copiar dirección al Portapapeles"
|
"message": "Copiar dirección al Portapapeles"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Esta es su clave privada (haga clic para copiarla)"
|
|
||||||
},
|
|
||||||
"copyRawTransactionData": {
|
"copyRawTransactionData": {
|
||||||
"message": "Copiar los datos de las transacciones en bruto"
|
"message": "Copiar los datos de las transacciones en bruto"
|
||||||
},
|
},
|
||||||
@ -767,9 +764,6 @@
|
|||||||
"enterMaxSpendLimit": {
|
"enterMaxSpendLimit": {
|
||||||
"message": "Escribir límite máximo de gastos"
|
"message": "Escribir límite máximo de gastos"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Escribir contraseña"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Escribir contraseña para continuar"
|
"message": "Escribir contraseña para continuar"
|
||||||
},
|
},
|
||||||
@ -826,9 +820,6 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"message": "Experimental"
|
"message": "Experimental"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportar clave privada"
|
|
||||||
},
|
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "Extensión externa"
|
"message": "Extensión externa"
|
||||||
},
|
},
|
||||||
@ -1623,14 +1614,6 @@
|
|||||||
"onboardingPinExtensionTitle": {
|
"onboardingPinExtensionTitle": {
|
||||||
"message": "¡Su instalación de MetaMask ha finalizado!"
|
"message": "¡Su instalación de MetaMask ha finalizado!"
|
||||||
},
|
},
|
||||||
"onboardingShowIncomingTransactionsDescription": {
|
|
||||||
"message": "Mostrar las transacciones entrantes en su cartera depende de la comunicación con $1. Etherscan tendrá acceso a su dirección de Ethereum y a su dirección IP. Ver $2.",
|
|
||||||
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
|
|
||||||
},
|
|
||||||
"onboardingUsePhishingDetectionDescription": {
|
|
||||||
"message": "Las alertas de detección de phishing se basan en la comunicación con $1. jsDeliver tendrá acceso a su dirección IP. Ver $2.",
|
|
||||||
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
|
|
||||||
},
|
|
||||||
"onlyAddTrustedNetworks": {
|
"onlyAddTrustedNetworks": {
|
||||||
"message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza."
|
"message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza."
|
||||||
},
|
},
|
||||||
@ -2020,9 +2003,6 @@
|
|||||||
"showPermissions": {
|
"showPermissions": {
|
||||||
"message": "Mostrar permisos"
|
"message": "Mostrar permisos"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Mostrar claves privadas"
|
|
||||||
},
|
|
||||||
"showTestnetNetworks": {
|
"showTestnetNetworks": {
|
||||||
"message": "Mostrar redes de prueba"
|
"message": "Mostrar redes de prueba"
|
||||||
},
|
},
|
||||||
@ -2637,9 +2617,6 @@
|
|||||||
"txInsightsNotSupported": {
|
"txInsightsNotSupported": {
|
||||||
"message": "En este momento no se admiten informaciones sobre las transacciones para este contrato."
|
"message": "En este momento no se admiten informaciones sobre las transacciones para este contrato."
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Escriba su contraseña de MetaMask"
|
|
||||||
},
|
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"message": "U2F",
|
"message": "U2F",
|
||||||
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
|
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
|
||||||
|
15
app/_locales/et/messages.json
generated
15
app/_locales/et/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopeeri aadress lõikelauale"
|
"message": "Kopeeri aadress lõikelauale"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "See on teie privaatne võti (klõpsake kopeerimiseks)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopeeri lõikelauale"
|
"message": "Kopeeri lõikelauale"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Tõrge ENS-i nime registreerimisel"
|
"message": "Tõrge ENS-i nime registreerimisel"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Sisestage parool"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Jätkamiseks sisestage parool"
|
"message": "Jätkamiseks sisestage parool"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Laienda vaadet"
|
"message": "Laienda vaadet"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Ekspordi privaatvõti"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Nurjus"
|
"message": "Nurjus"
|
||||||
},
|
},
|
||||||
@ -653,9 +644,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Valige see, et kuvada saatmisekraanil hex-andmete väli"
|
"message": "Valige see, et kuvada saatmisekraanil hex-andmete väli"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Kuva privaatvõtmed"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Allkirja taotlus"
|
"message": "Allkirja taotlus"
|
||||||
},
|
},
|
||||||
@ -776,9 +764,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Proovi uuesti"
|
"message": "Proovi uuesti"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Sisestage oma MetaMaski parool"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Kinnitamata"
|
"message": "Kinnitamata"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/fa/messages.json
generated
15
app/_locales/fa/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "کاپی آدرس به کلیپ بورد"
|
"message": "کاپی آدرس به کلیپ بورد"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "این کلید خصوصی شما است (برای کاپی نمودن کلیک کنید)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "کپی در بریدهدان"
|
"message": "کپی در بریدهدان"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "خطا در ثبت نام ENS"
|
"message": "خطا در ثبت نام ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "رمز عبور را وارد کنید"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "برای ادامه رمز عبور را وارد کنید"
|
"message": "برای ادامه رمز عبور را وارد کنید"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "توسعه ساحه دید"
|
"message": "توسعه ساحه دید"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "صدور کلید شخصی"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "ناموفق شد"
|
"message": "ناموفق شد"
|
||||||
},
|
},
|
||||||
@ -663,9 +654,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "برای نمایش بخش اطلاعات hex در صفحه ارسال این را انتخاب نمایید"
|
"message": "برای نمایش بخش اطلاعات hex در صفحه ارسال این را انتخاب نمایید"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "نمایش کلید های شخصی"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "درخواست امضاء"
|
"message": "درخواست امضاء"
|
||||||
},
|
},
|
||||||
@ -786,9 +774,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "امتحان مجدد"
|
"message": "امتحان مجدد"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "رمز عبور MetaMask تان را تایپ نمایید"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "تصدیق ناشده"
|
"message": "تصدیق ناشده"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/fi/messages.json
generated
15
app/_locales/fi/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopioi osoite leikepöydälle"
|
"message": "Kopioi osoite leikepöydälle"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Tämä on yksityinen avaimesi (kopioi napsauttamalla)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopioi leikepöydälle"
|
"message": "Kopioi leikepöydälle"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Virhe ENS-nimen rekisteröinnissä"
|
"message": "Virhe ENS-nimen rekisteröinnissä"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Kirjoita salasana"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Syötä salasana voidaksesi jatkaa"
|
"message": "Syötä salasana voidaksesi jatkaa"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Laajenna näkymää"
|
"message": "Laajenna näkymää"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Vie yksityinen avain"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Epäonnistui"
|
"message": "Epäonnistui"
|
||||||
},
|
},
|
||||||
@ -660,9 +651,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Näytä hex-tietokenttä lähetysnäytössä valitsemalla tämän"
|
"message": "Näytä hex-tietokenttä lähetysnäytössä valitsemalla tämän"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Näytä yksityiset avaimet"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Allekirjoitus pyydetään"
|
"message": "Allekirjoitus pyydetään"
|
||||||
},
|
},
|
||||||
@ -783,9 +771,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Yritä uudelleen"
|
"message": "Yritä uudelleen"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Kirjoita MetaMask-salasanasi"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Ei hyväksytty"
|
"message": "Ei hyväksytty"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/fil/messages.json
generated
15
app/_locales/fil/messages.json
generated
@ -154,9 +154,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopyahin ang address sa clipboard"
|
"message": "Kopyahin ang address sa clipboard"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ito ang iyong pribadong private key (i-click para kopyahin)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopyahin sa clipboard"
|
"message": "Kopyahin sa clipboard"
|
||||||
},
|
},
|
||||||
@ -217,9 +214,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "May error sa pagrerehistro ng ENS name"
|
"message": "May error sa pagrerehistro ng ENS name"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Ilagay ang password"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Ilagay ang password para magpatuloy"
|
"message": "Ilagay ang password para magpatuloy"
|
||||||
},
|
},
|
||||||
@ -229,9 +223,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "I-expand ang View"
|
"message": "I-expand ang View"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "I-export ang Private Key"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Nabigo"
|
"message": "Nabigo"
|
||||||
},
|
},
|
||||||
@ -587,9 +578,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Piliin ito para ipakita ang hex data field sa screen ng pagpapadala"
|
"message": "Piliin ito para ipakita ang hex data field sa screen ng pagpapadala"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Ipakita ang mga Private Key"
|
|
||||||
},
|
|
||||||
"sign": {
|
"sign": {
|
||||||
"message": "I-sign"
|
"message": "I-sign"
|
||||||
},
|
},
|
||||||
@ -698,9 +686,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Subukang muli"
|
"message": "Subukang muli"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "I-type ang iyong password sa MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Hindi inaprubahan"
|
"message": "Hindi inaprubahan"
|
||||||
},
|
},
|
||||||
|
1162
app/_locales/fr/messages.json
generated
1162
app/_locales/fr/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/he/messages.json
generated
15
app/_locales/he/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "העתק כתובת ללוח"
|
"message": "העתק כתובת ללוח"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "זה המפתח הפרטי שלך (נא להקיש כדי להעתיק)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "העתק ללוח"
|
"message": "העתק ללוח"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "שגיאה ברישום שם ENS"
|
"message": "שגיאה ברישום שם ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "יש להזין ססמה"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "יש להזין ססמה כדי להמשיך"
|
"message": "יש להזין ססמה כדי להמשיך"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "הרחב תצוגה"
|
"message": "הרחב תצוגה"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "יצא/י מפתח פרטי"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "נכשל"
|
"message": "נכשל"
|
||||||
},
|
},
|
||||||
@ -660,9 +651,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "בחר/י בזה כדי להציג את שדה הנתונים ההקסדצימאלים על מסך השליחה"
|
"message": "בחר/י בזה כדי להציג את שדה הנתונים ההקסדצימאלים על מסך השליחה"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "הצג מפתחות פרטיים"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "בקשת חתימה"
|
"message": "בקשת חתימה"
|
||||||
},
|
},
|
||||||
@ -783,9 +771,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "ניסיון חוזר"
|
"message": "ניסיון חוזר"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "נא להקליד את סיסמת MetaMask שלך"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "לא אושר"
|
"message": "לא אושר"
|
||||||
},
|
},
|
||||||
|
1160
app/_locales/hi/messages.json
generated
1160
app/_locales/hi/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/hn/messages.json
generated
15
app/_locales/hn/messages.json
generated
@ -60,9 +60,6 @@
|
|||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "कॉपी कर दिया गया!"
|
"message": "कॉपी कर दिया गया!"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "यह आपकी निजी कुंजी है (कॉपी करने के लिए क्लिक करें)।"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "क्लिपबोर्ड पर कॉपी करें"
|
"message": "क्लिपबोर्ड पर कॉपी करें"
|
||||||
},
|
},
|
||||||
@ -87,15 +84,9 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "संपादित करें"
|
"message": "संपादित करें"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "पासवर्ड दर्ज करें"
|
|
||||||
},
|
|
||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "ईथरस्कैन पर खाता देखें"
|
"message": "ईथरस्कैन पर खाता देखें"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "निजी कुंजी निर्यात करें"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "विफल"
|
"message": "विफल"
|
||||||
},
|
},
|
||||||
@ -281,9 +272,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"message": "सेटिंग्स"
|
"message": "सेटिंग्स"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "निजी कुंजी दिखाएँ"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "हस्ताक्षर अनुरोध"
|
"message": "हस्ताक्षर अनुरोध"
|
||||||
},
|
},
|
||||||
@ -317,9 +305,6 @@
|
|||||||
"total": {
|
"total": {
|
||||||
"message": "कुल"
|
"message": "कुल"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "अपना पासवर्ड टाइप करें"
|
|
||||||
},
|
|
||||||
"unknown": {
|
"unknown": {
|
||||||
"message": "अज्ञात नेटवर्क"
|
"message": "अज्ञात नेटवर्क"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/hr/messages.json
generated
15
app/_locales/hr/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopiraj adresu u međuspremnik"
|
"message": "Kopiraj adresu u međuspremnik"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ovo je vaš privatni ključ (kliknite za kopiranje)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopiraj u međuspremnik"
|
"message": "Kopiraj u međuspremnik"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Greška u registraciji naziva ENS"
|
"message": "Greška u registraciji naziva ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Upiši lozinku"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Upišite lozinku za nastavak"
|
"message": "Upišite lozinku za nastavak"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Proširi prikaz"
|
"message": "Proširi prikaz"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Izvezi privatni ključ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Neuspješno"
|
"message": "Neuspješno"
|
||||||
},
|
},
|
||||||
@ -656,9 +647,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Odaberite ovu stavku za prikaz polja namijenjenog za podatke hex na zaslonu za slanje"
|
"message": "Odaberite ovu stavku za prikaz polja namijenjenog za podatke hex na zaslonu za slanje"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Prikaži privatne ključe"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Zahtjev za potpisom"
|
"message": "Zahtjev za potpisom"
|
||||||
},
|
},
|
||||||
@ -776,9 +764,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Pokušaj ponovo"
|
"message": "Pokušaj ponovo"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Upišite svoju lozinku MetaMask."
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neodobreno"
|
"message": "Neodobreno"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ht/messages.json
generated
15
app/_locales/ht/messages.json
generated
@ -108,9 +108,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopi adrès clipboard"
|
"message": "Kopi adrès clipboard"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Sa a se kle prive ou (klike pou ou kopye)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopi clipboard"
|
"message": "Kopi clipboard"
|
||||||
},
|
},
|
||||||
@ -147,9 +144,6 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "Korije"
|
"message": "Korije"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Mete modpas"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Mete modpas pou kontinye"
|
"message": "Mete modpas pou kontinye"
|
||||||
},
|
},
|
||||||
@ -159,9 +153,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Elaji Wè"
|
"message": "Elaji Wè"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Voye Kòd Prive"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Tonbe"
|
"message": "Tonbe"
|
||||||
},
|
},
|
||||||
@ -482,9 +473,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Pran sa pouw ka montre chan entèfas hex data a"
|
"message": "Pran sa pouw ka montre chan entèfas hex data a"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Montre Kle Prive"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Demann Siyati"
|
"message": "Demann Siyati"
|
||||||
},
|
},
|
||||||
@ -554,9 +542,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Eseye anko"
|
"message": "Eseye anko"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Tape modpas ou"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Pa apwouve"
|
"message": "Pa apwouve"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/hu/messages.json
generated
15
app/_locales/hu/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Másolja a címet a vágólapra"
|
"message": "Másolja a címet a vágólapra"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ez a saját titkos kulcsod (kattints rá a másoláshoz)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Másolás a vágólapra"
|
"message": "Másolás a vágólapra"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Hiba történt az ENS név regisztrációjakor"
|
"message": "Hiba történt az ENS név regisztrációjakor"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Adja meg a jelszót"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "A folytatáshoz adja meg a jelszót"
|
"message": "A folytatáshoz adja meg a jelszót"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Nézet nagyítása"
|
"message": "Nézet nagyítása"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Privát kulcs exportálása"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Sikertelen"
|
"message": "Sikertelen"
|
||||||
},
|
},
|
||||||
@ -656,9 +647,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Válassza ezt, ha a hex adatmezőt a küldő képernyőn szeretné megnézni"
|
"message": "Válassza ezt, ha a hex adatmezőt a küldő képernyőn szeretné megnézni"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Mutassa a privát kulcsokat"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Aláírás kérése"
|
"message": "Aláírás kérése"
|
||||||
},
|
},
|
||||||
@ -776,9 +764,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Újra"
|
"message": "Újra"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Írd be MetaMask jelszavadat"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Jóvá nem hagyott"
|
"message": "Jóvá nem hagyott"
|
||||||
},
|
},
|
||||||
|
1162
app/_locales/id/messages.json
generated
1162
app/_locales/id/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/it/messages.json
generated
15
app/_locales/it/messages.json
generated
@ -579,9 +579,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copia l'indirizzo"
|
"message": "Copia l'indirizzo"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Questa è la tua chiave privata (clicca per copiare)"
|
|
||||||
},
|
|
||||||
"copyRawTransactionData": {
|
"copyRawTransactionData": {
|
||||||
"message": "Copia i dati grezzi della transazione"
|
"message": "Copia i dati grezzi della transazione"
|
||||||
},
|
},
|
||||||
@ -840,9 +837,6 @@
|
|||||||
"enterMaxSpendLimit": {
|
"enterMaxSpendLimit": {
|
||||||
"message": "Inserisici Limite Spesa"
|
"message": "Inserisici Limite Spesa"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Inserisci password"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Inserisci la tua password per continuare"
|
"message": "Inserisci la tua password per continuare"
|
||||||
},
|
},
|
||||||
@ -875,9 +869,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Espandi Vista"
|
"message": "Espandi Vista"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Esporta Chiave Privata"
|
|
||||||
},
|
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "Estensione Esterna"
|
"message": "Estensione Esterna"
|
||||||
},
|
},
|
||||||
@ -1439,9 +1430,6 @@
|
|||||||
"showPermissions": {
|
"showPermissions": {
|
||||||
"message": "Mostra permessi"
|
"message": "Mostra permessi"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Mostra Chiave Privata"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Firma Richiesta"
|
"message": "Firma Richiesta"
|
||||||
},
|
},
|
||||||
@ -1794,9 +1782,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Prova di nuovo"
|
"message": "Prova di nuovo"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Inserisci Password"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Non approvata"
|
"message": "Non approvata"
|
||||||
},
|
},
|
||||||
|
1158
app/_locales/ja/messages.json
generated
1158
app/_locales/ja/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/kn/messages.json
generated
15
app/_locales/kn/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "ವಿಳಾಸವನ್ನು ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
"message": "ವಿಳಾಸವನ್ನು ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "ಇದು ನಿಮ್ಮ ಖಾಸಗಿ ಕೀ ಆಗಿದೆ (ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
"message": "ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "ENS ಹೆಸರಿನ ನೋಂದಣಿಯಲ್ಲಿ ದೋಷ"
|
"message": "ENS ಹೆಸರಿನ ನೋಂದಣಿಯಲ್ಲಿ ದೋಷ"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "ಮುಂದುವರೆಯಲು ಪಾಸ್ವರ್ಡ್ ನಮೂದಿಸಿ"
|
"message": "ಮುಂದುವರೆಯಲು ಪಾಸ್ವರ್ಡ್ ನಮೂದಿಸಿ"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "ವಿಸ್ತರಿಸಿದ ವೀಕ್ಷಣೆ"
|
"message": "ವಿಸ್ತರಿಸಿದ ವೀಕ್ಷಣೆ"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "ಖಾಸಗಿ ಕೀಲಿಯನ್ನು ರಫ್ತು ಮಾಡಿ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "ವಿಫಲವಾಗಿದೆ"
|
"message": "ವಿಫಲವಾಗಿದೆ"
|
||||||
},
|
},
|
||||||
@ -663,9 +654,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "ಕಳುಹಿಸುವ ಪರದೆಯಲ್ಲಿ ಹೆಕ್ಸ್ ಡೇಟಾ ಕ್ಷೇತ್ರವನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ"
|
"message": "ಕಳುಹಿಸುವ ಪರದೆಯಲ್ಲಿ ಹೆಕ್ಸ್ ಡೇಟಾ ಕ್ಷೇತ್ರವನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "ಖಾಸಗಿ ಕೀಗಳನ್ನು ತೋರಿಸಿ"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "ಸಹಿಯ ವಿನಂತಿ"
|
"message": "ಸಹಿಯ ವಿನಂತಿ"
|
||||||
},
|
},
|
||||||
@ -786,9 +774,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"
|
"message": "ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "ನಿಮ್ಮ MetaMask ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಟೈಪ್ ಮಾಡಿ"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "ಅನುಮೋದಿಸದಿರುವುದು"
|
"message": "ಅನುಮೋದಿಸದಿರುವುದು"
|
||||||
},
|
},
|
||||||
|
1200
app/_locales/ko/messages.json
generated
1200
app/_locales/ko/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/lt/messages.json
generated
15
app/_locales/lt/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopijuoti adresą į iškarpinę"
|
"message": "Kopijuoti adresą į iškarpinę"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Tai yra jūsų asmeninis raktas (spustelėkite, kad nukopijuotumėte)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopijuoti į iškarpinę"
|
"message": "Kopijuoti į iškarpinę"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "ENS pavadinimo registracijos klaida"
|
"message": "ENS pavadinimo registracijos klaida"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Įveskite slaptažodį"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Norėdami tęsti, įveskite slaptažodį"
|
"message": "Norėdami tęsti, įveskite slaptažodį"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Išskleisti rodinį"
|
"message": "Išskleisti rodinį"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksportuoti asmeninį raktą"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Nepavyko"
|
"message": "Nepavyko"
|
||||||
},
|
},
|
||||||
@ -663,9 +654,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Pasirinkite tai, kad siuntimo ekrane būtų rodomas šešioliktainių duomenų laukas"
|
"message": "Pasirinkite tai, kad siuntimo ekrane būtų rodomas šešioliktainių duomenų laukas"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Rodyti asmeninius raktus"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Parašo užklausa"
|
"message": "Parašo užklausa"
|
||||||
},
|
},
|
||||||
@ -786,9 +774,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Bandyti dar kartą"
|
"message": "Bandyti dar kartą"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Įveskite savo „MetaMask“ slaptažodį"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Nepatvirtinta"
|
"message": "Nepatvirtinta"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/lv/messages.json
generated
15
app/_locales/lv/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Iekopēt adresi starpliktuvē"
|
"message": "Iekopēt adresi starpliktuvē"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Šī ir jūsu privātā atslēga (noklikšķiniet, lai nokopētu)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopēt starpliktuvē"
|
"message": "Kopēt starpliktuvē"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Kļūda ENS vārda reģistrācijā"
|
"message": "Kļūda ENS vārda reģistrācijā"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Ievadiet paroli"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Ievadiet paroli, lai turpinātu"
|
"message": "Ievadiet paroli, lai turpinātu"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Izvērst skatījumu"
|
"message": "Izvērst skatījumu"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksportēt privāto atslēgu"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Neizdevās"
|
"message": "Neizdevās"
|
||||||
},
|
},
|
||||||
@ -659,9 +650,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Atlasiet šo, lai atvērtu hex datus sūtīšanas ekrānā"
|
"message": "Atlasiet šo, lai atvērtu hex datus sūtīšanas ekrānā"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Rādīt privātās atslēgas"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Paraksta pieprasījums"
|
"message": "Paraksta pieprasījums"
|
||||||
},
|
},
|
||||||
@ -782,9 +770,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Mēģināt vēlreiz"
|
"message": "Mēģināt vēlreiz"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Ievadiet savu MetaMask paroli"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Nav apstiprināts"
|
"message": "Nav apstiprināts"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ms/messages.json
generated
15
app/_locales/ms/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Salin alamat kepada papan klip"
|
"message": "Salin alamat kepada papan klip"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ini kunci persendirian anda (klik untuk menyalin)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Salin ke papan keratan"
|
"message": "Salin ke papan keratan"
|
||||||
},
|
},
|
||||||
@ -238,9 +235,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Ralat dalam pendaftaran nama ENS"
|
"message": "Ralat dalam pendaftaran nama ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Masukkan kata laluan"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Masukkan kata laluan untuk teruskan"
|
"message": "Masukkan kata laluan untuk teruskan"
|
||||||
},
|
},
|
||||||
@ -253,9 +247,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Kembangkan Paparan"
|
"message": "Kembangkan Paparan"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksport Kekunci Persendirian"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Gagal"
|
"message": "Gagal"
|
||||||
},
|
},
|
||||||
@ -643,9 +634,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Pilih ini untuk menunjukkan medan data hex pada skrin hantar"
|
"message": "Pilih ini untuk menunjukkan medan data hex pada skrin hantar"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Tunjukkan Kunci Persendirian"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Permintaan Tandatangan"
|
"message": "Permintaan Tandatangan"
|
||||||
},
|
},
|
||||||
@ -763,9 +751,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Cuba lagi"
|
"message": "Cuba lagi"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Taip kata laluan MetaMask anda"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Belum Diluluskan"
|
"message": "Belum Diluluskan"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/nl/messages.json
generated
15
app/_locales/nl/messages.json
generated
@ -60,9 +60,6 @@
|
|||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "Gekopieerd!"
|
"message": "Gekopieerd!"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Dit is uw privésleutel (klik om te kopiëren)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopieer naar klembord"
|
"message": "Kopieer naar klembord"
|
||||||
},
|
},
|
||||||
@ -84,15 +81,9 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "Bewerk"
|
"message": "Bewerk"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Voer wachtwoord in"
|
|
||||||
},
|
|
||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "Bekijk account op Etherscan"
|
"message": "Bekijk account op Etherscan"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exporteer privésleutel"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "mislukt"
|
"message": "mislukt"
|
||||||
},
|
},
|
||||||
@ -271,9 +262,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"message": "instellingen"
|
"message": "instellingen"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Privésleutels weergeven"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Ondertekeningsverzoek"
|
"message": "Ondertekeningsverzoek"
|
||||||
},
|
},
|
||||||
@ -307,9 +295,6 @@
|
|||||||
"total": {
|
"total": {
|
||||||
"message": "Totaal"
|
"message": "Totaal"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Typ uw wachtwoord"
|
|
||||||
},
|
|
||||||
"unknown": {
|
"unknown": {
|
||||||
"message": "Onbekend"
|
"message": "Onbekend"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/no/messages.json
generated
15
app/_locales/no/messages.json
generated
@ -172,9 +172,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopier adresse til utklippstavlen "
|
"message": "Kopier adresse til utklippstavlen "
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Dette er din private nøkkel (klikk for å kopiere)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopiér til utklippstavlen"
|
"message": "Kopiér til utklippstavlen"
|
||||||
},
|
},
|
||||||
@ -238,9 +235,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Feil i ENS-navneregistrering"
|
"message": "Feil i ENS-navneregistrering"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Skriv inn passord "
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Skriv inn passord for å fortsette"
|
"message": "Skriv inn passord for å fortsette"
|
||||||
},
|
},
|
||||||
@ -253,9 +247,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Utvid visning"
|
"message": "Utvid visning"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksporter privat nøkkel"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Mislyktes"
|
"message": "Mislyktes"
|
||||||
},
|
},
|
||||||
@ -644,9 +635,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Velg dette for å vise hex-datafeltet på sendskjermen"
|
"message": "Velg dette for å vise hex-datafeltet på sendskjermen"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Vis private nøkler"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Signaturforespørsel "
|
"message": "Signaturforespørsel "
|
||||||
},
|
},
|
||||||
@ -761,9 +749,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Prøv igjen"
|
"message": "Prøv igjen"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Skriv inn MetaMask-passordet"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Ikke godkjent "
|
"message": "Ikke godkjent "
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ph/messages.json
generated
15
app/_locales/ph/messages.json
generated
@ -338,9 +338,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopyahin ang address sa clipboard"
|
"message": "Kopyahin ang address sa clipboard"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ito ang iyong pribadong key (i-click para kopyahin)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopyahin sa clipboard"
|
"message": "Kopyahin sa clipboard"
|
||||||
},
|
},
|
||||||
@ -489,9 +486,6 @@
|
|||||||
"enterMaxSpendLimit": {
|
"enterMaxSpendLimit": {
|
||||||
"message": "Ilagay ang Max na Limitasyon sa Paggastos"
|
"message": "Ilagay ang Max na Limitasyon sa Paggastos"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Ilagay ang password"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Ilagay ang password para magpatuloy"
|
"message": "Ilagay ang password para magpatuloy"
|
||||||
},
|
},
|
||||||
@ -542,9 +536,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "I-expand ang view"
|
"message": "I-expand ang view"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "I-export ang Pribadong Key"
|
|
||||||
},
|
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "External Extension"
|
"message": "External Extension"
|
||||||
},
|
},
|
||||||
@ -1309,9 +1300,6 @@
|
|||||||
"showPermissions": {
|
"showPermissions": {
|
||||||
"message": "Ipakita ang mga pahintulot"
|
"message": "Ipakita ang mga pahintulot"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Ipakita ang Mga Private Key"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Request ng Signature"
|
"message": "Request ng Signature"
|
||||||
},
|
},
|
||||||
@ -1764,9 +1752,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Subukan ulit"
|
"message": "Subukan ulit"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Uri ng password ng iyong MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Hindi inaprubahan"
|
"message": "Hindi inaprubahan"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/pl/messages.json
generated
15
app/_locales/pl/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Skopiuj adres do schowka"
|
"message": "Skopiuj adres do schowka"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "To jest Twój prywatny klucz (kliknij żeby skopiować)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Skopiuj do schowka"
|
"message": "Skopiuj do schowka"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Błąd w rejestracji nazwy ENS"
|
"message": "Błąd w rejestracji nazwy ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Wpisz hasło"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Podaj hasło żeby kontynuować"
|
"message": "Podaj hasło żeby kontynuować"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Rozwiń widok"
|
"message": "Rozwiń widok"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Eksportuj klucz prywatny"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Nie udało się"
|
"message": "Nie udało się"
|
||||||
},
|
},
|
||||||
@ -657,9 +648,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania"
|
"message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Pokaż prywatne klucze"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Prośba o podpis"
|
"message": "Prośba o podpis"
|
||||||
},
|
},
|
||||||
@ -774,9 +762,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Spróbuj ponownie"
|
"message": "Spróbuj ponownie"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Wpisz hasło"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Niezatwierdzone"
|
"message": "Niezatwierdzone"
|
||||||
},
|
},
|
||||||
|
1188
app/_locales/pt/messages.json
generated
1188
app/_locales/pt/messages.json
generated
File diff suppressed because it is too large
Load Diff
19
app/_locales/pt_BR/messages.json
generated
19
app/_locales/pt_BR/messages.json
generated
@ -475,9 +475,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copiar endereço para a área de transferência"
|
"message": "Copiar endereço para a área de transferência"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Essa é a sua chave privada (clique para copiar)"
|
|
||||||
},
|
|
||||||
"copyRawTransactionData": {
|
"copyRawTransactionData": {
|
||||||
"message": "Copiar dados brutos da transação"
|
"message": "Copiar dados brutos da transação"
|
||||||
},
|
},
|
||||||
@ -767,9 +764,6 @@
|
|||||||
"enterMaxSpendLimit": {
|
"enterMaxSpendLimit": {
|
||||||
"message": "Digite um limite máximo de gastos"
|
"message": "Digite um limite máximo de gastos"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Digite a senha"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Digite a senha para continuar"
|
"message": "Digite a senha para continuar"
|
||||||
},
|
},
|
||||||
@ -826,9 +820,6 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"message": "Experimental"
|
"message": "Experimental"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportar chave privada"
|
|
||||||
},
|
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "Extensão externa"
|
"message": "Extensão externa"
|
||||||
},
|
},
|
||||||
@ -1623,10 +1614,6 @@
|
|||||||
"onboardingPinExtensionTitle": {
|
"onboardingPinExtensionTitle": {
|
||||||
"message": "Sua instalação da MetaMask está concluída!"
|
"message": "Sua instalação da MetaMask está concluída!"
|
||||||
},
|
},
|
||||||
"onboardingShowIncomingTransactionsDescription": {
|
|
||||||
"message": "A exibição de transações recebidas na sua carteira depende de comunicação com $1. O Etherscan terá acesso ao seu endereço Ethereum e ao seu endereço IP. Veja $2.",
|
|
||||||
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
|
|
||||||
},
|
|
||||||
"onboardingUsePhishingDetectionDescription": {
|
"onboardingUsePhishingDetectionDescription": {
|
||||||
"message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.",
|
"message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.",
|
||||||
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
|
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
|
||||||
@ -2020,9 +2007,6 @@
|
|||||||
"showPermissions": {
|
"showPermissions": {
|
||||||
"message": "Mostrar permissões"
|
"message": "Mostrar permissões"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Mostrar chaves privadas"
|
|
||||||
},
|
|
||||||
"showTestnetNetworks": {
|
"showTestnetNetworks": {
|
||||||
"message": "Mostrar redes de teste"
|
"message": "Mostrar redes de teste"
|
||||||
},
|
},
|
||||||
@ -2637,9 +2621,6 @@
|
|||||||
"txInsightsNotSupported": {
|
"txInsightsNotSupported": {
|
||||||
"message": "As informações sobre transações não são suportadas para esse contrato, por ora."
|
"message": "As informações sobre transações não são suportadas para esse contrato, por ora."
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Digite sua senha da MetaMask"
|
|
||||||
},
|
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"message": "U2F",
|
"message": "U2F",
|
||||||
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
|
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."
|
||||||
|
15
app/_locales/ro/messages.json
generated
15
app/_locales/ro/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Copiere adresă în clipboard"
|
"message": "Copiere adresă în clipboard"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Aceasta este cheia dumneavoastră privată (clic pentru a copia)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Copiați în clipboard"
|
"message": "Copiați în clipboard"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Eroare la înregistrarea numelui ENS"
|
"message": "Eroare la înregistrarea numelui ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Introduceți parola"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Introduceți parola pentru a continua"
|
"message": "Introduceți parola pentru a continua"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Extindeți vizualizarea"
|
"message": "Extindeți vizualizarea"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportați cheia privată"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Eșuat"
|
"message": "Eșuat"
|
||||||
},
|
},
|
||||||
@ -650,9 +641,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Selectați această opțiune pentru a arăta câmpul de date hexazecimale în ecranul de trimitere."
|
"message": "Selectați această opțiune pentru a arăta câmpul de date hexazecimale în ecranul de trimitere."
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Afișați cheile private"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Solicitare de semnătură"
|
"message": "Solicitare de semnătură"
|
||||||
},
|
},
|
||||||
@ -767,9 +755,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Încearcă din nou"
|
"message": "Încearcă din nou"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Scrieți parola dvs. pentru MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neaprobat"
|
"message": "Neaprobat"
|
||||||
},
|
},
|
||||||
|
1164
app/_locales/ru/messages.json
generated
1164
app/_locales/ru/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/sk/messages.json
generated
15
app/_locales/sk/messages.json
generated
@ -169,9 +169,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopírovať adresu do schránky"
|
"message": "Kopírovať adresu do schránky"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopírovat do schránky"
|
"message": "Kopírovat do schránky"
|
||||||
},
|
},
|
||||||
@ -235,9 +232,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Chyba pri registrácii názvu ENS"
|
"message": "Chyba pri registrácii názvu ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Zadejte heslo"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Pokračujte zadaním hesla"
|
"message": "Pokračujte zadaním hesla"
|
||||||
},
|
},
|
||||||
@ -250,9 +244,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Rozbaliť zobrazenie"
|
"message": "Rozbaliť zobrazenie"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportovat privátní klíč"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Neúspěšné"
|
"message": "Neúspěšné"
|
||||||
},
|
},
|
||||||
@ -635,9 +626,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Vyberte toto, ak chcete, aby sa na obrazovke odosielania zobrazilo hex dátové pole"
|
"message": "Vyberte toto, ak chcete, aby sa na obrazovke odosielania zobrazilo hex dátové pole"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Zobrazit privátní klíče"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Požadavek podpisu"
|
"message": "Požadavek podpisu"
|
||||||
},
|
},
|
||||||
@ -752,9 +740,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Skúsiť znova"
|
"message": "Skúsiť znova"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Zadejte své heslo"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neschváleno"
|
"message": "Neschváleno"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/sl/messages.json
generated
15
app/_locales/sl/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopiraj naslov v odložišče"
|
"message": "Kopiraj naslov v odložišče"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "To je vaš zesebni ključ (kliknite za kopiranje)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopiraj v odložišče"
|
"message": "Kopiraj v odložišče"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Napaka pri registraciji imena ENS"
|
"message": "Napaka pri registraciji imena ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Vnesite geslo"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Za nadaljevanje vnesite geslo"
|
"message": "Za nadaljevanje vnesite geslo"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Razširi pogled"
|
"message": "Razširi pogled"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Izvozi zasebni ključ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Ni uspelo"
|
"message": "Ni uspelo"
|
||||||
},
|
},
|
||||||
@ -651,9 +642,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Izberite za prikaz hex podatkov na zaslonu za pošiljanje"
|
"message": "Izberite za prikaz hex podatkov na zaslonu za pošiljanje"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Pokaži zasebni ključ"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Zahteva za podpis"
|
"message": "Zahteva za podpis"
|
||||||
},
|
},
|
||||||
@ -774,9 +762,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Poskusi znova"
|
"message": "Poskusi znova"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Vnesite vaše MetaMask geslo"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neodobreno"
|
"message": "Neodobreno"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/sr/messages.json
generated
15
app/_locales/sr/messages.json
generated
@ -172,9 +172,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopirajte adresu u ostavu"
|
"message": "Kopirajte adresu u ostavu"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Ovo je vaš privatni ključ (kliknite kako biste ga kopirali)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Копирај у меморију"
|
"message": "Копирај у меморију"
|
||||||
},
|
},
|
||||||
@ -238,9 +235,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Greška u registraciji ENS imena."
|
"message": "Greška u registraciji ENS imena."
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Unesite lozinku"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Unesite lozinku kako biste nastavili"
|
"message": "Unesite lozinku kako biste nastavili"
|
||||||
},
|
},
|
||||||
@ -253,9 +247,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Proširite prikaz"
|
"message": "Proširite prikaz"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Izvezite privatni ključ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Neuspešno"
|
"message": "Neuspešno"
|
||||||
},
|
},
|
||||||
@ -654,9 +645,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Izaberite ovo da bi se pokazalo polje sa hex podacima na „Pošalji” ekranu "
|
"message": "Izaberite ovo da bi se pokazalo polje sa hex podacima na „Pošalji” ekranu "
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Prikažite privatne ključeve"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Zahtev za potpisom"
|
"message": "Zahtev za potpisom"
|
||||||
},
|
},
|
||||||
@ -774,9 +762,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Пробај поново"
|
"message": "Пробај поново"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Ukucajte svoju MetaMask šifru"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Neodobren"
|
"message": "Neodobren"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/sv/messages.json
generated
15
app/_locales/sv/messages.json
generated
@ -169,9 +169,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Kopiera adress till urklipp"
|
"message": "Kopiera adress till urklipp"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Det här är din privata nyckel (klicka för att kopiera)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Kopiera till Urklipp"
|
"message": "Kopiera till Urklipp"
|
||||||
},
|
},
|
||||||
@ -235,9 +232,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Fel i ENS-namnregistrering"
|
"message": "Fel i ENS-namnregistrering"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Ange lösenord"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Ange lösenord för att fortsätta"
|
"message": "Ange lösenord för att fortsätta"
|
||||||
},
|
},
|
||||||
@ -250,9 +244,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Expandera vy"
|
"message": "Expandera vy"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Exportera privat nyckel"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Misslyckades"
|
"message": "Misslyckades"
|
||||||
},
|
},
|
||||||
@ -647,9 +638,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Välj detta för att visa hex-datafältet på sändarskärmen"
|
"message": "Välj detta för att visa hex-datafältet på sändarskärmen"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Visa privata nycklar"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Signaturförfrågan"
|
"message": "Signaturförfrågan"
|
||||||
},
|
},
|
||||||
@ -761,9 +749,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Försök igen"
|
"message": "Försök igen"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Ange ditt MetaMask-lösenord"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Inte godkänd"
|
"message": "Inte godkänd"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/sw/messages.json
generated
15
app/_locales/sw/messages.json
generated
@ -169,9 +169,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Nakili anwani kwenye ubao wa kunakilia"
|
"message": "Nakili anwani kwenye ubao wa kunakilia"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Huu ni ufunguo wako wa kibinafsi (bofya ili unakili)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Nakili kwenye ubao wa kunakili"
|
"message": "Nakili kwenye ubao wa kunakili"
|
||||||
},
|
},
|
||||||
@ -235,9 +232,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Hitilafu imetokea kwenye usajili wa jina la ENS"
|
"message": "Hitilafu imetokea kwenye usajili wa jina la ENS"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Ingiza nenosiri"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Ingiza nenosiri ili uendelee"
|
"message": "Ingiza nenosiri ili uendelee"
|
||||||
},
|
},
|
||||||
@ -250,9 +244,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Panua Mwonekano"
|
"message": "Panua Mwonekano"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Panua Mwonekano"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Imeshindwa"
|
"message": "Imeshindwa"
|
||||||
},
|
},
|
||||||
@ -641,9 +632,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Chagua hii ili uonyeshe sehemu ya data ya hex kwenye skrini ya tuma"
|
"message": "Chagua hii ili uonyeshe sehemu ya data ya hex kwenye skrini ya tuma"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Onyesha Fungo Binafsi"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Ombi la Saini"
|
"message": "Ombi la Saini"
|
||||||
},
|
},
|
||||||
@ -764,9 +752,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Jaribu tena"
|
"message": "Jaribu tena"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Andika nenosiri lako la MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Haijaidhinishwa"
|
"message": "Haijaidhinishwa"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/ta/messages.json
generated
15
app/_locales/ta/messages.json
generated
@ -87,9 +87,6 @@
|
|||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "நகலெடுக்கப்பட்டன!"
|
"message": "நகலெடுக்கப்பட்டன!"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "இது உங்கள் தனிப்பட்ட விசை (நகலெடுக்க கிளிக் செய்யவும்)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "கிளிப்போர்டுக்கு நகலெடு"
|
"message": "கிளிப்போர்டுக்கு நகலெடு"
|
||||||
},
|
},
|
||||||
@ -126,15 +123,9 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "திருத்து"
|
"message": "திருத்து"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "கடவுச்சொல்லை உள்ளிடவும்"
|
|
||||||
},
|
|
||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "Etherscan கணக்கைப் பார்க்கவும்"
|
"message": "Etherscan கணக்கைப் பார்க்கவும்"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "தனியார் விசை ஐ ஏற்றுமதி செய்க"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "தோல்வி"
|
"message": "தோல்வி"
|
||||||
},
|
},
|
||||||
@ -374,9 +365,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"message": "அமைப்புகள்"
|
"message": "அமைப்புகள்"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "தனிப்பட்ட விசைகளைக் காண்பி"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "கையொப்பம் கோரிக்கை"
|
"message": "கையொப்பம் கோரிக்கை"
|
||||||
},
|
},
|
||||||
@ -425,9 +413,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "மீண்டும் முயல்க"
|
"message": "மீண்டும் முயல்க"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "உங்கள் கடவுச்சொல்லை தட்டச்சு செய்யவும்"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "அங்கீகரிக்கப்படாத"
|
"message": "அங்கீகரிக்கப்படாத"
|
||||||
},
|
},
|
||||||
|
15
app/_locales/th/messages.json
generated
15
app/_locales/th/messages.json
generated
@ -78,9 +78,6 @@
|
|||||||
"copiedExclamation": {
|
"copiedExclamation": {
|
||||||
"message": "คัดลอกแล้ว!"
|
"message": "คัดลอกแล้ว!"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "นี่คือคีย์ส่วนตัวของคุณ(กดเพื่อคัดลอก)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "คัดลอกไปคลิปบอร์ด"
|
"message": "คัดลอกไปคลิปบอร์ด"
|
||||||
},
|
},
|
||||||
@ -117,18 +114,12 @@
|
|||||||
"editContact": {
|
"editContact": {
|
||||||
"message": "แก้ไขผู้ติดต่อ"
|
"message": "แก้ไขผู้ติดต่อ"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "ใส่รหัสผ่าน"
|
|
||||||
},
|
|
||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "ดูบัญชีบน Etherscan"
|
"message": "ดูบัญชีบน Etherscan"
|
||||||
},
|
},
|
||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "ขยายมุมมอง"
|
"message": "ขยายมุมมอง"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "ส่งออกคีย์ส่วนตัว"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "ล้มเหลว"
|
"message": "ล้มเหลว"
|
||||||
},
|
},
|
||||||
@ -338,9 +329,6 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"message": "การตั้งค่า"
|
"message": "การตั้งค่า"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "แสดงคีย์ส่วนตัว"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "ขอลายเซ็น"
|
"message": "ขอลายเซ็น"
|
||||||
},
|
},
|
||||||
@ -392,9 +380,6 @@
|
|||||||
"transactionDropped": {
|
"transactionDropped": {
|
||||||
"message": "ธุรกรรมถูกยกเลิกเมื่อ $2"
|
"message": "ธุรกรรมถูกยกเลิกเมื่อ $2"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "พิมพ์รหัสผ่านของคุณ"
|
|
||||||
},
|
|
||||||
"unknown": {
|
"unknown": {
|
||||||
"message": "ไม่รู้จัก"
|
"message": "ไม่รู้จัก"
|
||||||
},
|
},
|
||||||
|
1156
app/_locales/tl/messages.json
generated
1156
app/_locales/tl/messages.json
generated
File diff suppressed because it is too large
Load Diff
1162
app/_locales/tr/messages.json
generated
1162
app/_locales/tr/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/uk/messages.json
generated
15
app/_locales/uk/messages.json
generated
@ -175,9 +175,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "Копіювати адресу в буфер обміну"
|
"message": "Копіювати адресу в буфер обміну"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "Це ваш закритий ключ (натисніть, щоб скопіювати)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "Копіювати в буфер"
|
"message": "Копіювати в буфер"
|
||||||
},
|
},
|
||||||
@ -241,9 +238,6 @@
|
|||||||
"ensRegistrationError": {
|
"ensRegistrationError": {
|
||||||
"message": "Помилка у реєстрації ENS ім'я"
|
"message": "Помилка у реєстрації ENS ім'я"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "Введіть пароль"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "Введіть пароль, щоб продовжити"
|
"message": "Введіть пароль, щоб продовжити"
|
||||||
},
|
},
|
||||||
@ -256,9 +250,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Розгорнути подання"
|
"message": "Розгорнути подання"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "Експортувати приватний ключ"
|
|
||||||
},
|
|
||||||
"failed": {
|
"failed": {
|
||||||
"message": "Помилка"
|
"message": "Помилка"
|
||||||
},
|
},
|
||||||
@ -663,9 +654,6 @@
|
|||||||
"showHexDataDescription": {
|
"showHexDataDescription": {
|
||||||
"message": "Оберіть це, щоб показати поле для шістнадцятирикових даних на екрані надсилання"
|
"message": "Оберіть це, щоб показати поле для шістнадцятирикових даних на екрані надсилання"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "Показати приватні ключі"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "Запит підпису"
|
"message": "Запит підпису"
|
||||||
},
|
},
|
||||||
@ -786,9 +774,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Повторити"
|
"message": "Повторити"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "Введіть ваш пароль MetaMask"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "Не затверджено"
|
"message": "Не затверджено"
|
||||||
},
|
},
|
||||||
|
1164
app/_locales/vi/messages.json
generated
1164
app/_locales/vi/messages.json
generated
File diff suppressed because it is too large
Load Diff
1194
app/_locales/zh_CN/messages.json
generated
1194
app/_locales/zh_CN/messages.json
generated
File diff suppressed because it is too large
Load Diff
15
app/_locales/zh_TW/messages.json
generated
15
app/_locales/zh_TW/messages.json
generated
@ -334,9 +334,6 @@
|
|||||||
"copyAddress": {
|
"copyAddress": {
|
||||||
"message": "複製到剪貼簿"
|
"message": "複製到剪貼簿"
|
||||||
},
|
},
|
||||||
"copyPrivateKey": {
|
|
||||||
"message": "這是您的私鑰(點擊複製)"
|
|
||||||
},
|
|
||||||
"copyToClipboard": {
|
"copyToClipboard": {
|
||||||
"message": "複製到剪貼簿"
|
"message": "複製到剪貼簿"
|
||||||
},
|
},
|
||||||
@ -497,9 +494,6 @@
|
|||||||
"enterMaxSpendLimit": {
|
"enterMaxSpendLimit": {
|
||||||
"message": "輸入最大花費限制"
|
"message": "輸入最大花費限制"
|
||||||
},
|
},
|
||||||
"enterPassword": {
|
|
||||||
"message": "請輸入密碼"
|
|
||||||
},
|
|
||||||
"enterPasswordContinue": {
|
"enterPasswordContinue": {
|
||||||
"message": "請輸入密碼"
|
"message": "請輸入密碼"
|
||||||
},
|
},
|
||||||
@ -547,9 +541,6 @@
|
|||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "展開畫面"
|
"message": "展開畫面"
|
||||||
},
|
},
|
||||||
"exportPrivateKey": {
|
|
||||||
"message": "匯出私鑰"
|
|
||||||
},
|
|
||||||
"externalExtension": {
|
"externalExtension": {
|
||||||
"message": "外部擴充功能"
|
"message": "外部擴充功能"
|
||||||
},
|
},
|
||||||
@ -1231,9 +1222,6 @@
|
|||||||
"showPermissions": {
|
"showPermissions": {
|
||||||
"message": "顯示權限"
|
"message": "顯示權限"
|
||||||
},
|
},
|
||||||
"showPrivateKeys": {
|
|
||||||
"message": "顯示私鑰"
|
|
||||||
},
|
|
||||||
"sigRequest": {
|
"sigRequest": {
|
||||||
"message": "請求簽署"
|
"message": "請求簽署"
|
||||||
},
|
},
|
||||||
@ -1440,9 +1428,6 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "再試一次"
|
"message": "再試一次"
|
||||||
},
|
},
|
||||||
"typePassword": {
|
|
||||||
"message": "請輸入密碼"
|
|
||||||
},
|
|
||||||
"unapproved": {
|
"unapproved": {
|
||||||
"message": "未批准"
|
"message": "未批准"
|
||||||
},
|
},
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import { strict as assert } from 'assert';
|
|
||||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
|
||||||
import accountImporter from '.';
|
|
||||||
|
|
||||||
describe('Account Import Strategies', function () {
|
|
||||||
const privkey =
|
|
||||||
'0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553';
|
|
||||||
const json =
|
|
||||||
'{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}';
|
|
||||||
|
|
||||||
describe('private key import', function () {
|
|
||||||
it('imports a private key and strips 0x prefix', async function () {
|
|
||||||
const importPrivKey = await accountImporter.importAccount('Private Key', [
|
|
||||||
privkey,
|
|
||||||
]);
|
|
||||||
assert.equal(importPrivKey, stripHexPrefix(privkey));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error for empty string private key', async function () {
|
|
||||||
await assert.rejects(
|
|
||||||
async () => {
|
|
||||||
await accountImporter.importAccount('Private Key', ['']);
|
|
||||||
},
|
|
||||||
Error,
|
|
||||||
'no empty strings',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error for undefined string private key', async function () {
|
|
||||||
await assert.rejects(async () => {
|
|
||||||
await accountImporter.importAccount('Private Key', [undefined]);
|
|
||||||
});
|
|
||||||
|
|
||||||
await assert.rejects(async () => {
|
|
||||||
await accountImporter.importAccount('Private Key', []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error for invalid private key', async function () {
|
|
||||||
await assert.rejects(async () => {
|
|
||||||
await accountImporter.importAccount('Private Key', ['popcorn']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('JSON keystore import', function () {
|
|
||||||
it('fails when password is incorrect for keystore', async function () {
|
|
||||||
const wrongPassword = 'password2';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await accountImporter.importAccount('JSON File', [json, wrongPassword]);
|
|
||||||
} catch (error) {
|
|
||||||
assert.equal(
|
|
||||||
error.message,
|
|
||||||
'Key derivation failed - possibly wrong passphrase',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('imports json string and password to return a private key', async function () {
|
|
||||||
const fileContentsPassword = 'password1';
|
|
||||||
const importJson = await accountImporter.importAccount('JSON File', [
|
|
||||||
json,
|
|
||||||
fileContentsPassword,
|
|
||||||
]);
|
|
||||||
assert.equal(
|
|
||||||
importJson,
|
|
||||||
'0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,75 +0,0 @@
|
|||||||
import { isValidMnemonic } from '@ethersproject/hdnode';
|
|
||||||
import {
|
|
||||||
bufferToHex,
|
|
||||||
getBinarySize,
|
|
||||||
isValidPrivate,
|
|
||||||
toBuffer,
|
|
||||||
} from 'ethereumjs-util';
|
|
||||||
import Wallet from 'ethereumjs-wallet';
|
|
||||||
import importers from 'ethereumjs-wallet/thirdparty';
|
|
||||||
import log from 'loglevel';
|
|
||||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
|
||||||
import { addHexPrefix } from '../lib/util';
|
|
||||||
|
|
||||||
const accountImporter = {
|
|
||||||
async importAccount(strategy, args) {
|
|
||||||
const importer = this.strategies[strategy];
|
|
||||||
const privateKeyHex = importer(...args);
|
|
||||||
return privateKeyHex;
|
|
||||||
},
|
|
||||||
|
|
||||||
strategies: {
|
|
||||||
'Private Key': (privateKey) => {
|
|
||||||
if (!privateKey) {
|
|
||||||
throw new Error('Cannot import an empty key.'); // It should never get here, because this should be stopped in the UI
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has entered an SRP by mistake instead of a private key
|
|
||||||
if (isValidMnemonic(privateKey.trim())) {
|
|
||||||
throw new Error(`t('importAccountErrorIsSRP')`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmedPrivateKey = privateKey.replace(/\s+/gu, ''); // Remove all whitespace
|
|
||||||
|
|
||||||
const prefixedPrivateKey = addHexPrefix(trimmedPrivateKey);
|
|
||||||
let buffer;
|
|
||||||
try {
|
|
||||||
buffer = toBuffer(prefixedPrivateKey);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`t('importAccountErrorNotHexadecimal')`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!isValidPrivate(buffer) ||
|
|
||||||
getBinarySize(prefixedPrivateKey) !== 64 + '0x'.length // Fixes issue #17719 -- isValidPrivate() will let a key of 63 hex digits through without complaining, this line ensures 64 hex digits + '0x' = 66 digits
|
|
||||||
) {
|
|
||||||
throw new Error(`t('importAccountErrorNotAValidPrivateKey')`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`t('importAccountErrorNotAValidPrivateKey')`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const strippedPrivateKey = stripHexPrefix(prefixedPrivateKey);
|
|
||||||
return strippedPrivateKey;
|
|
||||||
},
|
|
||||||
'JSON File': (input, password) => {
|
|
||||||
let wallet;
|
|
||||||
try {
|
|
||||||
wallet = importers.fromEtherWallet(input, password);
|
|
||||||
} catch (e) {
|
|
||||||
log.debug('Attempt to import as EtherWallet format failed, trying V3');
|
|
||||||
wallet = Wallet.fromV3(input, password, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return walletToPrivateKey(wallet);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function walletToPrivateKey(wallet) {
|
|
||||||
const privateKeyBuffer = wallet.getPrivateKey();
|
|
||||||
return bufferToHex(privateKeyBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default accountImporter;
|
|
@ -2,9 +2,11 @@
|
|||||||
* @file The entry point for the web extension singleton process.
|
* @file The entry point for the web extension singleton process.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This import sets up a global function required for Sentry to function.
|
// Disabled to allow setting up initial state hooks first
|
||||||
|
|
||||||
|
// This import sets up global functions required for Sentry to function.
|
||||||
// It must be run first in case an error is thrown later during initialization.
|
// It must be run first in case an error is thrown later during initialization.
|
||||||
import './lib/setup-persisted-state-hook';
|
import './lib/setup-initial-state-hooks';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import endOfStream from 'end-of-stream';
|
import endOfStream from 'end-of-stream';
|
||||||
@ -13,6 +15,7 @@ import debounce from 'debounce-stream';
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
import { storeAsStream } from '@metamask/obs-store';
|
import { storeAsStream } from '@metamask/obs-store';
|
||||||
|
import { isObject } from '@metamask/utils';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||||
import { ApprovalType } from '@metamask/controller-utils';
|
import { ApprovalType } from '@metamask/controller-utils';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
@ -41,7 +44,7 @@ import Migrator from './lib/migrator';
|
|||||||
import ExtensionPlatform from './platforms/extension';
|
import ExtensionPlatform from './platforms/extension';
|
||||||
import LocalStore from './lib/local-store';
|
import LocalStore from './lib/local-store';
|
||||||
import ReadOnlyNetworkStore from './lib/network-store';
|
import ReadOnlyNetworkStore from './lib/network-store';
|
||||||
import { SENTRY_STATE } from './lib/setupSentry';
|
import { SENTRY_BACKGROUND_STATE } from './lib/setupSentry';
|
||||||
|
|
||||||
import createStreamSink from './lib/createStreamSink';
|
import createStreamSink from './lib/createStreamSink';
|
||||||
import NotificationManager, {
|
import NotificationManager, {
|
||||||
@ -68,6 +71,12 @@ import DesktopManager from '@metamask/desktop/dist/desktop-manager';
|
|||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
/* eslint-enable import/order */
|
/* eslint-enable import/order */
|
||||||
|
|
||||||
|
// Setup global hook for improved Sentry state snapshots during initialization
|
||||||
|
const inTest = process.env.IN_TEST;
|
||||||
|
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
||||||
|
global.stateHooks.getMostRecentPersistedState = () =>
|
||||||
|
localStore.mostRecentRetrievedState;
|
||||||
|
|
||||||
const { sentry } = global;
|
const { sentry } = global;
|
||||||
const firstTimeState = { ...rawFirstTimeState };
|
const firstTimeState = { ...rawFirstTimeState };
|
||||||
|
|
||||||
@ -79,7 +88,7 @@ const metamaskInternalProcessHash = {
|
|||||||
|
|
||||||
const metamaskBlockedPorts = ['trezor-connect'];
|
const metamaskBlockedPorts = ['trezor-connect'];
|
||||||
|
|
||||||
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info');
|
log.setLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info', false);
|
||||||
|
|
||||||
const platform = new ExtensionPlatform();
|
const platform = new ExtensionPlatform();
|
||||||
const notificationManager = new NotificationManager();
|
const notificationManager = new NotificationManager();
|
||||||
@ -90,10 +99,6 @@ let uiIsTriggering = false;
|
|||||||
const openMetamaskTabsIDs = {};
|
const openMetamaskTabsIDs = {};
|
||||||
const requestAccountTabIds = {};
|
const requestAccountTabIds = {};
|
||||||
let controller;
|
let controller;
|
||||||
|
|
||||||
// state persistence
|
|
||||||
const inTest = process.env.IN_TEST;
|
|
||||||
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
|
||||||
let versionedData;
|
let versionedData;
|
||||||
|
|
||||||
if (inTest || process.env.METAMASK_DEBUG) {
|
if (inTest || process.env.METAMASK_DEBUG) {
|
||||||
@ -264,7 +269,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
|
|||||||
*/
|
*/
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
try {
|
try {
|
||||||
const initState = await loadStateFromPersistence();
|
const initData = await loadStateFromPersistence();
|
||||||
|
const initState = initData.data;
|
||||||
const initLangCode = await getFirstPreferredLangCode();
|
const initLangCode = await getFirstPreferredLangCode();
|
||||||
|
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
||||||
@ -287,6 +293,7 @@ async function initialize() {
|
|||||||
initLangCode,
|
initLangCode,
|
||||||
{},
|
{},
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
initData.meta,
|
||||||
);
|
);
|
||||||
if (!isManifestV3) {
|
if (!isManifestV3) {
|
||||||
await loadPhishingWarningPage();
|
await loadPhishingWarningPage();
|
||||||
@ -409,6 +416,19 @@ export async function loadStateFromPersistence() {
|
|||||||
versionedData = await migrator.migrateData(versionedData);
|
versionedData = await migrator.migrateData(versionedData);
|
||||||
if (!versionedData) {
|
if (!versionedData) {
|
||||||
throw new Error('MetaMask - migrator returned undefined');
|
throw new Error('MetaMask - migrator returned undefined');
|
||||||
|
} else if (!isObject(versionedData.meta)) {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator metadata has invalid type '${typeof versionedData.meta}'`,
|
||||||
|
);
|
||||||
|
} else if (typeof versionedData.meta.version !== 'number') {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator metadata version has invalid type '${typeof versionedData
|
||||||
|
.meta.version}'`,
|
||||||
|
);
|
||||||
|
} else if (!isObject(versionedData.data)) {
|
||||||
|
throw new Error(
|
||||||
|
`MetaMask - migrator data has invalid type '${typeof versionedData.data}'`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// this initializes the meta/version data as a class variable to be used for future writes
|
// this initializes the meta/version data as a class variable to be used for future writes
|
||||||
localStore.setMetadata(versionedData.meta);
|
localStore.setMetadata(versionedData.meta);
|
||||||
@ -417,7 +437,7 @@ export async function loadStateFromPersistence() {
|
|||||||
localStore.set(versionedData.data);
|
localStore.set(versionedData.data);
|
||||||
|
|
||||||
// return just the data
|
// return just the data
|
||||||
return versionedData.data;
|
return versionedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -430,12 +450,14 @@ export async function loadStateFromPersistence() {
|
|||||||
* @param {string} initLangCode - The region code for the language preferred by the current user.
|
* @param {string} initLangCode - The region code for the language preferred by the current user.
|
||||||
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
||||||
* @param isFirstMetaMaskControllerSetup
|
* @param isFirstMetaMaskControllerSetup
|
||||||
|
* @param {object} stateMetadata - Metadata about the initial state and migrations, including the most recent migration version
|
||||||
*/
|
*/
|
||||||
export function setupController(
|
export function setupController(
|
||||||
initState,
|
initState,
|
||||||
initLangCode,
|
initLangCode,
|
||||||
overrides,
|
overrides,
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
stateMetadata,
|
||||||
) {
|
) {
|
||||||
//
|
//
|
||||||
// MetaMask Controller
|
// MetaMask Controller
|
||||||
@ -462,6 +484,7 @@ export function setupController(
|
|||||||
localStore,
|
localStore,
|
||||||
overrides,
|
overrides,
|
||||||
isFirstMetaMaskControllerSetup,
|
isFirstMetaMaskControllerSetup,
|
||||||
|
currentMigrationVersion: stateMetadata.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
setupEnsIpfsResolver({
|
setupEnsIpfsResolver({
|
||||||
@ -880,14 +903,9 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function setupSentryGetStateGlobal(store) {
|
function setupSentryGetStateGlobal(store) {
|
||||||
global.stateHooks.getSentryState = function () {
|
global.stateHooks.getSentryAppState = function () {
|
||||||
const fullState = store.getState();
|
const backgroundState = store.memStore.getState();
|
||||||
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
|
return maskObject(backgroundState, SENTRY_BACKGROUND_STATE);
|
||||||
return {
|
|
||||||
browser: window.navigator.userAgent,
|
|
||||||
store: debugState,
|
|
||||||
version: platform.getVersion(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
app/scripts/controllers/app-metadata.test.ts
Normal file
104
app/scripts/controllers/app-metadata.test.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import AppMetadataController from './app-metadata';
|
||||||
|
|
||||||
|
const EXPECTED_DEFAULT_STATE = {
|
||||||
|
currentAppVersion: '',
|
||||||
|
previousAppVersion: '',
|
||||||
|
previousMigrationVersion: 0,
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AppMetadataController', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('accepts initial state and does not modify it if currentMigrationVersion and platform.getVersion() match respective values in state', async () => {
|
||||||
|
const initState = {
|
||||||
|
currentAppVersion: '1',
|
||||||
|
previousAppVersion: '1',
|
||||||
|
previousMigrationVersion: 1,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
};
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: initState,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), initState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default state and does not modify it', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
appMetadataController.store.getState(),
|
||||||
|
EXPECTED_DEFAULT_STATE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets default state and does not modify it if options version parameters match respective default values', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
currentAppVersion: '',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
appMetadataController.store.getState(),
|
||||||
|
EXPECTED_DEFAULT_STATE,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentAppVersion state property if options.currentAppVersion does not match the default value', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentAppVersion: '1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentAppVersion and previousAppVersion state properties if options.currentAppVersion, currentAppVersion and previousAppVersion are all different', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {
|
||||||
|
currentAppVersion: '2',
|
||||||
|
previousAppVersion: '1',
|
||||||
|
},
|
||||||
|
currentAppVersion: '3',
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentAppVersion: '3',
|
||||||
|
previousAppVersion: '2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentMigrationVersion state property if the currentMigrationVersion param does not match the default value', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {},
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentMigrationVersion: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the currentMigrationVersion and previousMigrationVersion state properties if the currentMigrationVersion param, the currentMigrationVersion state property and the previousMigrationVersion state property are all different', async () => {
|
||||||
|
const appMetadataController = new AppMetadataController({
|
||||||
|
state: {
|
||||||
|
currentMigrationVersion: 2,
|
||||||
|
previousMigrationVersion: 1,
|
||||||
|
},
|
||||||
|
currentMigrationVersion: 3,
|
||||||
|
});
|
||||||
|
assert.deepStrictEqual(appMetadataController.store.getState(), {
|
||||||
|
...EXPECTED_DEFAULT_STATE,
|
||||||
|
currentMigrationVersion: 3,
|
||||||
|
previousMigrationVersion: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
99
app/scripts/controllers/app-metadata.ts
Normal file
99
app/scripts/controllers/app-metadata.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import EventEmitter from 'events';
|
||||||
|
import { ObservableStore } from '@metamask/obs-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state of the AppMetadataController
|
||||||
|
*/
|
||||||
|
export type AppMetadataControllerState = {
|
||||||
|
currentAppVersion: string;
|
||||||
|
previousAppVersion: string;
|
||||||
|
previousMigrationVersion: number;
|
||||||
|
currentMigrationVersion: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options that NetworkController takes.
|
||||||
|
*/
|
||||||
|
export type AppMetadataControllerOptions = {
|
||||||
|
currentMigrationVersion?: number;
|
||||||
|
currentAppVersion?: string;
|
||||||
|
state?: Partial<AppMetadataControllerState>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultState: AppMetadataControllerState = {
|
||||||
|
currentAppVersion: '',
|
||||||
|
previousAppVersion: '',
|
||||||
|
previousMigrationVersion: 0,
|
||||||
|
currentMigrationVersion: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AppMetadata controller stores metadata about the current extension instance,
|
||||||
|
* including the currently and previously installed versions, and the most recently
|
||||||
|
* run migration.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class AppMetadataController extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Observable store containing controller data.
|
||||||
|
*/
|
||||||
|
store: ObservableStore<AppMetadataControllerState>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a AppMetadata controller.
|
||||||
|
*
|
||||||
|
* @param options - the controller options
|
||||||
|
* @param options.state - Initial controller state.
|
||||||
|
* @param options.currentMigrationVersion
|
||||||
|
* @param options.currentAppVersion
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
currentAppVersion = '',
|
||||||
|
currentMigrationVersion = 0,
|
||||||
|
state = {},
|
||||||
|
}: AppMetadataControllerOptions) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.store = new ObservableStore({
|
||||||
|
...defaultState,
|
||||||
|
...state,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#maybeUpdateAppVersion(currentAppVersion);
|
||||||
|
|
||||||
|
this.#maybeUpdateMigrationVersion(currentMigrationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the currentAppVersion in state, and sets the previousAppVersion to the old currentAppVersion.
|
||||||
|
*
|
||||||
|
* @param maybeNewAppVersion
|
||||||
|
*/
|
||||||
|
#maybeUpdateAppVersion(maybeNewAppVersion: string): void {
|
||||||
|
const oldCurrentAppVersion = this.store.getState().currentAppVersion;
|
||||||
|
|
||||||
|
if (maybeNewAppVersion !== oldCurrentAppVersion) {
|
||||||
|
this.store.updateState({
|
||||||
|
currentAppVersion: maybeNewAppVersion,
|
||||||
|
previousAppVersion: oldCurrentAppVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the migrationVersion in state.
|
||||||
|
*
|
||||||
|
* @param maybeNewMigrationVersion
|
||||||
|
*/
|
||||||
|
#maybeUpdateMigrationVersion(maybeNewMigrationVersion: number): void {
|
||||||
|
const oldCurrentMigrationVersion =
|
||||||
|
this.store.getState().currentMigrationVersion;
|
||||||
|
|
||||||
|
if (maybeNewMigrationVersion !== oldCurrentMigrationVersion) {
|
||||||
|
this.store.updateState({
|
||||||
|
previousMigrationVersion: oldCurrentMigrationVersion,
|
||||||
|
currentMigrationVersion: maybeNewMigrationVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -225,6 +225,7 @@ describe('DetectTokensController', function () {
|
|||||||
tokenListController,
|
tokenListController,
|
||||||
onInfuraIsBlocked: sinon.stub(),
|
onInfuraIsBlocked: sinon.stub(),
|
||||||
onInfuraIsUnblocked: sinon.stub(),
|
onInfuraIsUnblocked: sinon.stub(),
|
||||||
|
networkConfigurations: {},
|
||||||
});
|
});
|
||||||
preferences.setAddresses([
|
preferences.setAddresses([
|
||||||
'0x7e57e2',
|
'0x7e57e2',
|
||||||
|
@ -1,320 +0,0 @@
|
|||||||
import { ObservableStore } from '@metamask/obs-store';
|
|
||||||
import log from 'loglevel';
|
|
||||||
import BN from 'bn.js';
|
|
||||||
import createId from '../../../shared/modules/random-id';
|
|
||||||
import { previousValueComparator } from '../lib/util';
|
|
||||||
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
|
|
||||||
|
|
||||||
import {
|
|
||||||
TransactionType,
|
|
||||||
TransactionStatus,
|
|
||||||
} from '../../../shared/constants/transaction';
|
|
||||||
import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../shared/constants/network';
|
|
||||||
import { bnToHex } from '../../../shared/modules/conversion.utils';
|
|
||||||
|
|
||||||
const fetchWithTimeout = getFetchWithTimeout();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A transaction object in the format returned by the Etherscan API.
|
|
||||||
*
|
|
||||||
* Note that this is not an exhaustive type definiton; only the properties we use are defined
|
|
||||||
*
|
|
||||||
* @typedef {object} EtherscanTransaction
|
|
||||||
* @property {string} blockNumber - The number of the block this transaction was found in, in decimal
|
|
||||||
* @property {string} from - The hex-prefixed address of the sender
|
|
||||||
* @property {string} gas - The gas limit, in decimal GWEI
|
|
||||||
* @property {string} [gasPrice] - The gas price, in decimal WEI
|
|
||||||
* @property {string} [maxFeePerGas] - The maximum fee per gas, inclusive of tip, in decimal WEI
|
|
||||||
* @property {string} [maxPriorityFeePerGas] - The maximum tip per gas in decimal WEI
|
|
||||||
* @property {string} hash - The hex-prefixed transaction hash
|
|
||||||
* @property {string} isError - Whether the transaction was confirmed or failed (0 for confirmed, 1 for failed)
|
|
||||||
* @property {string} nonce - The transaction nonce, in decimal
|
|
||||||
* @property {string} timeStamp - The timestamp for the transaction, in seconds
|
|
||||||
* @property {string} to - The hex-prefixed address of the recipient
|
|
||||||
* @property {string} value - The amount of ETH sent in this transaction, in decimal WEI
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This controller is responsible for retrieving incoming transactions. Etherscan is polled once every block to check
|
|
||||||
* for new incoming transactions for the current selected account on the current network
|
|
||||||
*
|
|
||||||
* Note that only Etherscan-compatible networks are supported. We will not attempt to retrieve incoming transactions
|
|
||||||
* on non-compatible custom RPC endpoints.
|
|
||||||
*/
|
|
||||||
export default class IncomingTransactionsController {
|
|
||||||
constructor(opts = {}) {
|
|
||||||
const {
|
|
||||||
blockTracker,
|
|
||||||
onNetworkDidChange,
|
|
||||||
getCurrentChainId,
|
|
||||||
preferencesController,
|
|
||||||
onboardingController,
|
|
||||||
} = opts;
|
|
||||||
this.blockTracker = blockTracker;
|
|
||||||
this.getCurrentChainId = getCurrentChainId;
|
|
||||||
this.preferencesController = preferencesController;
|
|
||||||
this.onboardingController = onboardingController;
|
|
||||||
|
|
||||||
this._onLatestBlock = async (newBlockNumberHex) => {
|
|
||||||
const selectedAddress = this.preferencesController.getSelectedAddress();
|
|
||||||
const newBlockNumberDec = parseInt(newBlockNumberHex, 16);
|
|
||||||
await this._update(selectedAddress, newBlockNumberDec);
|
|
||||||
};
|
|
||||||
|
|
||||||
const incomingTxLastFetchedBlockByChainId = Object.keys(
|
|
||||||
ETHERSCAN_SUPPORTED_NETWORKS,
|
|
||||||
).reduce((network, chainId) => {
|
|
||||||
network[chainId] = null;
|
|
||||||
return network;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const initState = {
|
|
||||||
incomingTransactions: {},
|
|
||||||
incomingTxLastFetchedBlockByChainId,
|
|
||||||
...opts.initState,
|
|
||||||
};
|
|
||||||
this.store = new ObservableStore(initState);
|
|
||||||
|
|
||||||
this.preferencesController.store.subscribe(
|
|
||||||
previousValueComparator((prevState, currState) => {
|
|
||||||
const {
|
|
||||||
featureFlags: {
|
|
||||||
showIncomingTransactions: prevShowIncomingTransactions,
|
|
||||||
} = {},
|
|
||||||
} = prevState;
|
|
||||||
const {
|
|
||||||
featureFlags: {
|
|
||||||
showIncomingTransactions: currShowIncomingTransactions,
|
|
||||||
} = {},
|
|
||||||
} = currState;
|
|
||||||
|
|
||||||
if (currShowIncomingTransactions === prevShowIncomingTransactions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevShowIncomingTransactions && !currShowIncomingTransactions) {
|
|
||||||
this.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.start();
|
|
||||||
}, this.preferencesController.store.getState()),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.preferencesController.store.subscribe(
|
|
||||||
previousValueComparator(async (prevState, currState) => {
|
|
||||||
const { selectedAddress: prevSelectedAddress } = prevState;
|
|
||||||
const { selectedAddress: currSelectedAddress } = currState;
|
|
||||||
|
|
||||||
if (currSelectedAddress === prevSelectedAddress) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this._update(currSelectedAddress);
|
|
||||||
}, this.preferencesController.store.getState()),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.onboardingController.store.subscribe(
|
|
||||||
previousValueComparator(async (prevState, currState) => {
|
|
||||||
const { completedOnboarding: prevCompletedOnboarding } = prevState;
|
|
||||||
const { completedOnboarding: currCompletedOnboarding } = currState;
|
|
||||||
if (!prevCompletedOnboarding && currCompletedOnboarding) {
|
|
||||||
const address = this.preferencesController.getSelectedAddress();
|
|
||||||
await this._update(address);
|
|
||||||
}
|
|
||||||
}, this.onboardingController.store.getState()),
|
|
||||||
);
|
|
||||||
|
|
||||||
onNetworkDidChange(async () => {
|
|
||||||
const address = this.preferencesController.getSelectedAddress();
|
|
||||||
await this._update(address);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
const chainId = this.getCurrentChainId();
|
|
||||||
|
|
||||||
if (this._allowedToMakeFetchIncomingTx(chainId)) {
|
|
||||||
this.blockTracker.removeListener('latest', this._onLatestBlock);
|
|
||||||
this.blockTracker.addListener('latest', this._onLatestBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.blockTracker.removeListener('latest', this._onLatestBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the correct block number to begin looking for new transactions
|
|
||||||
* from, fetches the transactions and then saves them and the next block
|
|
||||||
* number to begin fetching from in state. Block numbers and transactions are
|
|
||||||
* stored per chainId.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string} address - address to lookup transactions for
|
|
||||||
* @param {number} [newBlockNumberDec] - block number to begin fetching from
|
|
||||||
*/
|
|
||||||
async _update(address, newBlockNumberDec) {
|
|
||||||
const chainId = this.getCurrentChainId();
|
|
||||||
|
|
||||||
if (!address || !this._allowedToMakeFetchIncomingTx(chainId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const currentState = this.store.getState();
|
|
||||||
const currentBlock = parseInt(this.blockTracker.getCurrentBlock(), 16);
|
|
||||||
|
|
||||||
const mostRecentlyFetchedBlock =
|
|
||||||
currentState.incomingTxLastFetchedBlockByChainId[chainId];
|
|
||||||
const blockToFetchFrom =
|
|
||||||
mostRecentlyFetchedBlock ?? newBlockNumberDec ?? currentBlock;
|
|
||||||
|
|
||||||
const newIncomingTxs = await this._getNewIncomingTransactions(
|
|
||||||
address,
|
|
||||||
blockToFetchFrom,
|
|
||||||
chainId,
|
|
||||||
);
|
|
||||||
|
|
||||||
let newMostRecentlyFetchedBlock = blockToFetchFrom;
|
|
||||||
|
|
||||||
newIncomingTxs.forEach((tx) => {
|
|
||||||
if (
|
|
||||||
tx.blockNumber &&
|
|
||||||
parseInt(newMostRecentlyFetchedBlock, 10) <
|
|
||||||
parseInt(tx.blockNumber, 10)
|
|
||||||
) {
|
|
||||||
newMostRecentlyFetchedBlock = parseInt(tx.blockNumber, 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.store.updateState({
|
|
||||||
incomingTxLastFetchedBlockByChainId: {
|
|
||||||
...currentState.incomingTxLastFetchedBlockByChainId,
|
|
||||||
[chainId]: newMostRecentlyFetchedBlock + 1,
|
|
||||||
},
|
|
||||||
incomingTransactions: newIncomingTxs.reduce(
|
|
||||||
(transactions, tx) => {
|
|
||||||
transactions[tx.hash] = tx;
|
|
||||||
return transactions;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...currentState.incomingTransactions,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
log.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fetches transactions for the given address and chain, via etherscan, then
|
|
||||||
* processes the data into the necessary shape for usage in this controller.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string} [address] - Address to fetch transactions for
|
|
||||||
* @param {number} [fromBlock] - Block to look for transactions at
|
|
||||||
* @param {string} [chainId] - The chainId for the current network
|
|
||||||
* @returns {TransactionMeta[]}
|
|
||||||
*/
|
|
||||||
async _getNewIncomingTransactions(address, fromBlock, chainId) {
|
|
||||||
const etherscanDomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].domain;
|
|
||||||
const etherscanSubdomain = ETHERSCAN_SUPPORTED_NETWORKS[chainId].subdomain;
|
|
||||||
|
|
||||||
const apiUrl = `https://${etherscanSubdomain}.${etherscanDomain}`;
|
|
||||||
let url = `${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1`;
|
|
||||||
|
|
||||||
if (fromBlock) {
|
|
||||||
url += `&startBlock=${parseInt(fromBlock, 10)}`;
|
|
||||||
}
|
|
||||||
const response = await fetchWithTimeout(url);
|
|
||||||
const { status, result } = await response.json();
|
|
||||||
let newIncomingTxs = [];
|
|
||||||
if (status === '1' && Array.isArray(result) && result.length > 0) {
|
|
||||||
const remoteTxList = {};
|
|
||||||
const remoteTxs = [];
|
|
||||||
result.forEach((tx) => {
|
|
||||||
if (!remoteTxList[tx.hash]) {
|
|
||||||
remoteTxs.push(this._normalizeTxFromEtherscan(tx, chainId));
|
|
||||||
remoteTxList[tx.hash] = 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
newIncomingTxs = remoteTxs.filter(
|
|
||||||
(tx) => tx.txParams?.to?.toLowerCase() === address.toLowerCase(),
|
|
||||||
);
|
|
||||||
newIncomingTxs.sort((a, b) => (a.time < b.time ? -1 : 1));
|
|
||||||
}
|
|
||||||
return newIncomingTxs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transmutes a EtherscanTransaction into a TransactionMeta
|
|
||||||
*
|
|
||||||
* @param {EtherscanTransaction} etherscanTransaction - the transaction to normalize
|
|
||||||
* @param {string} chainId - The chainId of the current network
|
|
||||||
* @returns {TransactionMeta}
|
|
||||||
*/
|
|
||||||
_normalizeTxFromEtherscan(etherscanTransaction, chainId) {
|
|
||||||
const time = parseInt(etherscanTransaction.timeStamp, 10) * 1000;
|
|
||||||
const status =
|
|
||||||
etherscanTransaction.isError === '0'
|
|
||||||
? TransactionStatus.confirmed
|
|
||||||
: TransactionStatus.failed;
|
|
||||||
const txParams = {
|
|
||||||
from: etherscanTransaction.from,
|
|
||||||
gas: bnToHex(new BN(etherscanTransaction.gas)),
|
|
||||||
nonce: bnToHex(new BN(etherscanTransaction.nonce)),
|
|
||||||
to: etherscanTransaction.to,
|
|
||||||
value: bnToHex(new BN(etherscanTransaction.value)),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (etherscanTransaction.gasPrice) {
|
|
||||||
txParams.gasPrice = bnToHex(new BN(etherscanTransaction.gasPrice));
|
|
||||||
} else if (etherscanTransaction.maxFeePerGas) {
|
|
||||||
txParams.maxFeePerGas = bnToHex(
|
|
||||||
new BN(etherscanTransaction.maxFeePerGas),
|
|
||||||
);
|
|
||||||
txParams.maxPriorityFeePerGas = bnToHex(
|
|
||||||
new BN(etherscanTransaction.maxPriorityFeePerGas),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
blockNumber: etherscanTransaction.blockNumber,
|
|
||||||
id: createId(),
|
|
||||||
chainId,
|
|
||||||
metamaskNetworkId: ETHERSCAN_SUPPORTED_NETWORKS[chainId].networkId,
|
|
||||||
status,
|
|
||||||
time,
|
|
||||||
txParams,
|
|
||||||
hash: etherscanTransaction.hash,
|
|
||||||
type: TransactionType.incoming,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param chainId - {string} The chainId of the current network
|
|
||||||
* @returns {boolean} Whether or not the user has consented to show incoming transactions
|
|
||||||
*/
|
|
||||||
_allowedToMakeFetchIncomingTx(chainId) {
|
|
||||||
const { featureFlags = {} } = this.preferencesController.store.getState();
|
|
||||||
const { completedOnboarding } = this.onboardingController.store.getState();
|
|
||||||
|
|
||||||
const hasIncomingTransactionsFeatureEnabled = Boolean(
|
|
||||||
featureFlags.showIncomingTransactions,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isEtherscanSupportedNetwork = Boolean(
|
|
||||||
ETHERSCAN_SUPPORTED_NETWORKS[chainId],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
completedOnboarding &&
|
|
||||||
isEtherscanSupportedNetwork &&
|
|
||||||
hasIncomingTransactionsFeatureEnabled
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -292,9 +292,9 @@ export default class MMIController extends EventEmitter {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
newAccounts.forEach(
|
for (let i = 0; i < newAccounts.length; i++) {
|
||||||
async () => await this.keyringController.addNewAccount(keyring),
|
await this.keyringController.addNewAccount(keyring);
|
||||||
);
|
}
|
||||||
|
|
||||||
const allAccounts = await this.keyringController.getAccounts();
|
const allAccounts = await this.keyringController.getAccounts();
|
||||||
|
|
||||||
@ -303,12 +303,33 @@ export default class MMIController extends EventEmitter {
|
|||||||
...new Set(oldAccounts.concat(allAccounts.map((a) => a.toLowerCase()))),
|
...new Set(oldAccounts.concat(allAccounts.map((a) => a.toLowerCase()))),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Create a Set of lowercased addresses from oldAccounts for efficient existence checks
|
||||||
|
const oldAccountsSet = new Set(
|
||||||
|
oldAccounts.map((address) => address.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a map of lowercased addresses to names from newAccounts for efficient lookups
|
||||||
|
const accountNameMap = newAccounts.reduce((acc, item) => {
|
||||||
|
// For each account in newAccounts, add an entry to the map with the lowercased address as the key and the name as the value
|
||||||
|
acc[item.toLowerCase()] = accounts[item].name;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Iterate over all accounts
|
||||||
allAccounts.forEach((address) => {
|
allAccounts.forEach((address) => {
|
||||||
if (!oldAccounts.includes(address.toLowerCase())) {
|
// Convert the address to lowercase for consistent comparisons
|
||||||
const label = newAccounts
|
const lowercasedAddress = address.toLowerCase();
|
||||||
.filter((item) => item.toLowerCase() === address)
|
|
||||||
.map((item) => accounts[item].name)[0];
|
// If the address is not in oldAccounts
|
||||||
this.preferencesController.setAccountLabel(address, label);
|
if (!oldAccountsSet.has(lowercasedAddress)) {
|
||||||
|
// Look up the label in the map
|
||||||
|
const label = accountNameMap[lowercasedAddress];
|
||||||
|
|
||||||
|
// If the label is defined
|
||||||
|
if (label) {
|
||||||
|
// Set the label for the address
|
||||||
|
this.preferencesController.setAccountLabel(address, label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -569,7 +590,7 @@ export default class MMIController extends EventEmitter {
|
|||||||
const mmiDashboardData = await this.handleMmiDashboardData();
|
const mmiDashboardData = await this.handleMmiDashboardData();
|
||||||
const cookieSetUrls =
|
const cookieSetUrls =
|
||||||
this.mmiConfigurationController.store.mmiConfiguration?.portfolio
|
this.mmiConfigurationController.store.mmiConfiguration?.portfolio
|
||||||
?.cookieSetUrls;
|
?.cookieSetUrls || [];
|
||||||
setDashboardCookie(mmiDashboardData, cookieSetUrls);
|
setDashboardCookie(mmiDashboardData, cookieSetUrls);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -578,7 +599,12 @@ export default class MMIController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async newUnsignedMessage(msgParams, req, version) {
|
async newUnsignedMessage(msgParams, req, version) {
|
||||||
const updatedMsgParams = { ...msgParams, deferSetAsSigned: true };
|
// The code path triggered by deferSetAsSigned: true is for custodial accounts
|
||||||
|
const accountDetails = this.custodyController.getAccountDetails(
|
||||||
|
msgParams.from,
|
||||||
|
);
|
||||||
|
const isCustodial = Boolean(accountDetails);
|
||||||
|
const updatedMsgParams = { ...msgParams, deferSetAsSigned: isCustodial };
|
||||||
|
|
||||||
if (req.method.includes('eth_signTypedData')) {
|
if (req.method.includes('eth_signTypedData')) {
|
||||||
return await this.signatureController.newUnsignedTypedMessage(
|
return await this.signatureController.newUnsignedTypedMessage(
|
||||||
|
@ -53,6 +53,7 @@ describe('MMIController', function () {
|
|||||||
onInfuraIsBlocked: jest.fn(),
|
onInfuraIsBlocked: jest.fn(),
|
||||||
onInfuraIsUnblocked: jest.fn(),
|
onInfuraIsUnblocked: jest.fn(),
|
||||||
provider: {},
|
provider: {},
|
||||||
|
networkConfigurations: {},
|
||||||
}),
|
}),
|
||||||
appStateController: new AppStateController({
|
appStateController: new AppStateController({
|
||||||
addUnlockListener: jest.fn(),
|
addUnlockListener: jest.fn(),
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { ObservableStore } from '@metamask/obs-store';
|
import { ObservableStore } from '@metamask/obs-store';
|
||||||
import { normalize as normalizeAddress } from 'eth-sig-util';
|
import { normalize as normalizeAddress } from 'eth-sig-util';
|
||||||
import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network';
|
import {
|
||||||
|
CHAIN_IDS,
|
||||||
|
IPFS_DEFAULT_GATEWAY_URL,
|
||||||
|
} from '../../../shared/constants/network';
|
||||||
import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets';
|
import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets';
|
||||||
import { ThemeType } from '../../../shared/constants/preferences';
|
import { ThemeType } from '../../../shared/constants/preferences';
|
||||||
import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils';
|
import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils';
|
||||||
@ -8,6 +11,17 @@ import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils';
|
|||||||
import { KEYRING_SNAPS_REGISTRY_URL } from '../../../shared/constants/app';
|
import { KEYRING_SNAPS_REGISTRY_URL } from '../../../shared/constants/app';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
|
const mainNetworks = {
|
||||||
|
[CHAIN_IDS.MAINNET]: true,
|
||||||
|
[CHAIN_IDS.LINEA_MAINNET]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const testNetworks = {
|
||||||
|
[CHAIN_IDS.GOERLI]: true,
|
||||||
|
[CHAIN_IDS.SEPOLIA]: true,
|
||||||
|
[CHAIN_IDS.LINEA_GOERLI]: true,
|
||||||
|
};
|
||||||
|
|
||||||
export default class PreferencesController {
|
export default class PreferencesController {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -25,6 +39,13 @@ export default class PreferencesController {
|
|||||||
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
|
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
|
||||||
*/
|
*/
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
const addedNonMainNetwork = Object.values(
|
||||||
|
opts.networkConfigurations,
|
||||||
|
).reduce((acc, element) => {
|
||||||
|
acc[element.chainId] = true;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
const initState = {
|
const initState = {
|
||||||
useBlockie: false,
|
useBlockie: false,
|
||||||
useNonceField: false,
|
useNonceField: false,
|
||||||
@ -51,8 +72,11 @@ export default class PreferencesController {
|
|||||||
// Feature flag toggling is available in the global namespace
|
// Feature flag toggling is available in the global namespace
|
||||||
// for convenient testing of pre-release features, and should never
|
// for convenient testing of pre-release features, and should never
|
||||||
// perform sensitive operations.
|
// perform sensitive operations.
|
||||||
featureFlags: {
|
featureFlags: {},
|
||||||
showIncomingTransactions: true,
|
incomingTransactionsPreferences: {
|
||||||
|
...mainNetworks,
|
||||||
|
...addedNonMainNetwork,
|
||||||
|
...testNetworks,
|
||||||
},
|
},
|
||||||
knownMethodData: {},
|
knownMethodData: {},
|
||||||
currentLocale: opts.initLangCode,
|
currentLocale: opts.initLangCode,
|
||||||
@ -84,6 +108,7 @@ export default class PreferencesController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.network = opts.network;
|
this.network = opts.network;
|
||||||
|
|
||||||
this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
|
this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
|
||||||
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
|
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
|
||||||
this.store = new ObservableStore(initState);
|
this.store = new ObservableStore(initState);
|
||||||
@ -448,7 +473,7 @@ export default class PreferencesController {
|
|||||||
* found in the settings page.
|
* found in the settings page.
|
||||||
*
|
*
|
||||||
* @param {string} preference - The preference to enable or disable.
|
* @param {string} preference - The preference to enable or disable.
|
||||||
* @param {boolean} value - Indicates whether or not the preference should be enabled or disabled.
|
* @param {boolean |object} value - Indicates whether or not the preference should be enabled or disabled.
|
||||||
* @returns {Promise<object>} Promises a new object; the updated preferences object.
|
* @returns {Promise<object>} Promises a new object; the updated preferences object.
|
||||||
*/
|
*/
|
||||||
async setPreference(preference, value) {
|
async setPreference(preference, value) {
|
||||||
@ -550,6 +575,18 @@ export default class PreferencesController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setter for the incomingTransactions in preference to be updated
|
||||||
|
*
|
||||||
|
* @param {string} chainId - chainId of the network
|
||||||
|
* @param {bool} value - preference of certain network, true to be enabled
|
||||||
|
*/
|
||||||
|
setIncomingTransactionsPreferences(chainId, value) {
|
||||||
|
const previousValue = this.store.getState().incomingTransactionsPreferences;
|
||||||
|
const updatedValue = { ...previousValue, [chainId]: value };
|
||||||
|
this.store.updateState({ incomingTransactionsPreferences: updatedValue });
|
||||||
|
}
|
||||||
|
|
||||||
getRpcMethodPreferences() {
|
getRpcMethodPreferences() {
|
||||||
return this.store.getState().disabledRpcMethodPreferences;
|
return this.store.getState().disabledRpcMethodPreferences;
|
||||||
}
|
}
|
||||||
@ -570,6 +607,7 @@ export default class PreferencesController {
|
|||||||
}
|
}
|
||||||
this.store.updateState({ snapRegistryList: snapRegistry });
|
this.store.updateState({ snapRegistryList: snapRegistry });
|
||||||
}
|
}
|
||||||
|
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -1,67 +1,85 @@
|
|||||||
import { strict as assert } from 'assert';
|
/**
|
||||||
import sinon from 'sinon';
|
* @jest-environment node
|
||||||
|
*/
|
||||||
import { ControllerMessenger } from '@metamask/base-controller';
|
import { ControllerMessenger } from '@metamask/base-controller';
|
||||||
import { TokenListController } from '@metamask/assets-controllers';
|
import { TokenListController } from '@metamask/assets-controllers';
|
||||||
|
import { CHAIN_IDS } from '../../../shared/constants/network';
|
||||||
import PreferencesController from './preferences';
|
import PreferencesController from './preferences';
|
||||||
|
|
||||||
describe('preferences controller', function () {
|
const NETWORK_CONFIGURATION_DATA = {
|
||||||
|
'test-networkConfigurationId-1': {
|
||||||
|
rpcUrl: 'https://testrpc.com',
|
||||||
|
chainId: CHAIN_IDS.GOERLI,
|
||||||
|
nickname: '0X5',
|
||||||
|
rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' },
|
||||||
|
},
|
||||||
|
'test-networkConfigurationId-2': {
|
||||||
|
rpcUrl: 'http://localhost:8545',
|
||||||
|
chainId: '0x539',
|
||||||
|
ticker: 'ETH',
|
||||||
|
nickname: 'Localhost 8545',
|
||||||
|
rpcPrefs: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
describe('preferences controller', () => {
|
||||||
let preferencesController;
|
let preferencesController;
|
||||||
let tokenListController;
|
let tokenListController;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
const tokenListMessenger = new ControllerMessenger().getRestricted({
|
const tokenListMessenger = new ControllerMessenger().getRestricted({
|
||||||
name: 'TokenListController',
|
name: 'TokenListController',
|
||||||
});
|
});
|
||||||
tokenListController = new TokenListController({
|
tokenListController = new TokenListController({
|
||||||
chainId: '1',
|
chainId: '1',
|
||||||
preventPollingOnNetworkRestart: false,
|
preventPollingOnNetworkRestart: false,
|
||||||
onNetworkStateChange: sinon.spy(),
|
onNetworkStateChange: jest.fn(),
|
||||||
onPreferencesStateChange: sinon.spy(),
|
onPreferencesStateChange: jest.fn(),
|
||||||
messenger: tokenListMessenger,
|
messenger: tokenListMessenger,
|
||||||
});
|
});
|
||||||
|
|
||||||
preferencesController = new PreferencesController({
|
preferencesController = new PreferencesController({
|
||||||
initLangCode: 'en_US',
|
initLangCode: 'en_US',
|
||||||
tokenListController,
|
tokenListController,
|
||||||
onInfuraIsBlocked: sinon.spy(),
|
onInfuraIsBlocked: jest.fn(),
|
||||||
onInfuraIsUnblocked: sinon.spy(),
|
onInfuraIsUnblocked: jest.fn(),
|
||||||
|
networkConfigurations: NETWORK_CONFIGURATION_DATA,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
describe('useBlockie', () => {
|
||||||
sinon.restore();
|
it('defaults useBlockie to false', () => {
|
||||||
});
|
expect(preferencesController.store.getState().useBlockie).toStrictEqual(
|
||||||
|
false,
|
||||||
describe('useBlockie', function () {
|
);
|
||||||
it('defaults useBlockie to false', function () {
|
|
||||||
assert.equal(preferencesController.store.getState().useBlockie, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setUseBlockie to true', function () {
|
it('setUseBlockie to true', () => {
|
||||||
preferencesController.setUseBlockie(true);
|
preferencesController.setUseBlockie(true);
|
||||||
assert.equal(preferencesController.store.getState().useBlockie, true);
|
expect(preferencesController.store.getState().useBlockie).toStrictEqual(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setCurrentLocale', function () {
|
describe('setCurrentLocale', () => {
|
||||||
it('checks the default currentLocale', function () {
|
it('checks the default currentLocale', () => {
|
||||||
const { currentLocale } = preferencesController.store.getState();
|
const { currentLocale } = preferencesController.store.getState();
|
||||||
assert.equal(currentLocale, 'en_US');
|
expect(currentLocale).toStrictEqual('en_US');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets current locale in preferences controller', function () {
|
it('sets current locale in preferences controller', () => {
|
||||||
preferencesController.setCurrentLocale('ja');
|
preferencesController.setCurrentLocale('ja');
|
||||||
const { currentLocale } = preferencesController.store.getState();
|
const { currentLocale } = preferencesController.store.getState();
|
||||||
assert.equal(currentLocale, 'ja');
|
expect(currentLocale).toStrictEqual('ja');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setAddresses', function () {
|
describe('setAddresses', () => {
|
||||||
it('should keep a map of addresses to names and addresses in the store', function () {
|
it('should keep a map of addresses to names and addresses in the store', () => {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
|
|
||||||
const { identities } = preferencesController.store.getState();
|
const { identities } = preferencesController.store.getState();
|
||||||
assert.deepEqual(identities, {
|
expect(identities).toStrictEqual({
|
||||||
'0xda22le': {
|
'0xda22le': {
|
||||||
name: 'Account 1',
|
name: 'Account 1',
|
||||||
address: '0xda22le',
|
address: '0xda22le',
|
||||||
@ -73,12 +91,12 @@ describe('preferences controller', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace its list of addresses', function () {
|
it('should replace its list of addresses', () => {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
preferencesController.setAddresses(['0xda22le77', '0x7e57e277']);
|
preferencesController.setAddresses(['0xda22le77', '0x7e57e277']);
|
||||||
|
|
||||||
const { identities } = preferencesController.store.getState();
|
const { identities } = preferencesController.store.getState();
|
||||||
assert.deepEqual(identities, {
|
expect(identities).toStrictEqual({
|
||||||
'0xda22le77': {
|
'0xda22le77': {
|
||||||
name: 'Account 1',
|
name: 'Account 1',
|
||||||
address: '0xda22le77',
|
address: '0xda22le77',
|
||||||
@ -91,237 +109,235 @@ describe('preferences controller', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeAddress', function () {
|
describe('removeAddress', () => {
|
||||||
it('should remove an address from state', function () {
|
it('should remove an address from state', () => {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
|
|
||||||
preferencesController.removeAddress('0xda22le');
|
preferencesController.removeAddress('0xda22le');
|
||||||
|
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().identities['0xda22le'],
|
preferencesController.store.getState().identities['0xda22le'],
|
||||||
undefined,
|
).toStrictEqual(undefined);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch accounts if the selected address is removed', function () {
|
it('should switch accounts if the selected address is removed', () => {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
|
|
||||||
preferencesController.setSelectedAddress('0x7e57e2');
|
preferencesController.setSelectedAddress('0x7e57e2');
|
||||||
preferencesController.removeAddress('0x7e57e2');
|
preferencesController.removeAddress('0x7e57e2');
|
||||||
|
expect(preferencesController.getSelectedAddress()).toStrictEqual(
|
||||||
assert.equal(preferencesController.getSelectedAddress(), '0xda22le');
|
'0xda22le',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setAccountLabel', function () {
|
describe('setAccountLabel', () => {
|
||||||
it('should update a label for the given account', function () {
|
it('should update a label for the given account', () => {
|
||||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||||
|
|
||||||
assert.deepEqual(
|
expect(
|
||||||
preferencesController.store.getState().identities['0xda22le'],
|
preferencesController.store.getState().identities['0xda22le'],
|
||||||
{
|
).toStrictEqual({
|
||||||
name: 'Account 1',
|
name: 'Account 1',
|
||||||
address: '0xda22le',
|
address: '0xda22le',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
preferencesController.setAccountLabel('0xda22le', 'Dazzle');
|
preferencesController.setAccountLabel('0xda22le', 'Dazzle');
|
||||||
assert.deepEqual(
|
expect(
|
||||||
preferencesController.store.getState().identities['0xda22le'],
|
preferencesController.store.getState().identities['0xda22le'],
|
||||||
{
|
).toStrictEqual({
|
||||||
name: 'Dazzle',
|
name: 'Dazzle',
|
||||||
address: '0xda22le',
|
address: '0xda22le',
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setPasswordForgotten', function () {
|
describe('setPasswordForgotten', () => {
|
||||||
it('should default to false', function () {
|
it('should default to false', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.forgottenPassword, false);
|
preferencesController.store.getState().forgottenPassword,
|
||||||
|
).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the forgottenPassword property in state', function () {
|
it('should set the forgottenPassword property in state', () => {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().forgottenPassword,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
preferencesController.setPasswordForgotten(true);
|
preferencesController.setPasswordForgotten(true);
|
||||||
|
expect(
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().forgottenPassword,
|
preferencesController.store.getState().forgottenPassword,
|
||||||
true,
|
).toStrictEqual(true);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUsePhishDetect', function () {
|
describe('setUsePhishDetect', () => {
|
||||||
it('should default to true', function () {
|
it('should default to true', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.usePhishDetect, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the usePhishDetect property in state', function () {
|
|
||||||
assert.equal(preferencesController.store.getState().usePhishDetect, true);
|
|
||||||
preferencesController.setUsePhishDetect(false);
|
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().usePhishDetect,
|
preferencesController.store.getState().usePhishDetect,
|
||||||
false,
|
).toStrictEqual(true);
|
||||||
);
|
});
|
||||||
|
|
||||||
|
it('should set the usePhishDetect property in state', () => {
|
||||||
|
preferencesController.setUsePhishDetect(false);
|
||||||
|
expect(
|
||||||
|
preferencesController.store.getState().usePhishDetect,
|
||||||
|
).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUseMultiAccountBalanceChecker', function () {
|
describe('setUseMultiAccountBalanceChecker', () => {
|
||||||
it('should default to true', function () {
|
it('should default to true', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.useMultiAccountBalanceChecker, true);
|
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
||||||
|
).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the setUseMultiAccountBalanceChecker property in state', function () {
|
it('should set the setUseMultiAccountBalanceChecker property in state', () => {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
preferencesController.setUseMultiAccountBalanceChecker(false);
|
preferencesController.setUseMultiAccountBalanceChecker(false);
|
||||||
|
expect(
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
||||||
false,
|
).toStrictEqual(false);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUseTokenDetection', function () {
|
describe('setUseTokenDetection', () => {
|
||||||
it('should default to false', function () {
|
it('should default to false', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.useTokenDetection, false);
|
preferencesController.store.getState().useTokenDetection,
|
||||||
|
).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the useTokenDetection property in state', function () {
|
it('should set the useTokenDetection property in state', () => {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().useTokenDetection,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
preferencesController.setUseTokenDetection(true);
|
preferencesController.setUseTokenDetection(true);
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().useTokenDetection,
|
preferencesController.store.getState().useTokenDetection,
|
||||||
true,
|
).toStrictEqual(true);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUseNftDetection', function () {
|
describe('setUseNftDetection', () => {
|
||||||
it('should default to false', function () {
|
it('should default to false', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.useNftDetection, false);
|
preferencesController.store.getState().useNftDetection,
|
||||||
|
).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the useNftDetection property in state', function () {
|
it('should set the useNftDetection property in state', () => {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().useNftDetection,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
preferencesController.setOpenSeaEnabled(true);
|
preferencesController.setOpenSeaEnabled(true);
|
||||||
preferencesController.setUseNftDetection(true);
|
preferencesController.setUseNftDetection(true);
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().useNftDetection,
|
preferencesController.store.getState().useNftDetection,
|
||||||
true,
|
).toStrictEqual(true);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUse4ByteResolution', function () {
|
describe('setUse4ByteResolution', function () {
|
||||||
it('should default to true', function () {
|
it('should default to true', function () {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.use4ByteResolution, true);
|
preferencesController.store.getState().use4ByteResolution,
|
||||||
|
).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the use4ByteResolution property in state', function () {
|
it('should set the use4ByteResolution property in state', function () {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().use4ByteResolution,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
preferencesController.setUse4ByteResolution(false);
|
preferencesController.setUse4ByteResolution(false);
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().use4ByteResolution,
|
preferencesController.store.getState().use4ByteResolution,
|
||||||
false,
|
).toStrictEqual(false);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setOpenSeaEnabled', function () {
|
describe('setOpenSeaEnabled', () => {
|
||||||
it('should default to false', function () {
|
it('should default to false', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.openSeaEnabled, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the openSeaEnabled property in state', function () {
|
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().openSeaEnabled,
|
preferencesController.store.getState().openSeaEnabled,
|
||||||
false,
|
).toStrictEqual(false);
|
||||||
);
|
});
|
||||||
|
|
||||||
|
it('should set the openSeaEnabled property in state', () => {
|
||||||
preferencesController.setOpenSeaEnabled(true);
|
preferencesController.setOpenSeaEnabled(true);
|
||||||
assert.equal(preferencesController.store.getState().openSeaEnabled, true);
|
expect(
|
||||||
|
preferencesController.store.getState().openSeaEnabled,
|
||||||
|
).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setAdvancedGasFee', function () {
|
describe('setAdvancedGasFee', () => {
|
||||||
it('should default to null', function () {
|
it('should default to null', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.advancedGasFee, null);
|
preferencesController.store.getState().advancedGasFee,
|
||||||
|
).toStrictEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the setAdvancedGasFee property in state', function () {
|
it('should set the setAdvancedGasFee property in state', () => {
|
||||||
const state = preferencesController.store.getState();
|
|
||||||
assert.equal(state.advancedGasFee, null);
|
|
||||||
preferencesController.setAdvancedGasFee({
|
preferencesController.setAdvancedGasFee({
|
||||||
maxBaseFee: '1.5',
|
maxBaseFee: '1.5',
|
||||||
priorityFee: '2',
|
priorityFee: '2',
|
||||||
});
|
});
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().advancedGasFee.maxBaseFee,
|
preferencesController.store.getState().advancedGasFee.maxBaseFee,
|
||||||
'1.5',
|
).toStrictEqual('1.5');
|
||||||
);
|
expect(
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().advancedGasFee.priorityFee,
|
preferencesController.store.getState().advancedGasFee.priorityFee,
|
||||||
'2',
|
).toStrictEqual('2');
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setTheme', function () {
|
describe('setTheme', () => {
|
||||||
it('should default to value "OS"', function () {
|
it('should default to value "OS"', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(preferencesController.store.getState().theme).toStrictEqual('os');
|
||||||
assert.equal(state.theme, 'os');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the setTheme property in state', function () {
|
it('should set the setTheme property in state', () => {
|
||||||
const state = preferencesController.store.getState();
|
|
||||||
assert.equal(state.theme, 'os');
|
|
||||||
preferencesController.setTheme('dark');
|
preferencesController.setTheme('dark');
|
||||||
assert.equal(preferencesController.store.getState().theme, 'dark');
|
expect(preferencesController.store.getState().theme).toStrictEqual(
|
||||||
|
'dark',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setUseCurrencyRateCheck', function () {
|
describe('setUseCurrencyRateCheck', () => {
|
||||||
it('should default to false', function () {
|
it('should default to false', () => {
|
||||||
const state = preferencesController.store.getState();
|
expect(
|
||||||
assert.equal(state.useCurrencyRateCheck, true);
|
preferencesController.store.getState().useCurrencyRateCheck,
|
||||||
|
).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the useCurrencyRateCheck property in state', function () {
|
it('should set the useCurrencyRateCheck property in state', () => {
|
||||||
assert.equal(
|
|
||||||
preferencesController.store.getState().useCurrencyRateCheck,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
preferencesController.setUseCurrencyRateCheck(false);
|
preferencesController.setUseCurrencyRateCheck(false);
|
||||||
assert.equal(
|
expect(
|
||||||
preferencesController.store.getState().useCurrencyRateCheck,
|
preferencesController.store.getState().useCurrencyRateCheck,
|
||||||
|
).toStrictEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setIncomingTransactionsPreferences', () => {
|
||||||
|
const addedNonTestNetworks = Object.keys(NETWORK_CONFIGURATION_DATA);
|
||||||
|
|
||||||
|
it('should have default value combined', () => {
|
||||||
|
const state = preferencesController.store.getState();
|
||||||
|
expect(state.incomingTransactionsPreferences).toStrictEqual({
|
||||||
|
[CHAIN_IDS.MAINNET]: true,
|
||||||
|
[CHAIN_IDS.LINEA_MAINNET]: true,
|
||||||
|
[NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[0]].chainId]: true,
|
||||||
|
[NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[1]].chainId]: true,
|
||||||
|
[CHAIN_IDS.GOERLI]: true,
|
||||||
|
[CHAIN_IDS.SEPOLIA]: true,
|
||||||
|
[CHAIN_IDS.LINEA_GOERLI]: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update incomingTransactionsPreferences with given value set', () => {
|
||||||
|
preferencesController.setIncomingTransactionsPreferences(
|
||||||
|
[CHAIN_IDS.LINEA_MAINNET],
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
const state = preferencesController.store.getState();
|
||||||
|
expect(state.incomingTransactionsPreferences).toStrictEqual({
|
||||||
|
[CHAIN_IDS.MAINNET]: true,
|
||||||
|
[CHAIN_IDS.LINEA_MAINNET]: false,
|
||||||
|
[NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[0]].chainId]: true,
|
||||||
|
[NETWORK_CONFIGURATION_DATA[addedNonTestNetworks[1]].chainId]: true,
|
||||||
|
[CHAIN_IDS.GOERLI]: true,
|
||||||
|
[CHAIN_IDS.SEPOLIA]: true,
|
||||||
|
[CHAIN_IDS.LINEA_GOERLI]: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,219 @@
|
|||||||
|
import { CHAIN_IDS } from '../../../../shared/constants/network';
|
||||||
|
import {
|
||||||
|
TransactionStatus,
|
||||||
|
TransactionType,
|
||||||
|
} from '../../../../shared/constants/transaction';
|
||||||
|
import createRandomId from '../../../../shared/modules/random-id';
|
||||||
|
import type {
|
||||||
|
EtherscanTokenTransactionMeta,
|
||||||
|
EtherscanTransactionMeta,
|
||||||
|
EtherscanTransactionMetaBase,
|
||||||
|
EtherscanTransactionResponse,
|
||||||
|
} from './etherscan';
|
||||||
|
import {
|
||||||
|
fetchEtherscanTokenTransactions,
|
||||||
|
fetchEtherscanTransactions,
|
||||||
|
} from './etherscan';
|
||||||
|
import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource';
|
||||||
|
|
||||||
|
jest.mock('./etherscan', () => ({
|
||||||
|
fetchEtherscanTransactions: jest.fn(),
|
||||||
|
fetchEtherscanTokenTransactions: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../shared/modules/random-id');
|
||||||
|
|
||||||
|
const ID_MOCK = 123;
|
||||||
|
|
||||||
|
const ETHERSCAN_TRANSACTION_BASE_MOCK: EtherscanTransactionMetaBase = {
|
||||||
|
blockNumber: '4535105',
|
||||||
|
confirmations: '4',
|
||||||
|
contractAddress: '',
|
||||||
|
cumulativeGasUsed: '693910',
|
||||||
|
from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207',
|
||||||
|
gas: '335208',
|
||||||
|
gasPrice: '20000000000',
|
||||||
|
gasUsed: '21000',
|
||||||
|
hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff91',
|
||||||
|
nonce: '1',
|
||||||
|
timeStamp: '1543596356',
|
||||||
|
transactionIndex: '13',
|
||||||
|
value: '50000000000000000',
|
||||||
|
blockHash: '0x0000000001',
|
||||||
|
to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TRANSACTION_SUCCESS_MOCK: EtherscanTransactionMeta = {
|
||||||
|
...ETHERSCAN_TRANSACTION_BASE_MOCK,
|
||||||
|
functionName: 'testFunction',
|
||||||
|
input: '0x',
|
||||||
|
isError: '0',
|
||||||
|
methodId: 'testId',
|
||||||
|
txreceipt_status: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TRANSACTION_ERROR_MOCK: EtherscanTransactionMeta = {
|
||||||
|
...ETHERSCAN_TRANSACTION_SUCCESS_MOCK,
|
||||||
|
isError: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TOKEN_TRANSACTION_MOCK: EtherscanTokenTransactionMeta = {
|
||||||
|
...ETHERSCAN_TRANSACTION_BASE_MOCK,
|
||||||
|
tokenDecimal: '456',
|
||||||
|
tokenName: 'TestToken',
|
||||||
|
tokenSymbol: 'ABC',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse<EtherscanTransactionMeta> =
|
||||||
|
{
|
||||||
|
result: [
|
||||||
|
ETHERSCAN_TRANSACTION_SUCCESS_MOCK,
|
||||||
|
ETHERSCAN_TRANSACTION_ERROR_MOCK,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse<EtherscanTokenTransactionMeta> =
|
||||||
|
{
|
||||||
|
result: [
|
||||||
|
ETHERSCAN_TOKEN_TRANSACTION_MOCK,
|
||||||
|
ETHERSCAN_TOKEN_TRANSACTION_MOCK,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse<EtherscanTransactionMeta> =
|
||||||
|
{
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse<EtherscanTokenTransactionMeta> =
|
||||||
|
ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK as any;
|
||||||
|
|
||||||
|
const EXPECTED_NORMALISED_TRANSACTION_BASE = {
|
||||||
|
blockNumber: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.blockNumber,
|
||||||
|
chainId: undefined,
|
||||||
|
hash: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.hash,
|
||||||
|
id: ID_MOCK,
|
||||||
|
metamaskNetworkId: undefined,
|
||||||
|
status: TransactionStatus.confirmed,
|
||||||
|
time: 1543596356000,
|
||||||
|
txParams: {
|
||||||
|
from: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.from,
|
||||||
|
gas: '0x51d68',
|
||||||
|
gasPrice: '0x4a817c800',
|
||||||
|
nonce: '0x1',
|
||||||
|
to: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.to,
|
||||||
|
value: '0xb1a2bc2ec50000',
|
||||||
|
},
|
||||||
|
type: TransactionType.incoming,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXPECTED_NORMALISED_TRANSACTION_SUCCESS = {
|
||||||
|
...EXPECTED_NORMALISED_TRANSACTION_BASE,
|
||||||
|
txParams: {
|
||||||
|
...EXPECTED_NORMALISED_TRANSACTION_BASE.txParams,
|
||||||
|
data: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.input,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXPECTED_NORMALISED_TRANSACTION_ERROR = {
|
||||||
|
...EXPECTED_NORMALISED_TRANSACTION_SUCCESS,
|
||||||
|
status: TransactionStatus.failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXPECTED_NORMALISED_TOKEN_TRANSACTION = {
|
||||||
|
...EXPECTED_NORMALISED_TRANSACTION_BASE,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('EtherscanRemoteTransactionSource', () => {
|
||||||
|
const fetchEtherscanTransactionsMock =
|
||||||
|
fetchEtherscanTransactions as jest.MockedFn<
|
||||||
|
typeof fetchEtherscanTransactions
|
||||||
|
>;
|
||||||
|
|
||||||
|
const fetchEtherscanTokenTransactionsMock =
|
||||||
|
fetchEtherscanTokenTransactions as jest.MockedFn<
|
||||||
|
typeof fetchEtherscanTokenTransactions
|
||||||
|
>;
|
||||||
|
|
||||||
|
const createIdMock = createRandomId as jest.MockedFn<typeof createRandomId>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
|
||||||
|
fetchEtherscanTransactionsMock.mockResolvedValue(
|
||||||
|
ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchEtherscanTokenTransactionsMock.mockResolvedValue(
|
||||||
|
ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_EMPTY_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
createIdMock.mockReturnValue(ID_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSupportedNetwork', () => {
|
||||||
|
it('returns true if chain ID in constant', () => {
|
||||||
|
expect(
|
||||||
|
new EtherscanRemoteTransactionSource().isSupportedNetwork(
|
||||||
|
CHAIN_IDS.MAINNET,
|
||||||
|
'1',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if chain ID not in constant', () => {
|
||||||
|
expect(
|
||||||
|
new EtherscanRemoteTransactionSource().isSupportedNetwork(
|
||||||
|
CHAIN_IDS.LOCALHOST,
|
||||||
|
'1',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchTransactions', () => {
|
||||||
|
it('returns normalized transactions fetched from Etherscan', async () => {
|
||||||
|
fetchEtherscanTransactionsMock.mockResolvedValueOnce(
|
||||||
|
ETHERSCAN_TRANSACTION_RESPONSE_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactions =
|
||||||
|
await new EtherscanRemoteTransactionSource().fetchTransactions(
|
||||||
|
{} as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual([
|
||||||
|
EXPECTED_NORMALISED_TRANSACTION_SUCCESS,
|
||||||
|
EXPECTED_NORMALISED_TRANSACTION_ERROR,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns normalized token transactions fetched from Etherscan', async () => {
|
||||||
|
fetchEtherscanTokenTransactionsMock.mockResolvedValueOnce(
|
||||||
|
ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactions =
|
||||||
|
await new EtherscanRemoteTransactionSource().fetchTransactions(
|
||||||
|
{} as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual([
|
||||||
|
EXPECTED_NORMALISED_TOKEN_TRANSACTION,
|
||||||
|
EXPECTED_NORMALISED_TOKEN_TRANSACTION,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns no normalized token transactions if flag disabled', async () => {
|
||||||
|
fetchEtherscanTokenTransactionsMock.mockResolvedValueOnce(
|
||||||
|
ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactions = await new EtherscanRemoteTransactionSource({
|
||||||
|
includeTokenTransfers: false,
|
||||||
|
}).fetchTransactions({} as any);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,156 @@
|
|||||||
|
import { BNToHex } from '@metamask/controller-utils';
|
||||||
|
import type { Hex } from '@metamask/utils';
|
||||||
|
import { BN } from 'ethereumjs-util';
|
||||||
|
import createId from '../../../../shared/modules/random-id';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TransactionMeta,
|
||||||
|
TransactionStatus,
|
||||||
|
TransactionType,
|
||||||
|
} from '../../../../shared/constants/transaction';
|
||||||
|
import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../../shared/constants/network';
|
||||||
|
import type {
|
||||||
|
EtherscanTokenTransactionMeta,
|
||||||
|
EtherscanTransactionMeta,
|
||||||
|
EtherscanTransactionMetaBase,
|
||||||
|
EtherscanTransactionRequest,
|
||||||
|
EtherscanTransactionResponse,
|
||||||
|
} from './etherscan';
|
||||||
|
import {
|
||||||
|
fetchEtherscanTokenTransactions,
|
||||||
|
fetchEtherscanTransactions,
|
||||||
|
} from './etherscan';
|
||||||
|
import {
|
||||||
|
RemoteTransactionSource,
|
||||||
|
RemoteTransactionSourceRequest,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A RemoteTransactionSource that fetches transaction data from Etherscan.
|
||||||
|
*/
|
||||||
|
export class EtherscanRemoteTransactionSource
|
||||||
|
implements RemoteTransactionSource
|
||||||
|
{
|
||||||
|
#apiKey?: string;
|
||||||
|
|
||||||
|
#includeTokenTransfers: boolean;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
apiKey,
|
||||||
|
includeTokenTransfers,
|
||||||
|
}: { apiKey?: string; includeTokenTransfers?: boolean } = {}) {
|
||||||
|
this.#apiKey = apiKey;
|
||||||
|
this.#includeTokenTransfers = includeTokenTransfers ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupportedNetwork(chainId: Hex, _networkId: string): boolean {
|
||||||
|
return Object.keys(ETHERSCAN_SUPPORTED_NETWORKS).includes(chainId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTransactions(
|
||||||
|
request: RemoteTransactionSourceRequest,
|
||||||
|
): Promise<TransactionMeta[]> {
|
||||||
|
const etherscanRequest: EtherscanTransactionRequest = {
|
||||||
|
...request,
|
||||||
|
apiKey: this.#apiKey,
|
||||||
|
chainId: request.currentChainId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactionPromise = fetchEtherscanTransactions(etherscanRequest);
|
||||||
|
|
||||||
|
const tokenTransactionPromise = this.#includeTokenTransfers
|
||||||
|
? fetchEtherscanTokenTransactions(etherscanRequest)
|
||||||
|
: Promise.resolve({
|
||||||
|
result: [] as EtherscanTokenTransactionMeta[],
|
||||||
|
} as EtherscanTransactionResponse<EtherscanTokenTransactionMeta>);
|
||||||
|
|
||||||
|
const [etherscanTransactions, etherscanTokenTransactions] =
|
||||||
|
await Promise.all([transactionPromise, tokenTransactionPromise]);
|
||||||
|
|
||||||
|
const transactions = etherscanTransactions.result.map((tx) =>
|
||||||
|
this.#normalizeTransaction(
|
||||||
|
tx,
|
||||||
|
request.currentNetworkId,
|
||||||
|
request.currentChainId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenTransactions = etherscanTokenTransactions.result.map((tx) =>
|
||||||
|
this.#normalizeTokenTransaction(
|
||||||
|
tx,
|
||||||
|
request.currentNetworkId,
|
||||||
|
request.currentChainId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...transactions, ...tokenTransactions];
|
||||||
|
}
|
||||||
|
|
||||||
|
#normalizeTransaction(
|
||||||
|
txMeta: EtherscanTransactionMeta,
|
||||||
|
currentNetworkId: string,
|
||||||
|
currentChainId: Hex,
|
||||||
|
): TransactionMeta {
|
||||||
|
const base = this.#normalizeTransactionBase(
|
||||||
|
txMeta,
|
||||||
|
currentNetworkId,
|
||||||
|
currentChainId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
txParams: {
|
||||||
|
...base.txParams,
|
||||||
|
data: txMeta.input,
|
||||||
|
},
|
||||||
|
...(txMeta.isError === '0'
|
||||||
|
? { status: TransactionStatus.confirmed }
|
||||||
|
: {
|
||||||
|
status: TransactionStatus.failed,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#normalizeTokenTransaction(
|
||||||
|
txMeta: EtherscanTokenTransactionMeta,
|
||||||
|
currentNetworkId: string,
|
||||||
|
currentChainId: Hex,
|
||||||
|
): TransactionMeta {
|
||||||
|
const base = this.#normalizeTransactionBase(
|
||||||
|
txMeta,
|
||||||
|
currentNetworkId,
|
||||||
|
currentChainId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#normalizeTransactionBase(
|
||||||
|
txMeta: EtherscanTransactionMetaBase,
|
||||||
|
currentNetworkId: string,
|
||||||
|
currentChainId: Hex,
|
||||||
|
): TransactionMeta {
|
||||||
|
const time = parseInt(txMeta.timeStamp, 10) * 1000;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber: txMeta.blockNumber,
|
||||||
|
chainId: currentChainId,
|
||||||
|
hash: txMeta.hash,
|
||||||
|
id: createId(),
|
||||||
|
metamaskNetworkId: currentNetworkId,
|
||||||
|
status: TransactionStatus.confirmed,
|
||||||
|
time,
|
||||||
|
txParams: {
|
||||||
|
from: txMeta.from,
|
||||||
|
gas: BNToHex(new BN(txMeta.gas)),
|
||||||
|
gasPrice: BNToHex(new BN(txMeta.gasPrice)),
|
||||||
|
nonce: BNToHex(new BN(txMeta.nonce)),
|
||||||
|
to: txMeta.to,
|
||||||
|
value: BNToHex(new BN(txMeta.value)),
|
||||||
|
},
|
||||||
|
type: TransactionType.incoming,
|
||||||
|
} as TransactionMeta;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,585 @@
|
|||||||
|
import { NetworkType } from '@metamask/controller-utils';
|
||||||
|
import type { BlockTracker, NetworkState } from '@metamask/network-controller';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TransactionMeta,
|
||||||
|
TransactionStatus,
|
||||||
|
} from '../../../../shared/constants/transaction';
|
||||||
|
import { IncomingTransactionHelper } from './IncomingTransactionHelper';
|
||||||
|
import { RemoteTransactionSource } from './types';
|
||||||
|
|
||||||
|
jest.mock('@metamask/controller-utils', () => ({
|
||||||
|
...jest.requireActual('@metamask/controller-utils'),
|
||||||
|
isSmartContractCode: jest.fn(),
|
||||||
|
query: () => Promise.resolve({}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NETWORK_STATE_MOCK: NetworkState = {
|
||||||
|
providerConfig: {
|
||||||
|
chainId: '0x1',
|
||||||
|
type: NetworkType.mainnet,
|
||||||
|
},
|
||||||
|
networkId: '1',
|
||||||
|
} as unknown as NetworkState;
|
||||||
|
|
||||||
|
const ADDERSS_MOCK = '0x1';
|
||||||
|
const FROM_BLOCK_HEX_MOCK = '0x20';
|
||||||
|
const FROM_BLOCK_DECIMAL_MOCK = 32;
|
||||||
|
|
||||||
|
const BLOCK_TRACKER_MOCK = {
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
getLatestBlock: jest.fn(() => FROM_BLOCK_HEX_MOCK),
|
||||||
|
} as unknown as jest.Mocked<BlockTracker>;
|
||||||
|
|
||||||
|
const CONTROLLER_ARGS_MOCK = {
|
||||||
|
blockTracker: BLOCK_TRACKER_MOCK,
|
||||||
|
getCurrentAccount: () => ADDERSS_MOCK,
|
||||||
|
getNetworkState: () => NETWORK_STATE_MOCK,
|
||||||
|
remoteTransactionSource: {} as RemoteTransactionSource,
|
||||||
|
transactionLimit: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRANSACTION_MOCK: TransactionMeta = {
|
||||||
|
blockNumber: '123',
|
||||||
|
chainId: '0x1',
|
||||||
|
status: TransactionStatus.submitted,
|
||||||
|
time: 0,
|
||||||
|
txParams: { to: '0x1' },
|
||||||
|
} as unknown as TransactionMeta;
|
||||||
|
|
||||||
|
const TRANSACTION_MOCK_2: TransactionMeta = {
|
||||||
|
blockNumber: '234',
|
||||||
|
chainId: '0x1',
|
||||||
|
hash: '0x2',
|
||||||
|
time: 1,
|
||||||
|
txParams: { to: '0x1' },
|
||||||
|
} as unknown as TransactionMeta;
|
||||||
|
|
||||||
|
const createRemoteTransactionSourceMock = (
|
||||||
|
remoteTransactions: TransactionMeta[],
|
||||||
|
{
|
||||||
|
isSupportedNetwork,
|
||||||
|
error,
|
||||||
|
}: { isSupportedNetwork?: boolean; error?: boolean } = {},
|
||||||
|
): RemoteTransactionSource => ({
|
||||||
|
isSupportedNetwork: jest.fn(() => isSupportedNetwork ?? true),
|
||||||
|
fetchTransactions: jest.fn(() =>
|
||||||
|
error
|
||||||
|
? Promise.reject(new Error('Test Error'))
|
||||||
|
: Promise.resolve(remoteTransactions),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function emitBlockTrackerLatestEvent(
|
||||||
|
helper: IncomingTransactionHelper,
|
||||||
|
{ start, error }: { start?: boolean; error?: boolean } = {},
|
||||||
|
) {
|
||||||
|
const transactionsListener = jest.fn();
|
||||||
|
const blockNumberListener = jest.fn();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
transactionsListener.mockImplementation(() => {
|
||||||
|
throw new Error('Test Error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.hub.addListener('transactions', transactionsListener);
|
||||||
|
helper.hub.addListener('updatedLastFetchedBlockNumbers', blockNumberListener);
|
||||||
|
|
||||||
|
if (start !== false) {
|
||||||
|
helper.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
await BLOCK_TRACKER_MOCK.addListener.mock.calls[0]?.[1]?.(
|
||||||
|
FROM_BLOCK_HEX_MOCK,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactions: transactionsListener.mock.calls[0]?.[0],
|
||||||
|
lastFetchedBlockNumbers:
|
||||||
|
blockNumberListener.mock.calls[0]?.[0].lastFetchedBlockNumbers,
|
||||||
|
transactionsListener,
|
||||||
|
blockNumberListener,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('IncomingTransactionHelper', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on block tracker latest event', () => {
|
||||||
|
it('handles errors', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK_2,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
await emitBlockTrackerLatestEvent(helper, { error: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetches remote transactions', () => {
|
||||||
|
it('using remote transaction source', async () => {
|
||||||
|
const remoteTransactionSource = createRemoteTransactionSourceMock([]);
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledTimes(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith({
|
||||||
|
address: ADDERSS_MOCK,
|
||||||
|
currentChainId: NETWORK_STATE_MOCK.providerConfig.chainId,
|
||||||
|
currentNetworkId: NETWORK_STATE_MOCK.networkId,
|
||||||
|
fromBlock: expect.any(Number),
|
||||||
|
limit: CONTROLLER_ARGS_MOCK.transactionLimit,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('using from block as latest block minus ten if no last fetched data', async () => {
|
||||||
|
const remoteTransactionSource = createRemoteTransactionSourceMock([]);
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledTimes(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
fromBlock: FROM_BLOCK_DECIMAL_MOCK - 10,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('using from block as last fetched value plus one', async () => {
|
||||||
|
const remoteTransactionSource = createRemoteTransactionSourceMock([]);
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource,
|
||||||
|
lastFetchedBlockNumbers: {
|
||||||
|
[`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]:
|
||||||
|
FROM_BLOCK_DECIMAL_MOCK,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledTimes(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
fromBlock: FROM_BLOCK_DECIMAL_MOCK + 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emits transactions event', () => {
|
||||||
|
it('if new transaction fetched', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK_2,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactions } = await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual({
|
||||||
|
added: [TRANSACTION_MOCK_2],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if new outgoing transaction fetched and update transactions enabled', async () => {
|
||||||
|
const outgoingTransaction = {
|
||||||
|
...TRANSACTION_MOCK_2,
|
||||||
|
txParams: {
|
||||||
|
...TRANSACTION_MOCK_2.txParams,
|
||||||
|
from: '0x1',
|
||||||
|
to: '0x2',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
outgoingTransaction,
|
||||||
|
]),
|
||||||
|
updateTransactions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactions } = await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual({
|
||||||
|
added: [outgoingTransaction],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if existing transaction fetched with different status and update transactions enabled', async () => {
|
||||||
|
const updatedTransaction = {
|
||||||
|
...TRANSACTION_MOCK,
|
||||||
|
status: TransactionStatus.confirmed,
|
||||||
|
} as TransactionMeta;
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
updatedTransaction,
|
||||||
|
]),
|
||||||
|
getLocalTransactions: () => [TRANSACTION_MOCK],
|
||||||
|
updateTransactions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactions } = await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual({
|
||||||
|
added: [],
|
||||||
|
updated: [updatedTransaction],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorted by time in ascending order', async () => {
|
||||||
|
const firstTransaction = { ...TRANSACTION_MOCK, time: 5 };
|
||||||
|
const secondTransaction = { ...TRANSACTION_MOCK, time: 6 };
|
||||||
|
const thirdTransaction = { ...TRANSACTION_MOCK, time: 7 };
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
firstTransaction,
|
||||||
|
thirdTransaction,
|
||||||
|
secondTransaction,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactions } = await emitBlockTrackerLatestEvent(helper);
|
||||||
|
|
||||||
|
expect(transactions).toStrictEqual({
|
||||||
|
added: [firstTransaction, secondTransaction, thirdTransaction],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if identical transaction fetched and update transactions enabled', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK,
|
||||||
|
]),
|
||||||
|
getLocalTransactions: () => [TRANSACTION_MOCK],
|
||||||
|
updateTransactions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if disabled', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK,
|
||||||
|
]),
|
||||||
|
isEnabled: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(true)
|
||||||
|
.mockReturnValueOnce(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if current network is not supported by remote transaction source', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock(
|
||||||
|
[TRANSACTION_MOCK],
|
||||||
|
{ isSupportedNetwork: false },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if no remote transactions', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if update transactions disabled and no incoming transactions', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
{
|
||||||
|
...TRANSACTION_MOCK,
|
||||||
|
txParams: { to: '0x2' },
|
||||||
|
} as TransactionMeta,
|
||||||
|
{
|
||||||
|
...TRANSACTION_MOCK,
|
||||||
|
txParams: { to: undefined } as any,
|
||||||
|
} as TransactionMeta,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if error fetching transactions', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock(
|
||||||
|
[TRANSACTION_MOCK],
|
||||||
|
{ error: true },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if not started', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { transactionsListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
{ start: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(transactionsListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emits updatedLastFetchedBlockNumbers event', () => {
|
||||||
|
it('if fetched transaction has higher block number', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK_2,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFetchedBlockNumbers } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFetchedBlockNumbers).toStrictEqual({
|
||||||
|
[`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]:
|
||||||
|
parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if no fetched transactions', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { blockNumberListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(blockNumberListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if no block number on fetched transaction', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
{ ...TRANSACTION_MOCK_2, blockNumber: undefined },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { blockNumberListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(blockNumberListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if fetch transaction not to current account', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
{
|
||||||
|
...TRANSACTION_MOCK_2,
|
||||||
|
txParams: { to: '0x2' },
|
||||||
|
} as TransactionMeta,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { blockNumberListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(blockNumberListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not if fetched transaction has same block number', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK_2,
|
||||||
|
]),
|
||||||
|
lastFetchedBlockNumbers: {
|
||||||
|
[`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDERSS_MOCK}`]:
|
||||||
|
parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { blockNumberListener } = await emitBlockTrackerLatestEvent(
|
||||||
|
helper,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(blockNumberListener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('start', () => {
|
||||||
|
it('adds listener to block tracker', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.start();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
CONTROLLER_ARGS_MOCK.blockTracker.addListener,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if already started', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.start();
|
||||||
|
helper.start();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
CONTROLLER_ARGS_MOCK.blockTracker.addListener,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if disabled', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
isEnabled: () => false,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.start();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
CONTROLLER_ARGS_MOCK.blockTracker.addListener,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if network not supported by remote transaction source', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([], {
|
||||||
|
isSupportedNetwork: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.start();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
CONTROLLER_ARGS_MOCK.blockTracker.addListener,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stop', () => {
|
||||||
|
it('removes listener from block tracker', async () => {
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.start();
|
||||||
|
helper.stop();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
CONTROLLER_ARGS_MOCK.blockTracker.removeListener,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('emits transactions event', async () => {
|
||||||
|
const listener = jest.fn();
|
||||||
|
|
||||||
|
const helper = new IncomingTransactionHelper({
|
||||||
|
...CONTROLLER_ARGS_MOCK,
|
||||||
|
remoteTransactionSource: createRemoteTransactionSourceMock([
|
||||||
|
TRANSACTION_MOCK_2,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
helper.hub.on('transactions', listener);
|
||||||
|
|
||||||
|
await helper.update();
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
expect(listener).toHaveBeenCalledWith({
|
||||||
|
added: [TRANSACTION_MOCK_2],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,282 @@
|
|||||||
|
import EventEmitter from 'events';
|
||||||
|
import type { BlockTracker, NetworkState } from '@metamask/network-controller';
|
||||||
|
import type { Hex } from '@metamask/utils';
|
||||||
|
|
||||||
|
import log from 'loglevel';
|
||||||
|
import { TransactionMeta } from '../../../../shared/constants/transaction';
|
||||||
|
import { RemoteTransactionSource } from './types';
|
||||||
|
|
||||||
|
const UPDATE_CHECKS: ((txMeta: TransactionMeta) => any)[] = [
|
||||||
|
(txMeta) => txMeta.status,
|
||||||
|
];
|
||||||
|
|
||||||
|
export class IncomingTransactionHelper {
|
||||||
|
hub: EventEmitter;
|
||||||
|
|
||||||
|
#blockTracker: BlockTracker;
|
||||||
|
|
||||||
|
#getCurrentAccount: () => string;
|
||||||
|
|
||||||
|
#getLocalTransactions: () => TransactionMeta[];
|
||||||
|
|
||||||
|
#getNetworkState: () => NetworkState;
|
||||||
|
|
||||||
|
#isEnabled: () => boolean;
|
||||||
|
|
||||||
|
#isRunning: boolean;
|
||||||
|
|
||||||
|
#isUpdating: boolean;
|
||||||
|
|
||||||
|
#lastFetchedBlockNumbers: Record<string, number>;
|
||||||
|
|
||||||
|
#onLatestBlock: (blockNumberHex: Hex) => Promise<void>;
|
||||||
|
|
||||||
|
#remoteTransactionSource: RemoteTransactionSource;
|
||||||
|
|
||||||
|
#transactionLimit?: number;
|
||||||
|
|
||||||
|
#updateTransactions: boolean;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
blockTracker,
|
||||||
|
getCurrentAccount,
|
||||||
|
getLocalTransactions,
|
||||||
|
getNetworkState,
|
||||||
|
isEnabled,
|
||||||
|
lastFetchedBlockNumbers,
|
||||||
|
remoteTransactionSource,
|
||||||
|
transactionLimit,
|
||||||
|
updateTransactions,
|
||||||
|
}: {
|
||||||
|
blockTracker: BlockTracker;
|
||||||
|
getCurrentAccount: () => string;
|
||||||
|
getNetworkState: () => NetworkState;
|
||||||
|
getLocalTransactions?: () => TransactionMeta[];
|
||||||
|
isEnabled?: () => boolean;
|
||||||
|
lastFetchedBlockNumbers?: Record<string, number>;
|
||||||
|
remoteTransactionSource: RemoteTransactionSource;
|
||||||
|
transactionLimit?: number;
|
||||||
|
updateTransactions?: boolean;
|
||||||
|
}) {
|
||||||
|
this.hub = new EventEmitter();
|
||||||
|
|
||||||
|
this.#blockTracker = blockTracker;
|
||||||
|
this.#getCurrentAccount = getCurrentAccount;
|
||||||
|
this.#getLocalTransactions = getLocalTransactions || (() => []);
|
||||||
|
this.#getNetworkState = getNetworkState;
|
||||||
|
this.#isEnabled = isEnabled ?? (() => true);
|
||||||
|
this.#isRunning = false;
|
||||||
|
this.#isUpdating = false;
|
||||||
|
this.#lastFetchedBlockNumbers = lastFetchedBlockNumbers ?? {};
|
||||||
|
this.#remoteTransactionSource = remoteTransactionSource;
|
||||||
|
this.#transactionLimit = transactionLimit;
|
||||||
|
this.#updateTransactions = updateTransactions ?? false;
|
||||||
|
|
||||||
|
// Using a property instead of a method to provide a listener reference
|
||||||
|
// with the correct scope that we can remove later if stopped.
|
||||||
|
this.#onLatestBlock = async (blockNumberHex: Hex) => {
|
||||||
|
await this.update(blockNumberHex);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.#isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#canStart()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#blockTracker.addListener('latest', this.#onLatestBlock);
|
||||||
|
this.#isRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.#blockTracker.removeListener('latest', this.#onLatestBlock);
|
||||||
|
this.#isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(latestBlockNumberHex?: Hex): Promise<void> {
|
||||||
|
if (this.#isUpdating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#isUpdating = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.#canStart()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestBlockNumber = parseInt(
|
||||||
|
latestBlockNumberHex || (await this.#blockTracker.getLatestBlock()),
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromBlock = this.#getFromBlock(latestBlockNumber);
|
||||||
|
const address = this.#getCurrentAccount();
|
||||||
|
const currentChainId = this.#getCurrentChainId();
|
||||||
|
const currentNetworkId = this.#getCurrentNetworkId();
|
||||||
|
|
||||||
|
let remoteTransactions = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
remoteTransactions =
|
||||||
|
await this.#remoteTransactionSource.fetchTransactions({
|
||||||
|
address,
|
||||||
|
currentChainId,
|
||||||
|
currentNetworkId,
|
||||||
|
fromBlock,
|
||||||
|
limit: this.#transactionLimit,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#updateTransactions) {
|
||||||
|
remoteTransactions = remoteTransactions.filter(
|
||||||
|
(tx) => tx.txParams.to?.toLowerCase() === address.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const localTransactions = this.#updateTransactions
|
||||||
|
? this.#getLocalTransactions()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const newTransactions = this.#getNewTransactions(
|
||||||
|
remoteTransactions,
|
||||||
|
localTransactions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedTransactions = this.#getUpdatedTransactions(
|
||||||
|
remoteTransactions,
|
||||||
|
localTransactions,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newTransactions.length > 0 || updatedTransactions.length > 0) {
|
||||||
|
this.#sortTransactionsByTime(newTransactions);
|
||||||
|
this.#sortTransactionsByTime(updatedTransactions);
|
||||||
|
|
||||||
|
this.hub.emit('transactions', {
|
||||||
|
added: newTransactions,
|
||||||
|
updated: updatedTransactions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateLastFetchedBlockNumber(remoteTransactions);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error while checking incoming transactions', error);
|
||||||
|
} finally {
|
||||||
|
this.#isUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#sortTransactionsByTime(transactions: TransactionMeta[]) {
|
||||||
|
transactions.sort((a, b) => (a.time < b.time ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#getNewTransactions(
|
||||||
|
remoteTxs: TransactionMeta[],
|
||||||
|
localTxs: TransactionMeta[],
|
||||||
|
): TransactionMeta[] {
|
||||||
|
return remoteTxs.filter(
|
||||||
|
(tx) => !localTxs.some(({ hash }) => hash === tx.hash),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getUpdatedTransactions(
|
||||||
|
remoteTxs: TransactionMeta[],
|
||||||
|
localTxs: TransactionMeta[],
|
||||||
|
): TransactionMeta[] {
|
||||||
|
return remoteTxs.filter((remoteTx) =>
|
||||||
|
localTxs.some(
|
||||||
|
(localTx) =>
|
||||||
|
remoteTx.hash === localTx.hash &&
|
||||||
|
this.#isTransactionOutdated(remoteTx, localTx),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#isTransactionOutdated(
|
||||||
|
remoteTx: TransactionMeta,
|
||||||
|
localTx: TransactionMeta,
|
||||||
|
): boolean {
|
||||||
|
return UPDATE_CHECKS.some(
|
||||||
|
(getValue) => getValue(remoteTx) !== getValue(localTx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getFromBlock(latestBlockNumber: number): number {
|
||||||
|
const lastFetchedKey = this.#getBlockNumberKey();
|
||||||
|
|
||||||
|
const lastFetchedBlockNumber =
|
||||||
|
this.#lastFetchedBlockNumbers[lastFetchedKey];
|
||||||
|
|
||||||
|
if (lastFetchedBlockNumber) {
|
||||||
|
return lastFetchedBlockNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid using latest block as remote transaction source
|
||||||
|
// may not have indexed it yet
|
||||||
|
return Math.max(latestBlockNumber - 10, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateLastFetchedBlockNumber(remoteTxs: TransactionMeta[]) {
|
||||||
|
let lastFetchedBlockNumber = -1;
|
||||||
|
|
||||||
|
for (const tx of remoteTxs) {
|
||||||
|
const currentBlockNumberValue = tx.blockNumber
|
||||||
|
? parseInt(tx.blockNumber, 10)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
lastFetchedBlockNumber = Math.max(
|
||||||
|
lastFetchedBlockNumber,
|
||||||
|
currentBlockNumberValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFetchedBlockNumber === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastFetchedKey = this.#getBlockNumberKey();
|
||||||
|
const previousValue = this.#lastFetchedBlockNumbers[lastFetchedKey];
|
||||||
|
|
||||||
|
if (previousValue === lastFetchedBlockNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#lastFetchedBlockNumbers[lastFetchedKey] = lastFetchedBlockNumber;
|
||||||
|
|
||||||
|
this.hub.emit('updatedLastFetchedBlockNumbers', {
|
||||||
|
lastFetchedBlockNumbers: this.#lastFetchedBlockNumbers,
|
||||||
|
blockNumber: lastFetchedBlockNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#getBlockNumberKey(): string {
|
||||||
|
return `${this.#getCurrentChainId()}#${this.#getCurrentAccount().toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canStart(): boolean {
|
||||||
|
const isEnabled = this.#isEnabled();
|
||||||
|
const currentChainId = this.#getCurrentChainId();
|
||||||
|
const currentNetworkId = this.#getCurrentNetworkId();
|
||||||
|
|
||||||
|
const isSupportedNetwork = this.#remoteTransactionSource.isSupportedNetwork(
|
||||||
|
currentChainId,
|
||||||
|
currentNetworkId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isEnabled && isSupportedNetwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getCurrentChainId(): Hex {
|
||||||
|
return this.#getNetworkState().providerConfig.chainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getCurrentNetworkId(): string {
|
||||||
|
return this.#getNetworkState().networkId as string;
|
||||||
|
}
|
||||||
|
}
|
153
app/scripts/controllers/transactions/etherscan.test.ts
Normal file
153
app/scripts/controllers/transactions/etherscan.test.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { handleFetch } from '@metamask/controller-utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHAIN_IDS,
|
||||||
|
ETHERSCAN_SUPPORTED_NETWORKS,
|
||||||
|
} from '../../../../shared/constants/network';
|
||||||
|
import type {
|
||||||
|
EtherscanTransactionMeta,
|
||||||
|
EtherscanTransactionRequest,
|
||||||
|
EtherscanTransactionResponse,
|
||||||
|
} from './etherscan';
|
||||||
|
import * as Etherscan from './etherscan';
|
||||||
|
|
||||||
|
jest.mock('@metamask/controller-utils', () => ({
|
||||||
|
...jest.requireActual('@metamask/controller-utils'),
|
||||||
|
handleFetch: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ADDERSS_MOCK = '0x2A2D72308838A6A46a0B5FDA3055FE915b5D99eD';
|
||||||
|
|
||||||
|
const REQUEST_MOCK: EtherscanTransactionRequest = {
|
||||||
|
address: ADDERSS_MOCK,
|
||||||
|
chainId: CHAIN_IDS.GOERLI,
|
||||||
|
limit: 3,
|
||||||
|
fromBlock: 2,
|
||||||
|
apiKey: 'testApiKey',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESPONSE_MOCK: EtherscanTransactionResponse<EtherscanTransactionMeta> = {
|
||||||
|
result: [
|
||||||
|
{ from: ADDERSS_MOCK, nonce: '0x1' } as EtherscanTransactionMeta,
|
||||||
|
{ from: ADDERSS_MOCK, nonce: '0x2' } as EtherscanTransactionMeta,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Etherscan', () => {
|
||||||
|
const handleFetchMock = handleFetch as jest.MockedFunction<
|
||||||
|
typeof handleFetch
|
||||||
|
>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
['fetchEtherscanTransactions', 'txlist'],
|
||||||
|
['fetchEtherscanTokenTransactions', 'tokentx'],
|
||||||
|
])('%s', (method, action) => {
|
||||||
|
it('returns fetched response', async () => {
|
||||||
|
handleFetchMock.mockResolvedValueOnce(RESPONSE_MOCK);
|
||||||
|
|
||||||
|
const result = await (Etherscan as any)[method](REQUEST_MOCK);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual(RESPONSE_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches from Etherscan URL', async () => {
|
||||||
|
handleFetchMock.mockResolvedValueOnce(RESPONSE_MOCK);
|
||||||
|
|
||||||
|
await (Etherscan as any)[method](REQUEST_MOCK);
|
||||||
|
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledWith(
|
||||||
|
`https://${ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].subdomain}.${
|
||||||
|
ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].domain
|
||||||
|
}/api?` +
|
||||||
|
`module=account` +
|
||||||
|
`&address=${REQUEST_MOCK.address}` +
|
||||||
|
`&startBlock=${REQUEST_MOCK.fromBlock}` +
|
||||||
|
`&apikey=${REQUEST_MOCK.apiKey}` +
|
||||||
|
`&offset=${REQUEST_MOCK.limit}` +
|
||||||
|
`&order=desc` +
|
||||||
|
`&action=${action}` +
|
||||||
|
`&tag=latest` +
|
||||||
|
`&page=1`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports alternate networks', async () => {
|
||||||
|
handleFetchMock.mockResolvedValueOnce(RESPONSE_MOCK);
|
||||||
|
|
||||||
|
await (Etherscan as any)[method]({
|
||||||
|
...REQUEST_MOCK,
|
||||||
|
chainId: CHAIN_IDS.MAINNET,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledWith(
|
||||||
|
`https://${ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.MAINNET].subdomain}.${
|
||||||
|
ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.MAINNET].domain
|
||||||
|
}/api?` +
|
||||||
|
`module=account` +
|
||||||
|
`&address=${REQUEST_MOCK.address}` +
|
||||||
|
`&startBlock=${REQUEST_MOCK.fromBlock}` +
|
||||||
|
`&apikey=${REQUEST_MOCK.apiKey}` +
|
||||||
|
`&offset=${REQUEST_MOCK.limit}` +
|
||||||
|
`&order=desc` +
|
||||||
|
`&action=${action}` +
|
||||||
|
`&tag=latest` +
|
||||||
|
`&page=1`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if message is not ok', async () => {
|
||||||
|
handleFetchMock.mockResolvedValueOnce({
|
||||||
|
status: '0',
|
||||||
|
message: 'NOTOK',
|
||||||
|
result: 'test error',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect((Etherscan as any)[method](REQUEST_MOCK)).rejects.toThrow(
|
||||||
|
'Etherscan request failed - test error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if chain is not supported', async () => {
|
||||||
|
const unsupportedChainId = '0x11111111111111111111';
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(Etherscan as any)[method]({
|
||||||
|
...REQUEST_MOCK,
|
||||||
|
chainId: unsupportedChainId,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
`Etherscan does not support chain with ID: ${unsupportedChainId}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include empty values in fetched URL', async () => {
|
||||||
|
handleFetchMock.mockResolvedValueOnce(RESPONSE_MOCK);
|
||||||
|
|
||||||
|
await (Etherscan as any)[method]({
|
||||||
|
...REQUEST_MOCK,
|
||||||
|
fromBlock: undefined,
|
||||||
|
apiKey: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handleFetchMock).toHaveBeenCalledWith(
|
||||||
|
`https://${ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].subdomain}.${
|
||||||
|
ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].domain
|
||||||
|
}/api?` +
|
||||||
|
`module=account` +
|
||||||
|
`&address=${REQUEST_MOCK.address}` +
|
||||||
|
`&offset=${REQUEST_MOCK.limit}` +
|
||||||
|
`&order=desc` +
|
||||||
|
`&action=${action}` +
|
||||||
|
`&tag=latest` +
|
||||||
|
`&page=1`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
205
app/scripts/controllers/transactions/etherscan.ts
Normal file
205
app/scripts/controllers/transactions/etherscan.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { handleFetch } from '@metamask/controller-utils';
|
||||||
|
import { Hex } from '@metamask/utils';
|
||||||
|
import { ETHERSCAN_SUPPORTED_NETWORKS } from '../../../../shared/constants/network';
|
||||||
|
|
||||||
|
export interface EtherscanTransactionMetaBase {
|
||||||
|
blockNumber: string;
|
||||||
|
blockHash: string;
|
||||||
|
confirmations: string;
|
||||||
|
contractAddress: string;
|
||||||
|
cumulativeGasUsed: string;
|
||||||
|
from: string;
|
||||||
|
gas: string;
|
||||||
|
gasPrice: string;
|
||||||
|
gasUsed: string;
|
||||||
|
hash: string;
|
||||||
|
nonce: string;
|
||||||
|
timeStamp: string;
|
||||||
|
to: string;
|
||||||
|
transactionIndex: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EtherscanTransactionMeta extends EtherscanTransactionMetaBase {
|
||||||
|
functionName: string;
|
||||||
|
input: string;
|
||||||
|
isError: string;
|
||||||
|
methodId: string;
|
||||||
|
txreceipt_status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EtherscanTokenTransactionMeta
|
||||||
|
extends EtherscanTransactionMetaBase {
|
||||||
|
tokenDecimal: string;
|
||||||
|
tokenName: string;
|
||||||
|
tokenSymbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EtherscanTransactionResponse<
|
||||||
|
T extends EtherscanTransactionMetaBase,
|
||||||
|
> {
|
||||||
|
result: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EtherscanTransactionRequest {
|
||||||
|
address: string;
|
||||||
|
apiKey?: string;
|
||||||
|
chainId: Hex;
|
||||||
|
fromBlock?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RawEtherscanResponse<T extends EtherscanTransactionMetaBase> {
|
||||||
|
status: '0' | '1';
|
||||||
|
message: string;
|
||||||
|
result: string | T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves transaction data from Etherscan.
|
||||||
|
*
|
||||||
|
* @param request - Configuration required to fetch transactions.
|
||||||
|
* @param request.address - Address to retrieve transactions for.
|
||||||
|
* @param request.apiKey - Etherscan API key.
|
||||||
|
* @param request.chainId - Current chain ID used to determine subdomain and domain.
|
||||||
|
* @param request.fromBlock - Block number to start fetching transactions from.
|
||||||
|
* @param request.limit - Number of transactions to retrieve.
|
||||||
|
* @returns An Etherscan response object containing the request status and an array of token transaction data.
|
||||||
|
*/
|
||||||
|
export async function fetchEtherscanTransactions({
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
chainId,
|
||||||
|
fromBlock,
|
||||||
|
limit,
|
||||||
|
}: EtherscanTransactionRequest): Promise<
|
||||||
|
EtherscanTransactionResponse<EtherscanTransactionMeta>
|
||||||
|
> {
|
||||||
|
return await fetchTransactions('txlist', {
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
chainId,
|
||||||
|
fromBlock,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves token transaction data from Etherscan.
|
||||||
|
*
|
||||||
|
* @param request - Configuration required to fetch token transactions.
|
||||||
|
* @param request.address - Address to retrieve token transactions for.
|
||||||
|
* @param request.apiKey - Etherscan API key.
|
||||||
|
* @param request.chainId - Current chain ID used to determine subdomain and domain.
|
||||||
|
* @param request.fromBlock - Block number to start fetching token transactions from.
|
||||||
|
* @param request.limit - Number of token transactions to retrieve.
|
||||||
|
* @returns An Etherscan response object containing the request status and an array of token transaction data.
|
||||||
|
*/
|
||||||
|
export async function fetchEtherscanTokenTransactions({
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
chainId,
|
||||||
|
fromBlock,
|
||||||
|
limit,
|
||||||
|
}: EtherscanTransactionRequest): Promise<
|
||||||
|
EtherscanTransactionResponse<EtherscanTokenTransactionMeta>
|
||||||
|
> {
|
||||||
|
return await fetchTransactions('tokentx', {
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
chainId,
|
||||||
|
fromBlock,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves transaction data from Etherscan from a specific endpoint.
|
||||||
|
*
|
||||||
|
* @param action - The Etherscan endpoint to use.
|
||||||
|
* @param options - Options bag.
|
||||||
|
* @param options.address - Address to retrieve transactions for.
|
||||||
|
* @param options.apiKey - Etherscan API key.
|
||||||
|
* @param options.chainId - Current chain ID used to determine subdomain and domain.
|
||||||
|
* @param options.fromBlock - Block number to start fetching transactions from.
|
||||||
|
* @param options.limit - Number of transactions to retrieve.
|
||||||
|
* @returns An object containing the request status and an array of transaction data.
|
||||||
|
*/
|
||||||
|
async function fetchTransactions<T extends EtherscanTransactionMetaBase>(
|
||||||
|
action: string,
|
||||||
|
{
|
||||||
|
address,
|
||||||
|
apiKey,
|
||||||
|
chainId,
|
||||||
|
fromBlock,
|
||||||
|
limit,
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
apiKey?: string;
|
||||||
|
chainId: Hex;
|
||||||
|
fromBlock?: number;
|
||||||
|
limit?: number;
|
||||||
|
},
|
||||||
|
): Promise<EtherscanTransactionResponse<T>> {
|
||||||
|
const urlParams = {
|
||||||
|
module: 'account',
|
||||||
|
address,
|
||||||
|
startBlock: fromBlock?.toString(),
|
||||||
|
apikey: apiKey,
|
||||||
|
offset: limit?.toString(),
|
||||||
|
order: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
const etherscanTxUrl = getEtherscanApiUrl(chainId, {
|
||||||
|
...urlParams,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = (await handleFetch(
|
||||||
|
etherscanTxUrl,
|
||||||
|
)) as RawEtherscanResponse<T>;
|
||||||
|
|
||||||
|
if (response.status === '0' && response.message === 'NOTOK') {
|
||||||
|
throw new Error(`Etherscan request failed - ${response.result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result: response.result as T[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a URL that can be used to fetch data from Etherscan.
|
||||||
|
*
|
||||||
|
* @param chainId - Current chain ID used to determine subdomain and domain.
|
||||||
|
* @param urlParams - The parameters used to construct the URL.
|
||||||
|
* @returns URL to access Etherscan data.
|
||||||
|
*/
|
||||||
|
function getEtherscanApiUrl(
|
||||||
|
chainId: Hex,
|
||||||
|
urlParams: Record<string, string | undefined>,
|
||||||
|
): string {
|
||||||
|
type SupportedChainId = keyof typeof ETHERSCAN_SUPPORTED_NETWORKS;
|
||||||
|
|
||||||
|
const networkInfo = ETHERSCAN_SUPPORTED_NETWORKS[chainId as SupportedChainId];
|
||||||
|
|
||||||
|
if (!networkInfo) {
|
||||||
|
throw new Error(`Etherscan does not support chain with ID: ${chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = `https://${networkInfo.subdomain}.${networkInfo.domain}`;
|
||||||
|
let url = `${apiUrl}/api?`;
|
||||||
|
|
||||||
|
// eslint-disable-next-line guard-for-in
|
||||||
|
for (const paramKey in urlParams) {
|
||||||
|
const value = urlParams[paramKey];
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
url += `${paramKey}=${value}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
url += 'tag=latest&page=1';
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
@ -72,6 +72,8 @@ import TransactionStateManager from './tx-state-manager';
|
|||||||
import TxGasUtil from './tx-gas-utils';
|
import TxGasUtil from './tx-gas-utils';
|
||||||
import PendingTransactionTracker from './pending-tx-tracker';
|
import PendingTransactionTracker from './pending-tx-tracker';
|
||||||
import * as txUtils from './lib/util';
|
import * as txUtils from './lib/util';
|
||||||
|
import { IncomingTransactionHelper } from './IncomingTransactionHelper';
|
||||||
|
import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource';
|
||||||
|
|
||||||
const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory
|
const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory
|
||||||
const UPDATE_POST_TX_BALANCE_TIMEOUT = 5000;
|
const UPDATE_POST_TX_BALANCE_TIMEOUT = 5000;
|
||||||
@ -127,6 +129,7 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
|
|||||||
* @param {object} opts.initState - initial transaction list default is an empty array
|
* @param {object} opts.initState - initial transaction list default is an empty array
|
||||||
* @param {Function} opts.getNetworkId - Get the current network ID.
|
* @param {Function} opts.getNetworkId - Get the current network ID.
|
||||||
* @param {Function} opts.getNetworkStatus - Get the current network status.
|
* @param {Function} opts.getNetworkStatus - Get the current network status.
|
||||||
|
* @param {Function} opts.getNetworkState - Get the network state.
|
||||||
* @param {Function} opts.onNetworkStateChange - Subscribe to network state change events.
|
* @param {Function} opts.onNetworkStateChange - Subscribe to network state change events.
|
||||||
* @param {object} opts.blockTracker - An instance of eth-blocktracker
|
* @param {object} opts.blockTracker - An instance of eth-blocktracker
|
||||||
* @param {object} opts.provider - A network provider.
|
* @param {object} opts.provider - A network provider.
|
||||||
@ -134,6 +137,7 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
|
|||||||
* @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for
|
* @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for
|
||||||
* @param {Function} opts.signTransaction - ethTx signer that returns a rawTx
|
* @param {Function} opts.signTransaction - ethTx signer that returns a rawTx
|
||||||
* @param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
|
* @param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
|
||||||
|
* @param {Function} opts.hasCompletedOnboarding - Returns whether or not the user has completed the onboarding flow
|
||||||
* @param {object} opts.preferencesStore
|
* @param {object} opts.preferencesStore
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -142,6 +146,7 @@ export default class TransactionController extends EventEmitter {
|
|||||||
super();
|
super();
|
||||||
this.getNetworkId = opts.getNetworkId;
|
this.getNetworkId = opts.getNetworkId;
|
||||||
this.getNetworkStatus = opts.getNetworkStatus;
|
this.getNetworkStatus = opts.getNetworkStatus;
|
||||||
|
this._getNetworkState = opts.getNetworkState;
|
||||||
this._getCurrentChainId = opts.getCurrentChainId;
|
this._getCurrentChainId = opts.getCurrentChainId;
|
||||||
this.getProviderConfig = opts.getProviderConfig;
|
this.getProviderConfig = opts.getProviderConfig;
|
||||||
this._getCurrentNetworkEIP1559Compatibility =
|
this._getCurrentNetworkEIP1559Compatibility =
|
||||||
@ -166,6 +171,7 @@ export default class TransactionController extends EventEmitter {
|
|||||||
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
|
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
|
||||||
this.securityProviderRequest = opts.securityProviderRequest;
|
this.securityProviderRequest = opts.securityProviderRequest;
|
||||||
this.messagingSystem = opts.messenger;
|
this.messagingSystem = opts.messenger;
|
||||||
|
this._hasCompletedOnboarding = opts.hasCompletedOnboarding;
|
||||||
|
|
||||||
this.memStore = new ObservableStore({});
|
this.memStore = new ObservableStore({});
|
||||||
|
|
||||||
@ -216,6 +222,33 @@ export default class TransactionController extends EventEmitter {
|
|||||||
this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.incomingTransactionHelper = new IncomingTransactionHelper({
|
||||||
|
blockTracker: this.blockTracker,
|
||||||
|
getCurrentAccount: () => this.getSelectedAddress(),
|
||||||
|
getNetworkState: () => this._getNetworkState(),
|
||||||
|
isEnabled: () =>
|
||||||
|
Boolean(
|
||||||
|
this.preferencesStore.getState().incomingTransactionsPreferences?.[
|
||||||
|
this._getChainId()
|
||||||
|
] && this._hasCompletedOnboarding(),
|
||||||
|
),
|
||||||
|
lastFetchedBlockNumbers: opts.initState?.lastFetchedBlockNumbers || {},
|
||||||
|
remoteTransactionSource: new EtherscanRemoteTransactionSource({
|
||||||
|
includeTokenTransfers: false,
|
||||||
|
}),
|
||||||
|
updateTransactions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.incomingTransactionHelper.hub.on(
|
||||||
|
'transactions',
|
||||||
|
this._onIncomingTransactions.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.incomingTransactionHelper.hub.on(
|
||||||
|
'updatedLastFetchedBlockNumbers',
|
||||||
|
this._onUpdatedLastFetchedBlockNumbers.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
this.txStateManager.store.subscribe(() =>
|
this.txStateManager.store.subscribe(() =>
|
||||||
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE),
|
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE),
|
||||||
);
|
);
|
||||||
@ -759,6 +792,18 @@ export default class TransactionController extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startIncomingTransactionPolling() {
|
||||||
|
this.incomingTransactionHelper.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopIncomingTransactionPolling() {
|
||||||
|
this.incomingTransactionHelper.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIncomingTransactions() {
|
||||||
|
await this.incomingTransactionHelper.update();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// PRIVATE METHODS
|
// PRIVATE METHODS
|
||||||
//
|
//
|
||||||
@ -1779,7 +1824,11 @@ export default class TransactionController extends EventEmitter {
|
|||||||
// MMI does not broadcast transactions, as that is the responsibility of the custodian
|
// MMI does not broadcast transactions, as that is the responsibility of the custodian
|
||||||
if (txMeta.custodyStatus) {
|
if (txMeta.custodyStatus) {
|
||||||
this.inProcessOfSigning.delete(txId);
|
this.inProcessOfSigning.delete(txId);
|
||||||
|
// Custodial nonces and gas params are set by the custodian, so MMI follows the approve
|
||||||
|
// workflow before the transaction parameters are sent to the keyring
|
||||||
|
this.txStateManager.setTxStatusApproved(txId);
|
||||||
await this._signTransaction(txId);
|
await this._signTransaction(txId);
|
||||||
|
// MMI relies on custodian to publish transactions so exits this code path early
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
@ -2082,11 +2131,18 @@ export default class TransactionController extends EventEmitter {
|
|||||||
* Updates the memStore in transaction controller
|
* Updates the memStore in transaction controller
|
||||||
*/
|
*/
|
||||||
_updateMemstore() {
|
_updateMemstore() {
|
||||||
|
const { transactions } = this.store.getState();
|
||||||
const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
|
const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
|
||||||
|
|
||||||
const currentNetworkTxList = this.txStateManager.getTransactions({
|
const currentNetworkTxList = this.txStateManager.getTransactions({
|
||||||
limit: MAX_MEMSTORE_TX_LIST_SIZE,
|
limit: MAX_MEMSTORE_TX_LIST_SIZE,
|
||||||
});
|
});
|
||||||
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList });
|
|
||||||
|
this.memStore.updateState({
|
||||||
|
unapprovedTxs,
|
||||||
|
currentNetworkTxList,
|
||||||
|
transactions,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_calculateTransactionsCost(txMeta, approvalTxMeta) {
|
_calculateTransactionsCost(txMeta, approvalTxMeta) {
|
||||||
@ -2730,6 +2786,34 @@ export default class TransactionController extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onIncomingTransactions({ added: transactions }) {
|
||||||
|
log.debug('Detected new incoming transactions', transactions);
|
||||||
|
|
||||||
|
const currentTransactions = this.store.getState().transactions || {};
|
||||||
|
|
||||||
|
const incomingTransactions = transactions
|
||||||
|
.filter((tx) => !this._hasTransactionHash(tx.hash, currentTransactions))
|
||||||
|
.reduce((result, tx) => {
|
||||||
|
result[tx.id] = tx;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const updatedTransactions = {
|
||||||
|
...currentTransactions,
|
||||||
|
...incomingTransactions,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.store.updateState({ transactions: updatedTransactions });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUpdatedLastFetchedBlockNumbers({ lastFetchedBlockNumbers }) {
|
||||||
|
this.store.updateState({ lastFetchedBlockNumbers });
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasTransactionHash(hash, transactions) {
|
||||||
|
return Object.values(transactions).some((tx) => tx.hash === hash);
|
||||||
|
}
|
||||||
|
|
||||||
// Approvals
|
// Approvals
|
||||||
|
|
||||||
async _requestTransactionApproval(
|
async _requestTransactionApproval(
|
||||||
|
@ -38,6 +38,7 @@ import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
|
|||||||
import { NetworkStatus } from '../../../../shared/constants/network';
|
import { NetworkStatus } from '../../../../shared/constants/network';
|
||||||
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
|
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
|
||||||
import TxGasUtil from './tx-gas-utils';
|
import TxGasUtil from './tx-gas-utils';
|
||||||
|
import * as IncomingTransactionHelperClass from './IncomingTransactionHelper';
|
||||||
import TransactionController from '.';
|
import TransactionController from '.';
|
||||||
|
|
||||||
const noop = () => true;
|
const noop = () => true;
|
||||||
@ -51,6 +52,16 @@ const actionId = 'DUMMY_ACTION_ID';
|
|||||||
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
|
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||||
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
|
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
|
||||||
|
|
||||||
|
const TRANSACTION_META_MOCK = {
|
||||||
|
hash: '0x1',
|
||||||
|
id: 1,
|
||||||
|
status: TransactionStatus.confirmed,
|
||||||
|
transaction: {
|
||||||
|
from: VALID_ADDRESS,
|
||||||
|
},
|
||||||
|
time: 123456789,
|
||||||
|
};
|
||||||
|
|
||||||
async function flushPromises() {
|
async function flushPromises() {
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
}
|
}
|
||||||
@ -65,7 +76,9 @@ describe('Transaction Controller', function () {
|
|||||||
getCurrentChainId,
|
getCurrentChainId,
|
||||||
messengerMock,
|
messengerMock,
|
||||||
resultCallbacksMock,
|
resultCallbacksMock,
|
||||||
updateSpy;
|
updateSpy,
|
||||||
|
incomingTransactionHelperClassMock,
|
||||||
|
incomingTransactionHelperEventMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
fragmentExists = false;
|
fragmentExists = false;
|
||||||
@ -101,6 +114,16 @@ describe('Transaction Controller', function () {
|
|||||||
call: sinon.stub(),
|
call: sinon.stub(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
incomingTransactionHelperEventMock = sinon.spy();
|
||||||
|
|
||||||
|
incomingTransactionHelperClassMock = sinon
|
||||||
|
.stub(IncomingTransactionHelperClass, 'IncomingTransactionHelper')
|
||||||
|
.returns({
|
||||||
|
hub: {
|
||||||
|
on: incomingTransactionHelperEventMock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
txController = new TransactionController({
|
txController = new TransactionController({
|
||||||
provider,
|
provider,
|
||||||
getGasPrice() {
|
getGasPrice() {
|
||||||
@ -148,6 +171,10 @@ describe('Transaction Controller', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
incomingTransactionHelperClassMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
function getLastTxMeta() {
|
function getLastTxMeta() {
|
||||||
return updateSpy.lastCall.args[0];
|
return updateSpy.lastCall.args[0];
|
||||||
}
|
}
|
||||||
@ -3374,4 +3401,78 @@ describe('Transaction Controller', function () {
|
|||||||
assert.deepEqual(transaction1, transaction2);
|
assert.deepEqual(transaction1, transaction2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on incoming transaction helper transactions event', function () {
|
||||||
|
it('adds new transactions to state', async function () {
|
||||||
|
const existingTransaction = TRANSACTION_META_MOCK;
|
||||||
|
|
||||||
|
const incomingTransaction1 = {
|
||||||
|
...TRANSACTION_META_MOCK,
|
||||||
|
id: 2,
|
||||||
|
hash: '0x2',
|
||||||
|
};
|
||||||
|
|
||||||
|
const incomingTransaction2 = {
|
||||||
|
...TRANSACTION_META_MOCK,
|
||||||
|
id: 3,
|
||||||
|
hash: '0x3',
|
||||||
|
};
|
||||||
|
|
||||||
|
txController.store.getState().transactions = {
|
||||||
|
[existingTransaction.id]: existingTransaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
await incomingTransactionHelperEventMock.firstCall.args[1]({
|
||||||
|
added: [incomingTransaction1, incomingTransaction2],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(txController.store.getState().transactions, {
|
||||||
|
[existingTransaction.id]: existingTransaction,
|
||||||
|
[incomingTransaction1.id]: incomingTransaction1,
|
||||||
|
[incomingTransaction2.id]: incomingTransaction2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores new transactions if hash matches existing transaction', async function () {
|
||||||
|
const existingTransaction = TRANSACTION_META_MOCK;
|
||||||
|
const incomingTransaction1 = { ...TRANSACTION_META_MOCK, id: 2 };
|
||||||
|
const incomingTransaction2 = { ...TRANSACTION_META_MOCK, id: 3 };
|
||||||
|
|
||||||
|
txController.store.getState().transactions = {
|
||||||
|
[existingTransaction.id]: existingTransaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
await incomingTransactionHelperEventMock.firstCall.args[1]({
|
||||||
|
added: [incomingTransaction1, incomingTransaction2],
|
||||||
|
updated: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(txController.store.getState().transactions, {
|
||||||
|
[existingTransaction.id]: existingTransaction,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on incoming transaction helper updatedLastFetchedBlockNumbers event', function () {
|
||||||
|
it('updates state', async function () {
|
||||||
|
const lastFetchedBlockNumbers = {
|
||||||
|
key: 234,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
txController.store.getState().lastFetchedBlockNumbers,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
await incomingTransactionHelperEventMock.secondCall.args[1]({
|
||||||
|
lastFetchedBlockNumbers,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
txController.store.getState().lastFetchedBlockNumbers,
|
||||||
|
lastFetchedBlockNumbers,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
49
app/scripts/controllers/transactions/types.ts
Normal file
49
app/scripts/controllers/transactions/types.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Hex } from '@metamask/utils';
|
||||||
|
import { TransactionMeta } from '../../../../shared/constants/transaction';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration required to fetch transaction data from a RemoteTransactionSource.
|
||||||
|
*/
|
||||||
|
export interface RemoteTransactionSourceRequest {
|
||||||
|
/**
|
||||||
|
* The address of the account to fetch transactions for.
|
||||||
|
*/
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key if required by the remote source.
|
||||||
|
*/
|
||||||
|
apiKey?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chainId of the current network.
|
||||||
|
*/
|
||||||
|
currentChainId: Hex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The networkId of the current network.
|
||||||
|
*/
|
||||||
|
currentNetworkId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block number to start fetching transactions from.
|
||||||
|
*/
|
||||||
|
fromBlock?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of transactions to retrieve.
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object capable of fetching transaction data from a remote source.
|
||||||
|
* Used by the IncomingTransactionHelper to retrieve remote transaction data.
|
||||||
|
*/
|
||||||
|
export interface RemoteTransactionSource {
|
||||||
|
isSupportedNetwork: (chainId: Hex, networkId: string) => boolean;
|
||||||
|
|
||||||
|
fetchTransactions: (
|
||||||
|
request: RemoteTransactionSourceRequest,
|
||||||
|
) => Promise<TransactionMeta[]>;
|
||||||
|
}
|
@ -51,6 +51,7 @@ export default class ComposableObservableStore extends ObservableStore {
|
|||||||
updateStructure(config) {
|
updateStructure(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
|
const initialState = {};
|
||||||
for (const key of Object.keys(config)) {
|
for (const key of Object.keys(config)) {
|
||||||
if (!config[key]) {
|
if (!config[key]) {
|
||||||
throw new Error(`Undefined '${key}'`);
|
throw new Error(`Undefined '${key}'`);
|
||||||
@ -72,7 +73,10 @@ export default class ComposableObservableStore extends ObservableStore {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialState[key] = store.state ?? store.getState?.();
|
||||||
}
|
}
|
||||||
|
this.updateState(initialState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,6 +120,46 @@ describe('ComposableObservableStore', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize state with all three types of stores', () => {
|
||||||
|
const controllerMessenger = new ControllerMessenger();
|
||||||
|
const exampleStore = new ObservableStore();
|
||||||
|
const exampleController = new ExampleController({
|
||||||
|
messenger: controllerMessenger,
|
||||||
|
});
|
||||||
|
const oldExampleController = new OldExampleController();
|
||||||
|
exampleStore.putState('state');
|
||||||
|
exampleController.updateBar('state');
|
||||||
|
oldExampleController.updateBaz('state');
|
||||||
|
const store = new ComposableObservableStore({ controllerMessenger });
|
||||||
|
|
||||||
|
store.updateStructure({
|
||||||
|
Example: exampleController,
|
||||||
|
OldExample: oldExampleController,
|
||||||
|
Store: exampleStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getState()).toStrictEqual({
|
||||||
|
Example: { bar: 'state' },
|
||||||
|
OldExample: { baz: 'state' },
|
||||||
|
Store: 'state',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize falsy state', () => {
|
||||||
|
const controllerMessenger = new ControllerMessenger();
|
||||||
|
const exampleStore = new ObservableStore();
|
||||||
|
exampleStore.putState(false);
|
||||||
|
const store = new ComposableObservableStore({ controllerMessenger });
|
||||||
|
|
||||||
|
store.updateStructure({
|
||||||
|
Example: exampleStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.getState()).toStrictEqual({
|
||||||
|
Example: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return flattened state', () => {
|
it('should return flattened state', () => {
|
||||||
const controllerMessenger = new ControllerMessenger();
|
const controllerMessenger = new ControllerMessenger();
|
||||||
const fooStore = new ObservableStore({ foo: 'foo' });
|
const fooStore = new ObservableStore({ foo: 'foo' });
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { prependZero } from '../../../shared/modules/string-utils';
|
import { prependZero } from '../../../shared/modules/string-utils';
|
||||||
|
|
||||||
export default class BackupController {
|
export default class Backup {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
const {
|
const {
|
||||||
preferencesController,
|
preferencesController,
|
@ -1,6 +1,6 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import BackupController from './backup';
|
import Backup from './backup';
|
||||||
|
|
||||||
function getMockPreferencesController() {
|
function getMockPreferencesController() {
|
||||||
const mcState = {
|
const mcState = {
|
||||||
@ -131,7 +131,6 @@ const jsonData = JSON.stringify({
|
|||||||
advancedGasFee: null,
|
advancedGasFee: null,
|
||||||
featureFlags: {
|
featureFlags: {
|
||||||
sendHexData: true,
|
sendHexData: true,
|
||||||
showIncomingTransactions: true,
|
|
||||||
},
|
},
|
||||||
knownMethodData: {},
|
knownMethodData: {},
|
||||||
currentLocale: 'en',
|
currentLocale: 'en',
|
||||||
@ -151,9 +150,9 @@ const jsonData = JSON.stringify({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('BackupController', function () {
|
describe('Backup', function () {
|
||||||
const getBackupController = () => {
|
const getBackup = () => {
|
||||||
return new BackupController({
|
return new Backup({
|
||||||
preferencesController: getMockPreferencesController(),
|
preferencesController: getMockPreferencesController(),
|
||||||
addressBookController: getMockAddressBookController(),
|
addressBookController: getMockAddressBookController(),
|
||||||
networkController: getMockNetworkController(),
|
networkController: getMockNetworkController(),
|
||||||
@ -163,85 +162,81 @@ describe('BackupController', function () {
|
|||||||
|
|
||||||
describe('constructor', function () {
|
describe('constructor', function () {
|
||||||
it('should setup correctly', async function () {
|
it('should setup correctly', async function () {
|
||||||
const backupController = getBackupController();
|
const backup = getBackup();
|
||||||
const selectedAddress =
|
const selectedAddress = backup.preferencesController.getSelectedAddress();
|
||||||
backupController.preferencesController.getSelectedAddress();
|
|
||||||
assert.equal(selectedAddress, '0x01');
|
assert.equal(selectedAddress, '0x01');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore backup', async function () {
|
it('should restore backup', async function () {
|
||||||
const backupController = getBackupController();
|
const backup = getBackup();
|
||||||
await backupController.restoreUserData(jsonData);
|
await backup.restoreUserData(jsonData);
|
||||||
// check networks backup
|
// check networks backup
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.networkController.state.networkConfigurations[
|
backup.networkController.state.networkConfigurations[
|
||||||
'network-configuration-id-1'
|
'network-configuration-id-1'
|
||||||
].chainId,
|
].chainId,
|
||||||
'0x539',
|
'0x539',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.networkController.state.networkConfigurations[
|
backup.networkController.state.networkConfigurations[
|
||||||
'network-configuration-id-2'
|
'network-configuration-id-2'
|
||||||
].chainId,
|
].chainId,
|
||||||
'0x38',
|
'0x38',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.networkController.state.networkConfigurations[
|
backup.networkController.state.networkConfigurations[
|
||||||
'network-configuration-id-3'
|
'network-configuration-id-3'
|
||||||
].chainId,
|
].chainId,
|
||||||
'0x61',
|
'0x61',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.networkController.state.networkConfigurations[
|
backup.networkController.state.networkConfigurations[
|
||||||
'network-configuration-id-4'
|
'network-configuration-id-4'
|
||||||
].chainId,
|
].chainId,
|
||||||
'0x89',
|
'0x89',
|
||||||
);
|
);
|
||||||
// make sure identities are not lost after restore
|
// make sure identities are not lost after restore
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.preferencesController.store.identities[
|
backup.preferencesController.store.identities[
|
||||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||||
].lastSelected,
|
].lastSelected,
|
||||||
1655380342907,
|
1655380342907,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.preferencesController.store.identities[
|
backup.preferencesController.store.identities[
|
||||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||||
].name,
|
].name,
|
||||||
'Account 3',
|
'Account 3',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.preferencesController.store.lostIdentities[
|
backup.preferencesController.store.lostIdentities[
|
||||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||||
].lastSelected,
|
].lastSelected,
|
||||||
1655379648197,
|
1655379648197,
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.preferencesController.store.lostIdentities[
|
backup.preferencesController.store.lostIdentities[
|
||||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||||
].name,
|
].name,
|
||||||
'Ledger 1',
|
'Ledger 1',
|
||||||
);
|
);
|
||||||
// make sure selected address is not lost after restore
|
// make sure selected address is not lost after restore
|
||||||
assert.equal(
|
assert.equal(backup.preferencesController.store.selectedAddress, '0x01');
|
||||||
backupController.preferencesController.store.selectedAddress,
|
|
||||||
'0x01',
|
|
||||||
);
|
|
||||||
// check address book backup
|
// check address book backup
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.addressBookController.store.addressBook['0x61'][
|
backup.addressBookController.store.addressBook['0x61'][
|
||||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||||
].chainId,
|
].chainId,
|
||||||
'0x61',
|
'0x61',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.addressBookController.store.addressBook['0x61'][
|
backup.addressBookController.store.addressBook['0x61'][
|
||||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||||
].address,
|
].address,
|
||||||
'0x42EB768f2244C8811C63729A21A3569731535f06',
|
'0x42EB768f2244C8811C63729A21A3569731535f06',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
backupController.addressBookController.store.addressBook['0x61'][
|
backup.addressBookController.store.addressBook['0x61'][
|
||||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||||
].isEns,
|
].isEns,
|
||||||
false,
|
false,
|
@ -16,6 +16,7 @@ export default class ExtensionStore {
|
|||||||
// once data persistence fails once and it flips true we don't send further
|
// once data persistence fails once and it flips true we don't send further
|
||||||
// data persistence errors to sentry
|
// data persistence errors to sentry
|
||||||
this.dataPersistenceFailing = false;
|
this.dataPersistenceFailing = false;
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadata(initMetaData) {
|
setMetadata(initMetaData) {
|
||||||
@ -66,8 +67,10 @@ export default class ExtensionStore {
|
|||||||
// extension.storage.local always returns an obj
|
// extension.storage.local always returns an obj
|
||||||
// if the object is empty, treat it as undefined
|
// if the object is empty, treat it as undefined
|
||||||
if (isEmpty(result)) {
|
if (isEmpty(result)) {
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
this.mostRecentRetrievedState = result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@ import browser from 'webextension-polyfill';
|
|||||||
import LocalStore from './local-store';
|
import LocalStore from './local-store';
|
||||||
|
|
||||||
jest.mock('webextension-polyfill', () => ({
|
jest.mock('webextension-polyfill', () => ({
|
||||||
|
runtime: { lastError: null },
|
||||||
storage: { local: true },
|
storage: { local: true },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setup = ({ isSupported }) => {
|
const setup = ({ localMock = jest.fn() } = {}) => {
|
||||||
browser.storage.local = isSupported;
|
browser.storage.local = localMock;
|
||||||
return new LocalStore();
|
return new LocalStore();
|
||||||
};
|
};
|
||||||
describe('LocalStore', () => {
|
describe('LocalStore', () => {
|
||||||
@ -15,21 +16,27 @@ describe('LocalStore', () => {
|
|||||||
});
|
});
|
||||||
describe('contructor', () => {
|
describe('contructor', () => {
|
||||||
it('should set isSupported property to false when browser does not support local storage', () => {
|
it('should set isSupported property to false when browser does not support local storage', () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
|
|
||||||
expect(localStore.isSupported).toBe(false);
|
expect(localStore.isSupported).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set isSupported property to true when browser supports local storage', () => {
|
it('should set isSupported property to true when browser supports local storage', () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
expect(localStore.isSupported).toBe(true);
|
expect(localStore.isSupported).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize mostRecentRetrievedState to null', () => {
|
||||||
|
const localStore = setup({ localMock: false });
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setMetadata', () => {
|
describe('setMetadata', () => {
|
||||||
it('should set the metadata property on LocalStore', () => {
|
it('should set the metadata property on LocalStore', () => {
|
||||||
const metadata = { version: 74 };
|
const metadata = { version: 74 };
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
localStore.setMetadata(metadata);
|
localStore.setMetadata(metadata);
|
||||||
|
|
||||||
expect(localStore.metadata).toStrictEqual(metadata);
|
expect(localStore.metadata).toStrictEqual(metadata);
|
||||||
@ -38,21 +45,21 @@ describe('LocalStore', () => {
|
|||||||
|
|
||||||
describe('set', () => {
|
describe('set', () => {
|
||||||
it('should throw an error if called in a browser that does not support local storage', async () => {
|
it('should throw an error if called in a browser that does not support local storage', async () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
await expect(() => localStore.set()).rejects.toThrow(
|
await expect(() => localStore.set()).rejects.toThrow(
|
||||||
'Metamask- cannot persist state to local store as this browser does not support this action',
|
'Metamask- cannot persist state to local store as this browser does not support this action',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if not passed a truthy value as an argument', async () => {
|
it('should throw an error if not passed a truthy value as an argument', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
await expect(() => localStore.set()).rejects.toThrow(
|
await expect(() => localStore.set()).rejects.toThrow(
|
||||||
'MetaMask - updated state is missing',
|
'MetaMask - updated state is missing',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if passed a valid argument but metadata has not yet been set', async () => {
|
it('should throw an error if passed a valid argument but metadata has not yet been set', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
await expect(() =>
|
await expect(() =>
|
||||||
localStore.set({ appState: { test: true } }),
|
localStore.set({ appState: { test: true } }),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
@ -61,7 +68,7 @@ describe('LocalStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw if passed a valid argument and metadata has been set', async () => {
|
it('should not throw if passed a valid argument and metadata has been set', async () => {
|
||||||
const localStore = setup({ isSupported: true });
|
const localStore = setup();
|
||||||
localStore.setMetadata({ version: 74 });
|
localStore.setMetadata({ version: 74 });
|
||||||
await expect(async function () {
|
await expect(async function () {
|
||||||
localStore.set({ appState: { test: true } });
|
localStore.set({ appState: { test: true } });
|
||||||
@ -71,9 +78,39 @@ describe('LocalStore', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should return undefined if called in a browser that does not support local storage', async () => {
|
it('should return undefined if called in a browser that does not support local storage', async () => {
|
||||||
const localStore = setup({ isSupported: false });
|
const localStore = setup({ localMock: false });
|
||||||
const result = await localStore.get();
|
const result = await localStore.get();
|
||||||
expect(result).toStrictEqual(undefined);
|
expect(result).toStrictEqual(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update mostRecentRetrievedState', async () => {
|
||||||
|
const localStore = setup({
|
||||||
|
localMock: {
|
||||||
|
get: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ appState: { test: true } }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await localStore.get();
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toStrictEqual({
|
||||||
|
appState: { test: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset mostRecentRetrievedState to null if storage.local is empty', async () => {
|
||||||
|
const localStore = setup({
|
||||||
|
localMock: {
|
||||||
|
get: jest.fn().mockImplementation(() => Promise.resolve({})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await localStore.get();
|
||||||
|
|
||||||
|
expect(localStore.mostRecentRetrievedState).toStrictEqual(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import log from 'loglevel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} Migration
|
* @typedef {object} Migration
|
||||||
@ -36,6 +37,8 @@ export default class Migrator extends EventEmitter {
|
|||||||
// perform each migration
|
// perform each migration
|
||||||
for (const migration of pendingMigrations) {
|
for (const migration of pendingMigrations) {
|
||||||
try {
|
try {
|
||||||
|
log.info(`Running migration ${migration.version}...`);
|
||||||
|
|
||||||
// attempt migration and validate
|
// attempt migration and validate
|
||||||
const migratedData = await migration.migrate(versionedData);
|
const migratedData = await migration.migrate(versionedData);
|
||||||
if (!migratedData.data) {
|
if (!migratedData.data) {
|
||||||
@ -52,6 +55,8 @@ export default class Migrator extends EventEmitter {
|
|||||||
// accept the migration as good
|
// accept the migration as good
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
versionedData = migratedData;
|
versionedData = migratedData;
|
||||||
|
|
||||||
|
log.info(`Migration ${migration.version} complete`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// rewrite error message to add context without clobbering stack
|
// rewrite error message to add context without clobbering stack
|
||||||
const originalErrorMessage = err.message;
|
const originalErrorMessage = err.message;
|
||||||
|
@ -15,6 +15,7 @@ export default class ReadOnlyNetworkStore {
|
|||||||
this._initialized = false;
|
this._initialized = false;
|
||||||
this._initializing = this._init();
|
this._initializing = this._init();
|
||||||
this._state = undefined;
|
this._state = undefined;
|
||||||
|
this.mostRecentRetrievedState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,6 +48,11 @@ export default class ReadOnlyNetworkStore {
|
|||||||
if (!this._initialized) {
|
if (!this._initialized) {
|
||||||
await this._initializing;
|
await this._initializing;
|
||||||
}
|
}
|
||||||
|
// Delay setting this until after the first read, to match the
|
||||||
|
// behavior of the local store.
|
||||||
|
if (!this.mostRecentRetrievedState) {
|
||||||
|
this.mostRecentRetrievedState = this._state;
|
||||||
|
}
|
||||||
return this._state;
|
return this._state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { ethErrors, errorCodes } from 'eth-rpc-errors';
|
|
||||||
import validUrl from 'valid-url';
|
|
||||||
import { omit } from 'lodash';
|
|
||||||
import { ApprovalType } from '@metamask/controller-utils';
|
import { ApprovalType } from '@metamask/controller-utils';
|
||||||
|
import { errorCodes, ethErrors } from 'eth-rpc-errors';
|
||||||
|
import { omit } from 'lodash';
|
||||||
import {
|
import {
|
||||||
MESSAGE_TYPE,
|
MESSAGE_TYPE,
|
||||||
UNKNOWN_TICKER_SYMBOL,
|
UNKNOWN_TICKER_SYMBOL,
|
||||||
} from '../../../../../shared/constants/app';
|
} from '../../../../../shared/constants/app';
|
||||||
|
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
|
||||||
import {
|
import {
|
||||||
isPrefixedFormattedHexString,
|
isPrefixedFormattedHexString,
|
||||||
isSafeChainId,
|
isSafeChainId,
|
||||||
} from '../../../../../shared/modules/network.utils';
|
} from '../../../../../shared/modules/network.utils';
|
||||||
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
|
import { getValidUrl } from '../../util';
|
||||||
|
|
||||||
const addEthereumChain = {
|
const addEthereumChain = {
|
||||||
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
||||||
@ -83,27 +83,25 @@ async function addEthereumChainHandler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocalhost = (strUrl) => {
|
function isLocalhostOrHttps(urlString) {
|
||||||
try {
|
const url = getValidUrl(urlString);
|
||||||
const url = new URL(strUrl);
|
|
||||||
return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
return (
|
||||||
} catch (error) {
|
url !== null &&
|
||||||
return false;
|
(url.hostname === 'localhost' ||
|
||||||
}
|
url.hostname === '127.0.0.1' ||
|
||||||
};
|
url.protocol === 'https:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const firstValidRPCUrl = Array.isArray(rpcUrls)
|
const firstValidRPCUrl = Array.isArray(rpcUrls)
|
||||||
? rpcUrls.find(
|
? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl))
|
||||||
(rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl),
|
|
||||||
)
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const firstValidBlockExplorerUrl =
|
const firstValidBlockExplorerUrl =
|
||||||
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
|
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
|
||||||
? blockExplorerUrls.find(
|
? blockExplorerUrls.find((blockExplorerUrl) =>
|
||||||
(blockExplorerUrl) =>
|
isLocalhostOrHttps(blockExplorerUrl),
|
||||||
isLocalhost(blockExplorerUrl) ||
|
|
||||||
validUrl.isHttpsUri(blockExplorerUrl),
|
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||||
import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This RPC method is called by the inpage provider whenever it detects the
|
* This RPC method is called by the inpage provider whenever it detects the
|
||||||
* accessing of a non-existent property on our window.web3 shim.
|
* accessing of a non-existent property on our window.web3 shim. We use this
|
||||||
* We collect this data to understand which sites are breaking due to the
|
* to alert the user that they are using a legacy dapp, and will have to take
|
||||||
* removal of our window.web3.
|
* further steps to be able to use it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const logWeb3ShimUsage = {
|
const logWeb3ShimUsage = {
|
||||||
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
||||||
implementation: logWeb3ShimUsageHandler,
|
implementation: logWeb3ShimUsageHandler,
|
||||||
hookNames: {
|
hookNames: {
|
||||||
sendMetrics: true,
|
|
||||||
getWeb3ShimUsageState: true,
|
getWeb3ShimUsageState: true,
|
||||||
setWeb3ShimUsageRecorded: true,
|
setWeb3ShimUsageRecorded: true,
|
||||||
},
|
},
|
||||||
@ -21,7 +18,6 @@ export default logWeb3ShimUsage;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} LogWeb3ShimUsageOptions
|
* @typedef {object} LogWeb3ShimUsageOptions
|
||||||
* @property {Function} sendMetrics - A function that registers a metrics event.
|
|
||||||
* @property {Function} getWeb3ShimUsageState - A function that gets web3 shim
|
* @property {Function} getWeb3ShimUsageState - A function that gets web3 shim
|
||||||
* usage state for the given origin.
|
* usage state for the given origin.
|
||||||
* @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim
|
* @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim
|
||||||
@ -40,24 +36,11 @@ function logWeb3ShimUsageHandler(
|
|||||||
res,
|
res,
|
||||||
_next,
|
_next,
|
||||||
end,
|
end,
|
||||||
{ sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
|
{ getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
|
||||||
) {
|
) {
|
||||||
const { origin } = req;
|
const { origin } = req;
|
||||||
if (getWeb3ShimUsageState(origin) === undefined) {
|
if (getWeb3ShimUsageState(origin) === undefined) {
|
||||||
setWeb3ShimUsageRecorded(origin);
|
setWeb3ShimUsageRecorded(origin);
|
||||||
|
|
||||||
sendMetrics(
|
|
||||||
{
|
|
||||||
event: `Website Accessed window.web3 Shim`,
|
|
||||||
category: MetaMetricsEventCategory.InpageProvider,
|
|
||||||
referrer: {
|
|
||||||
url: origin,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
excludeMetaMetricsId: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.result = true;
|
res.result = true;
|
||||||
|
76
app/scripts/lib/setup-initial-state-hooks.js
Normal file
76
app/scripts/lib/setup-initial-state-hooks.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { maskObject } from '../../../shared/modules/object.utils';
|
||||||
|
import ExtensionPlatform from '../platforms/extension';
|
||||||
|
import LocalStore from './local-store';
|
||||||
|
import ReadOnlyNetworkStore from './network-store';
|
||||||
|
import { SENTRY_BACKGROUND_STATE } from './setupSentry';
|
||||||
|
|
||||||
|
const platform = new ExtensionPlatform();
|
||||||
|
|
||||||
|
// This instance of `localStore` is used by Sentry to get the persisted state
|
||||||
|
const sentryLocalStore = process.env.IN_TEST
|
||||||
|
? new ReadOnlyNetworkStore()
|
||||||
|
: new LocalStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the persisted wallet state.
|
||||||
|
*
|
||||||
|
* @returns The persisted wallet state.
|
||||||
|
*/
|
||||||
|
globalThis.stateHooks.getPersistedState = async function () {
|
||||||
|
return await sentryLocalStore.get();
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedStateMask = {
|
||||||
|
data: SENTRY_BACKGROUND_STATE,
|
||||||
|
meta: {
|
||||||
|
version: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a state snapshot for Sentry. This is used to add additional context to
|
||||||
|
* error reports, and it's used when processing errors and breadcrumbs to
|
||||||
|
* determine whether the user has opted into Metametrics.
|
||||||
|
*
|
||||||
|
* This uses the persisted state pre-initialization, and the in-memory state
|
||||||
|
* post-initialization. In both cases the state is anonymized.
|
||||||
|
*
|
||||||
|
* @returns A Sentry state snapshot.
|
||||||
|
*/
|
||||||
|
globalThis.stateHooks.getSentryState = function () {
|
||||||
|
const sentryState = {
|
||||||
|
browser: window.navigator.userAgent,
|
||||||
|
version: platform.getVersion(),
|
||||||
|
};
|
||||||
|
// If `getSentryAppState` is set, it implies that initialization has completed
|
||||||
|
if (globalThis.stateHooks.getSentryAppState) {
|
||||||
|
return {
|
||||||
|
...sentryState,
|
||||||
|
state: globalThis.stateHooks.getSentryAppState(),
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
// This is truthy if Sentry has retrieved state at least once already. This
|
||||||
|
// should always be true when getting context for an error report, but can
|
||||||
|
// be unset when Sentry is performing the opt-in check.
|
||||||
|
sentryLocalStore.mostRecentRetrievedState ||
|
||||||
|
// This is only set in the background process.
|
||||||
|
globalThis.stateHooks.getMostRecentPersistedState
|
||||||
|
) {
|
||||||
|
const persistedState =
|
||||||
|
sentryLocalStore.mostRecentRetrievedState ||
|
||||||
|
globalThis.stateHooks.getMostRecentPersistedState();
|
||||||
|
// This can be unset when this method is called in the background for an
|
||||||
|
// opt-in check, but the state hasn't been loaded yet.
|
||||||
|
if (persistedState) {
|
||||||
|
return {
|
||||||
|
...sentryState,
|
||||||
|
persistedState: maskObject(persistedState, persistedStateMask),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This branch means that local storage has not yet been read, so we have
|
||||||
|
// no choice but to omit the application state.
|
||||||
|
// This should be unreachable when getting context for an error report, but
|
||||||
|
// can be false when Sentry is performing the opt-in check.
|
||||||
|
return sentryState;
|
||||||
|
};
|
@ -1,10 +0,0 @@
|
|||||||
import LocalStore from './local-store';
|
|
||||||
import ReadOnlyNetworkStore from './network-store';
|
|
||||||
|
|
||||||
const localStore = process.env.IN_TEST
|
|
||||||
? new ReadOnlyNetworkStore()
|
|
||||||
: new LocalStore();
|
|
||||||
|
|
||||||
globalThis.stateHooks.getPersistedState = async function () {
|
|
||||||
return await localStore.get();
|
|
||||||
};
|
|
@ -23,58 +23,172 @@ export const ERROR_URL_ALLOWLIST = {
|
|||||||
SEGMENT: 'segment.io',
|
SEGMENT: 'segment.io',
|
||||||
};
|
};
|
||||||
|
|
||||||
// This describes the subset of Redux state attached to errors sent to Sentry
|
// This describes the subset of background controller state attached to errors
|
||||||
// These properties have some potential to be useful for debugging, and they do
|
// sent to Sentry These properties have some potential to be useful for
|
||||||
// not contain any identifiable information.
|
// debugging, and they do not contain any identifiable information.
|
||||||
export const SENTRY_STATE = {
|
export const SENTRY_BACKGROUND_STATE = {
|
||||||
gas: true,
|
AccountTracker: {
|
||||||
history: true,
|
currentBlockGasLimit: true,
|
||||||
metamask: {
|
},
|
||||||
|
AlertController: {
|
||||||
alertEnabledness: true,
|
alertEnabledness: true,
|
||||||
completedOnboarding: true,
|
},
|
||||||
|
AppMetadataController: {
|
||||||
|
currentAppVersion: true,
|
||||||
|
previousAppVersion: true,
|
||||||
|
previousMigrationVersion: true,
|
||||||
|
currentMigrationVersion: true,
|
||||||
|
},
|
||||||
|
AppStateController: {
|
||||||
connectedStatusPopoverHasBeenShown: true,
|
connectedStatusPopoverHasBeenShown: true,
|
||||||
|
defaultHomeActiveTabName: true,
|
||||||
|
},
|
||||||
|
CurrencyController: {
|
||||||
conversionDate: true,
|
conversionDate: true,
|
||||||
conversionRate: true,
|
conversionRate: true,
|
||||||
currentBlockGasLimit: true,
|
|
||||||
currentCurrency: true,
|
currentCurrency: true,
|
||||||
currentLocale: true,
|
|
||||||
customNonceValue: true,
|
|
||||||
defaultHomeActiveTabName: true,
|
|
||||||
desktopEnabled: true,
|
|
||||||
featureFlags: true,
|
|
||||||
firstTimeFlowType: true,
|
|
||||||
forgottenPassword: true,
|
|
||||||
incomingTxLastFetchedBlockByChainId: true,
|
|
||||||
ipfsGateway: true,
|
|
||||||
isAccountMenuOpen: true,
|
|
||||||
isInitialized: true,
|
|
||||||
isUnlocked: true,
|
|
||||||
metaMetricsId: true,
|
|
||||||
nativeCurrency: true,
|
nativeCurrency: true,
|
||||||
|
},
|
||||||
|
DecryptMessageController: {
|
||||||
|
unapprovedDecryptMsgCount: true,
|
||||||
|
},
|
||||||
|
DesktopController: {
|
||||||
|
desktopEnabled: true,
|
||||||
|
},
|
||||||
|
EncryptionPublicKeyController: {
|
||||||
|
unapprovedEncryptionPublicKeyMsgCount: true,
|
||||||
|
},
|
||||||
|
KeyringController: {
|
||||||
|
isUnlocked: true,
|
||||||
|
},
|
||||||
|
MetaMetricsController: {
|
||||||
|
metaMetricsId: true,
|
||||||
|
participateInMetaMetrics: true,
|
||||||
|
},
|
||||||
|
NetworkController: {
|
||||||
networkId: true,
|
networkId: true,
|
||||||
networkStatus: true,
|
networkStatus: true,
|
||||||
nextNonce: true,
|
|
||||||
participateInMetaMetrics: true,
|
|
||||||
preferences: true,
|
|
||||||
providerConfig: {
|
providerConfig: {
|
||||||
nickname: true,
|
nickname: true,
|
||||||
ticker: true,
|
ticker: true,
|
||||||
type: true,
|
type: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
OnboardingController: {
|
||||||
|
completedOnboarding: true,
|
||||||
|
firstTimeFlowType: true,
|
||||||
seedPhraseBackedUp: true,
|
seedPhraseBackedUp: true,
|
||||||
unapprovedDecryptMsgCount: true,
|
},
|
||||||
unapprovedEncryptionPublicKeyMsgCount: true,
|
PreferencesController: {
|
||||||
unapprovedMsgCount: true,
|
currentLocale: true,
|
||||||
unapprovedPersonalMsgCount: true,
|
featureFlags: true,
|
||||||
unapprovedTypedMessagesCount: true,
|
forgottenPassword: true,
|
||||||
|
ipfsGateway: true,
|
||||||
|
preferences: true,
|
||||||
useBlockie: true,
|
useBlockie: true,
|
||||||
useNonceField: true,
|
useNonceField: true,
|
||||||
usePhishDetect: true,
|
usePhishDetect: true,
|
||||||
|
},
|
||||||
|
SignatureController: {
|
||||||
|
unapprovedMsgCount: true,
|
||||||
|
unapprovedPersonalMsgCount: true,
|
||||||
|
unapprovedTypedMessagesCount: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedBackgroundStateMask = Object.values(
|
||||||
|
SENTRY_BACKGROUND_STATE,
|
||||||
|
).reduce((partialBackgroundState, controllerState) => {
|
||||||
|
return {
|
||||||
|
...partialBackgroundState,
|
||||||
|
...controllerState,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// This describes the subset of Redux state attached to errors sent to Sentry
|
||||||
|
// These properties have some potential to be useful for debugging, and they do
|
||||||
|
// not contain any identifiable information.
|
||||||
|
export const SENTRY_UI_STATE = {
|
||||||
|
gas: true,
|
||||||
|
history: true,
|
||||||
|
metamask: {
|
||||||
|
...flattenedBackgroundStateMask,
|
||||||
|
// This property comes from the background but isn't in controller state
|
||||||
|
isInitialized: true,
|
||||||
|
// These properties are in the `metamask` slice but not in the background state
|
||||||
|
customNonceValue: true,
|
||||||
|
isAccountMenuOpen: true,
|
||||||
|
nextNonce: true,
|
||||||
welcomeScreenSeen: true,
|
welcomeScreenSeen: true,
|
||||||
},
|
},
|
||||||
unconnectedAccount: true,
|
unconnectedAccount: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether MetaMetrics is enabled, given the application state.
|
||||||
|
*
|
||||||
|
* @param {{ state: unknown} | { persistedState: unknown }} appState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getMetaMetricsEnabledFromAppState(appState) {
|
||||||
|
// during initialization after loading persisted state
|
||||||
|
if (appState.persistedState) {
|
||||||
|
return getMetaMetricsEnabledFromPersistedState(appState.persistedState);
|
||||||
|
// After initialization
|
||||||
|
} else if (appState.state) {
|
||||||
|
// UI
|
||||||
|
if (appState.state.metamask) {
|
||||||
|
return Boolean(appState.state.metamask.participateInMetaMetrics);
|
||||||
|
}
|
||||||
|
// background
|
||||||
|
return Boolean(
|
||||||
|
appState.state.MetaMetricsController?.participateInMetaMetrics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// during initialization, before first persisted state is read
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether MetaMetrics is enabled, given the persisted state.
|
||||||
|
*
|
||||||
|
* @param {unknown} persistedState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getMetaMetricsEnabledFromPersistedState(persistedState) {
|
||||||
|
return Boolean(
|
||||||
|
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether onboarding has completed, given the application state.
|
||||||
|
*
|
||||||
|
* @param {Record<string, unknown>} appState - Application state
|
||||||
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
|
* is enabled, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function getOnboardingCompleteFromAppState(appState) {
|
||||||
|
// during initialization after loading persisted state
|
||||||
|
if (appState.persistedState) {
|
||||||
|
return Boolean(
|
||||||
|
appState.persistedState.data?.OnboardingController?.completedOnboarding,
|
||||||
|
);
|
||||||
|
// After initialization
|
||||||
|
} else if (appState.state) {
|
||||||
|
// UI
|
||||||
|
if (appState.state.metamask) {
|
||||||
|
return Boolean(appState.state.metamask.completedOnboarding);
|
||||||
|
}
|
||||||
|
// background
|
||||||
|
return Boolean(appState.state.OnboardingController?.completedOnboarding);
|
||||||
|
}
|
||||||
|
// during initialization, before first persisted state is read
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export default function setupSentry({ release, getState }) {
|
export default function setupSentry({ release, getState }) {
|
||||||
if (!release) {
|
if (!release) {
|
||||||
throw new Error('Missing release');
|
throw new Error('Missing release');
|
||||||
@ -112,22 +226,21 @@ export default function setupSentry({ release, getState }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that returns whether MetaMetrics is enabled. This should also
|
* Returns whether MetaMetrics is enabled. If the application hasn't yet
|
||||||
* return `false` if state has not yet been initialzed.
|
* been initialized, the persisted state will be used (if any).
|
||||||
*
|
*
|
||||||
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
* @returns `true` if MetaMetrics is enabled, `false` otherwise.
|
||||||
* is enabled, `false` otherwise.
|
|
||||||
*/
|
*/
|
||||||
async function getMetaMetricsEnabled() {
|
async function getMetaMetricsEnabled() {
|
||||||
const appState = getState();
|
const appState = getState();
|
||||||
if (Object.keys(appState) > 0) {
|
if (appState.state || appState.persistedState) {
|
||||||
return Boolean(appState?.store?.metamask?.participateInMetaMetrics);
|
return getMetaMetricsEnabledFromAppState(appState);
|
||||||
}
|
}
|
||||||
|
// If we reach here, it means the error was thrown before initialization
|
||||||
|
// completed, and before we loaded the persisted state for the first time.
|
||||||
try {
|
try {
|
||||||
const persistedState = await globalThis.stateHooks.getPersistedState();
|
const persistedState = await globalThis.stateHooks.getPersistedState();
|
||||||
return Boolean(
|
return getMetaMetricsEnabledFromPersistedState(persistedState);
|
||||||
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return false;
|
return false;
|
||||||
@ -269,17 +382,15 @@ function hideUrlIfNotInternal(url) {
|
|||||||
*/
|
*/
|
||||||
export function beforeBreadcrumb(getState) {
|
export function beforeBreadcrumb(getState) {
|
||||||
return (breadcrumb) => {
|
return (breadcrumb) => {
|
||||||
if (getState) {
|
if (!getState) {
|
||||||
const appState = getState();
|
return null;
|
||||||
if (
|
}
|
||||||
Object.values(appState).length &&
|
const appState = getState();
|
||||||
(!appState?.store?.metamask?.participateInMetaMetrics ||
|
if (
|
||||||
!appState?.store?.metamask?.completedOnboarding ||
|
!getMetaMetricsEnabledFromAppState(appState) ||
|
||||||
breadcrumb?.category === 'ui.input')
|
!getOnboardingCompleteFromAppState(appState) ||
|
||||||
) {
|
breadcrumb?.category === 'ui.input'
|
||||||
return null;
|
) {
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
|
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
|
||||||
import {
|
import {
|
||||||
ENVIRONMENT_TYPE_POPUP,
|
|
||||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
|
||||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
|
||||||
ENVIRONMENT_TYPE_BACKGROUND,
|
ENVIRONMENT_TYPE_BACKGROUND,
|
||||||
PLATFORM_FIREFOX,
|
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||||
PLATFORM_OPERA,
|
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||||
|
ENVIRONMENT_TYPE_POPUP,
|
||||||
PLATFORM_CHROME,
|
PLATFORM_CHROME,
|
||||||
PLATFORM_EDGE,
|
PLATFORM_EDGE,
|
||||||
|
PLATFORM_FIREFOX,
|
||||||
|
PLATFORM_OPERA,
|
||||||
} from '../../../shared/constants/app';
|
} from '../../../shared/constants/app';
|
||||||
import {
|
import {
|
||||||
|
TransactionEnvelopeType,
|
||||||
TransactionStatus,
|
TransactionStatus,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
TransactionEnvelopeType,
|
|
||||||
} from '../../../shared/constants/transaction';
|
} from '../../../shared/constants/transaction';
|
||||||
|
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||||
import {
|
import {
|
||||||
|
addUrlProtocolPrefix,
|
||||||
deferredPromise,
|
deferredPromise,
|
||||||
|
formatTxMetaForRpcResult,
|
||||||
getEnvironmentType,
|
getEnvironmentType,
|
||||||
getPlatform,
|
getPlatform,
|
||||||
formatTxMetaForRpcResult,
|
getValidUrl,
|
||||||
|
isWebUrl,
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
describe('app utils', () => {
|
describe('app utils', () => {
|
||||||
@ -73,6 +76,39 @@ describe('app utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('URL utils', () => {
|
||||||
|
it('should test addUrlProtocolPrefix', () => {
|
||||||
|
expect(addUrlProtocolPrefix('http://example.com')).toStrictEqual(
|
||||||
|
'http://example.com',
|
||||||
|
);
|
||||||
|
expect(addUrlProtocolPrefix('https://example.com')).toStrictEqual(
|
||||||
|
'https://example.com',
|
||||||
|
);
|
||||||
|
expect(addUrlProtocolPrefix('example.com')).toStrictEqual(
|
||||||
|
'https://example.com',
|
||||||
|
);
|
||||||
|
expect(addUrlProtocolPrefix('exa mple.com')).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should test isWebUrl', () => {
|
||||||
|
expect(isWebUrl('http://example.com')).toStrictEqual(true);
|
||||||
|
expect(isWebUrl('https://example.com')).toStrictEqual(true);
|
||||||
|
expect(isWebUrl('https://exa mple.com')).toStrictEqual(false);
|
||||||
|
expect(isWebUrl('')).toStrictEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should test getValidUrl', () => {
|
||||||
|
expect(getValidUrl('http://example.com').toString()).toStrictEqual(
|
||||||
|
'http://example.com/',
|
||||||
|
);
|
||||||
|
expect(getValidUrl('https://example.com').toString()).toStrictEqual(
|
||||||
|
'https://example.com/',
|
||||||
|
);
|
||||||
|
expect(getValidUrl('https://exa%20mple.com')).toStrictEqual(null);
|
||||||
|
expect(getValidUrl('')).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('isPrefixedFormattedHexString', () => {
|
describe('isPrefixedFormattedHexString', () => {
|
||||||
it('should return true for valid hex strings', () => {
|
it('should return true for valid hex strings', () => {
|
||||||
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);
|
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
|
import urlLib from 'url';
|
||||||
|
import { AccessList } from '@ethereumjs/tx';
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import { memoize } from 'lodash';
|
import { memoize } from 'lodash';
|
||||||
import { AccessList } from '@ethereumjs/tx';
|
|
||||||
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ENVIRONMENT_TYPE_POPUP,
|
|
||||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
|
||||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
|
||||||
ENVIRONMENT_TYPE_BACKGROUND,
|
ENVIRONMENT_TYPE_BACKGROUND,
|
||||||
PLATFORM_FIREFOX,
|
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||||
PLATFORM_OPERA,
|
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||||
|
ENVIRONMENT_TYPE_POPUP,
|
||||||
|
PLATFORM_BRAVE,
|
||||||
PLATFORM_CHROME,
|
PLATFORM_CHROME,
|
||||||
PLATFORM_EDGE,
|
PLATFORM_EDGE,
|
||||||
PLATFORM_BRAVE,
|
PLATFORM_FIREFOX,
|
||||||
|
PLATFORM_OPERA,
|
||||||
} from '../../../shared/constants/app';
|
} from '../../../shared/constants/app';
|
||||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
|
||||||
import {
|
import {
|
||||||
TransactionEnvelopeType,
|
TransactionEnvelopeType,
|
||||||
TransactionMeta,
|
TransactionMeta,
|
||||||
} from '../../../shared/constants/transaction';
|
} from '../../../shared/constants/transaction';
|
||||||
|
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see {@link getEnvironmentType}
|
* @see {@link getEnvironmentType}
|
||||||
@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getPlatform,
|
|
||||||
getEnvironmentType,
|
|
||||||
hexToBn,
|
|
||||||
BnMultiplyByFraction,
|
BnMultiplyByFraction,
|
||||||
addHexPrefix,
|
addHexPrefix,
|
||||||
getChainType,
|
|
||||||
checkAlarmExists,
|
checkAlarmExists,
|
||||||
|
getChainType,
|
||||||
|
getEnvironmentType,
|
||||||
|
getPlatform,
|
||||||
|
hexToBn,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Taken from https://stackoverflow.com/a/1349426/3696652
|
// Taken from https://stackoverflow.com/a/1349426/3696652
|
||||||
@ -235,10 +235,43 @@ export function previousValueComparator<A>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addUrlProtocolPrefix(urlString: string) {
|
export function addUrlProtocolPrefix(urlString: string) {
|
||||||
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) {
|
let trimmed = urlString.trim();
|
||||||
return `https://${urlString}`;
|
|
||||||
|
if (trimmed.length && !urlLib.parse(trimmed).protocol) {
|
||||||
|
trimmed = `https://${trimmed}`;
|
||||||
}
|
}
|
||||||
return urlString;
|
|
||||||
|
if (getValidUrl(trimmed) !== null) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValidUrl(urlString: string): URL | null {
|
||||||
|
try {
|
||||||
|
const url = new URL(urlString);
|
||||||
|
|
||||||
|
if (url.hostname.length === 0 || url.pathname.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.hostname !== decodeURIComponent(url.hostname)) {
|
||||||
|
return null; // will happen if there's a %, a space, or other invalid character in the hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebUrl(urlString: string): boolean {
|
||||||
|
const url = getValidUrl(urlString);
|
||||||
|
|
||||||
|
return (
|
||||||
|
url !== null && (url.protocol === 'https:' || url.protocol === 'http:')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattedTransactionMeta {
|
interface FormattedTransactionMeta {
|
||||||
|
@ -145,27 +145,21 @@ describe('MetaMaskController', function () {
|
|||||||
metamaskController.addNewAccount(1),
|
metamaskController.addNewAccount(1),
|
||||||
metamaskController.addNewAccount(1),
|
metamaskController.addNewAccount(1),
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(
|
assert.equal(addNewAccountResult1, addNewAccountResult2);
|
||||||
Object.keys(addNewAccountResult1.identities),
|
|
||||||
Object.keys(addNewAccountResult2.identities),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('two successive calls with same accountCount give same result', async function () {
|
it('two successive calls with same accountCount give same result', async function () {
|
||||||
await metamaskController.createNewVaultAndKeychain('test@123');
|
await metamaskController.createNewVaultAndKeychain('test@123');
|
||||||
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
||||||
const addNewAccountResult2 = await metamaskController.addNewAccount(1);
|
const addNewAccountResult2 = await metamaskController.addNewAccount(1);
|
||||||
assert.deepEqual(
|
assert.equal(addNewAccountResult1, addNewAccountResult2);
|
||||||
Object.keys(addNewAccountResult1.identities),
|
|
||||||
Object.keys(addNewAccountResult2.identities),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('two successive calls with different accountCount give different results', async function () {
|
it('two successive calls with different accountCount give different results', async function () {
|
||||||
await metamaskController.createNewVaultAndKeychain('test@123');
|
await metamaskController.createNewVaultAndKeychain('test@123');
|
||||||
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
||||||
const addNewAccountResult2 = await metamaskController.addNewAccount(2);
|
const addNewAccountResult2 = await metamaskController.addNewAccount(2);
|
||||||
assert.notDeepEqual(addNewAccountResult1, addNewAccountResult2);
|
assert.notEqual(addNewAccountResult1, addNewAccountResult2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -178,14 +172,14 @@ describe('MetaMaskController', function () {
|
|||||||
|
|
||||||
await metamaskController.createNewVaultAndKeychain('test@123');
|
await metamaskController.createNewVaultAndKeychain('test@123');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
metamaskController.importAccountWithStrategy('Private Key', [
|
metamaskController.importAccountWithStrategy('privateKey', [
|
||||||
importPrivkey,
|
importPrivkey,
|
||||||
]),
|
]),
|
||||||
Promise.resolve(1).then(() => {
|
Promise.resolve(1).then(() => {
|
||||||
keyringControllerState1 = JSON.stringify(
|
keyringControllerState1 = JSON.stringify(
|
||||||
metamaskController.keyringController.memStore.getState(),
|
metamaskController.keyringController.memStore.getState(),
|
||||||
);
|
);
|
||||||
metamaskController.importAccountWithStrategy('Private Key', [
|
metamaskController.importAccountWithStrategy('privateKey', [
|
||||||
importPrivkey,
|
importPrivkey,
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
@ -226,6 +220,14 @@ describe('MetaMaskController', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setLocked', function () {
|
||||||
|
it('should lock the wallet', async function () {
|
||||||
|
const { isUnlocked, keyrings } = await metamaskController.setLocked();
|
||||||
|
assert(!isUnlocked);
|
||||||
|
assert.deepEqual(keyrings, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#addToken', function () {
|
describe('#addToken', function () {
|
||||||
const address = '0x514910771af9ca656af840dff83e8264ecf986ca';
|
const address = '0x514910771af9ca656af840dff83e8264ecf986ca';
|
||||||
const symbol = 'LINK';
|
const symbol = 'LINK';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user