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
|
||||
- test-e2e-chrome-snaps:
|
||||
requires:
|
||||
- prep-build-test-flask
|
||||
- prep-build-test
|
||||
- test-e2e-firefox-snaps:
|
||||
requires:
|
||||
- prep-build-test
|
||||
- test-e2e-chrome-snaps-flask:
|
||||
requires:
|
||||
- prep-build-test-flask
|
||||
- test-e2e-firefox-snaps-flask:
|
||||
requires:
|
||||
- prep-build-test-flask
|
||||
- test-e2e-chrome-mv3:
|
||||
@ -215,6 +221,7 @@ workflows:
|
||||
- prep-build-flask
|
||||
- all-tests-pass:
|
||||
requires:
|
||||
- test-deps-depcheck
|
||||
- validate-lavamoat-allow-scripts
|
||||
- validate-lavamoat-policy-build
|
||||
- validate-lavamoat-policy-webapp
|
||||
@ -847,6 +854,80 @@ jobs:
|
||||
path: test/test-results/e2e.xml
|
||||
|
||||
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
|
||||
parallelism: 4
|
||||
steps:
|
||||
@ -883,7 +964,7 @@ jobs:
|
||||
- store_test_results:
|
||||
path: test/test-results/e2e.xml
|
||||
|
||||
test-e2e-chrome-snaps:
|
||||
test-e2e-chrome-snaps-flask:
|
||||
executor: node-browsers
|
||||
parallelism: 4
|
||||
steps:
|
||||
|
@ -239,6 +239,7 @@ module.exports = {
|
||||
'app/scripts/controllers/app-state.test.js',
|
||||
'app/scripts/controllers/mmi-controller.test.js',
|
||||
'app/scripts/controllers/permissions/**/*.test.js',
|
||||
'app/scripts/controllers/preferences.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/migrations/*.test.js',
|
||||
'app/scripts/platforms/*.test.js',
|
||||
@ -268,6 +269,7 @@ module.exports = {
|
||||
'app/scripts/controllers/app-state.test.js',
|
||||
'app/scripts/controllers/mmi-controller.test.js',
|
||||
'app/scripts/controllers/permissions/**/*.test.js',
|
||||
'app/scripts/controllers/preferences.test.js',
|
||||
'app/scripts/lib/**/*.test.js',
|
||||
'app/scripts/migrations/*.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:
|
||||
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
|
||||
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
|
||||
run: npm run add-release-label-to-pr-and-linked-issues
|
||||
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
|
||||
env:
|
||||
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/permissions/**/*.test.js',
|
||||
'./app/scripts/controllers/mmi-controller.test.js',
|
||||
'./app/scripts/controllers/preferences.test.js',
|
||||
'./app/scripts/constants/error-utils.test.js',
|
||||
'./development/fitness-functions/**/*.test.ts',
|
||||
'./test/e2e/helpers.test.js',
|
||||
|
@ -2,6 +2,7 @@ import { draftTransactionInitialState } from '../ui/ducks/send';
|
||||
import { KeyringType } from '../shared/constants/keyring';
|
||||
import { NetworkType } from '@metamask/controller-utils';
|
||||
import { NetworkStatus } from '@metamask/network-controller';
|
||||
import { CHAIN_IDS } from '../shared/constants/network';
|
||||
|
||||
const state = {
|
||||
invalidCustomNetwork: {
|
||||
@ -529,6 +530,12 @@ const state = {
|
||||
preferences: {
|
||||
useNativeCurrencyAsPrimaryCurrency: true,
|
||||
},
|
||||
incomingTransactionsPreferences: {
|
||||
[CHAIN_IDS.MAINNET]: true,
|
||||
[CHAIN_IDS.GOERLI]: false,
|
||||
[CHAIN_IDS.OPTIMISM_TESTNET]: false,
|
||||
[CHAIN_IDS.AVALANCHE_TESTNET]: true,
|
||||
},
|
||||
firstTimeFlowType: 'create',
|
||||
completedOnboarding: true,
|
||||
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]
|
||||
|
||||
## [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]
|
||||
### Changed
|
||||
- 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
|
||||
- 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.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
|
||||
|
@ -133,6 +133,8 @@ Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.
|
||||
--leave-running Leaves the browser running after a test fails, along with
|
||||
anything else that the test used (ganache, the test dapp,
|
||||
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:
|
||||
|
15
app/_locales/am/messages.json
generated
15
app/_locales/am/messages.json
generated
@ -169,9 +169,6 @@
|
||||
"copyAddress": {
|
||||
"message": "አድራሻን ወደ ቅንጥብ ሰሌዳ ቅዳ"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "የግል ቁልፍዎ ይህ ነው (ለመቅዳት ጠቅ ያድርጉ)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "ወደ ቅንጥብ ሰሌዳ ገልብጥ"
|
||||
},
|
||||
@ -232,9 +229,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "በ ENS የስም ምዝገባ ላይ የተፈጠረ ስህተት"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "የይለፍ ቃል ያስገቡ"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "ለመቀጠል የይለፍ ቃል ያስገቡ"
|
||||
},
|
||||
@ -247,9 +241,6 @@
|
||||
"expandView": {
|
||||
"message": "እይታን ዘርጋ"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "የግል ቁልፍን ላክ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "አልተሳካም"
|
||||
},
|
||||
@ -648,9 +639,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "በመላኪያ ማያ ላይ የ hex ውሂብ መስክን ለማሳየት ይህን ይምረጡ"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "የግል ቁልፎችን አሳይ"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "የፊርማ ጥያቄ"
|
||||
},
|
||||
@ -771,9 +759,6 @@
|
||||
"tryAgain": {
|
||||
"message": "እንደገና ሞክር"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "የ MetaMask የይለፍ ቃልዎን ይጻፉ"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "ያልተፈቀደ"
|
||||
},
|
||||
|
15
app/_locales/ar/messages.json
generated
15
app/_locales/ar/messages.json
generated
@ -179,9 +179,6 @@
|
||||
"copyAddress": {
|
||||
"message": "نسخ العنوان إلى الحافظة"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "هذا هو مفتاحك الخاص (انقر للنسخ)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "نسخ إلى الحافظة"
|
||||
},
|
||||
@ -245,9 +242,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "خطأ في تسجيل اسم ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "أدخل كلمة مرور"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "أدخل كلمة المرور للمتابعة"
|
||||
},
|
||||
@ -260,9 +254,6 @@
|
||||
"expandView": {
|
||||
"message": "توسيع العرض"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "تصدير المفتاح الخاص"
|
||||
},
|
||||
"failed": {
|
||||
"message": "فشل"
|
||||
},
|
||||
@ -660,9 +651,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "حدد هذا لإظهار حقل بيانات سداسي عشرية على شاشة الإرسال"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "عرض المفاتيح الخاصة"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "طلب التوقيع"
|
||||
},
|
||||
@ -783,9 +771,6 @@
|
||||
"tryAgain": {
|
||||
"message": "إعادة المحاولة"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "أدخل كلمة مرور MetaMask الخاصة بك"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "تم الرفض"
|
||||
},
|
||||
|
15
app/_locales/bg/messages.json
generated
15
app/_locales/bg/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Копирайте адреса в клипборда"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Това е Вашият личен ключ (кликнете, за да го копирате)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Копиране в буферната памет"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Грешка при регистрацията на име на ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Въведете парола"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Въведете парола, за да продължите"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Разгъване на изглед"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Експортиране на частен ключ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Неуспешно"
|
||||
},
|
||||
@ -659,9 +650,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Изберете това, за да се покаже полето с шестнадесетични данни на екрана за изпращане"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Показване на частни ключове"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Заявка за подпис"
|
||||
},
|
||||
@ -782,9 +770,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Нов опит"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Въведете паролата си за MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Неодобрено"
|
||||
},
|
||||
|
15
app/_locales/bn/messages.json
generated
15
app/_locales/bn/messages.json
generated
@ -172,9 +172,6 @@
|
||||
"copyAddress": {
|
||||
"message": "ক্লিপবোর্ডে ঠিকানা কপি করুন"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "এটি হল আপনার গোপন কী (কপি করতে ক্লিক করুন)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "ক্লিপবোর্ডে কপি করুন"
|
||||
},
|
||||
@ -238,9 +235,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "ENS নাম নিবন্ধীকরণে ত্রুটি হয়েছে"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "পাসওয়ার্ড লিখুন"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "অবিরত রাখতে পাসওয়ার্ড লিখুন"
|
||||
},
|
||||
@ -253,9 +247,6 @@
|
||||
"expandView": {
|
||||
"message": "ভিউ সম্প্রসারিত করুন"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "ব্যক্তিগত কী রপ্তানি করুন"
|
||||
},
|
||||
"failed": {
|
||||
"message": "ব্যর্থ হয়েছে"
|
||||
},
|
||||
@ -657,9 +648,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "পাঠাবার স্ক্রিনে হেক্স ডেটা ফিল্ডটি দেখাবার জন্য এটি নির্বাচন করুন"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "গোপনীয় কীগুলি দেখান"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "স্বাক্ষরের অনুরোধ"
|
||||
},
|
||||
@ -780,9 +768,6 @@
|
||||
"tryAgain": {
|
||||
"message": "আবার করুন"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "আপনার MetaMask পাসওয়ার্ড টাইপ করুন"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "অননুমোদিত"
|
||||
},
|
||||
|
15
app/_locales/ca/messages.json
generated
15
app/_locales/ca/messages.json
generated
@ -172,9 +172,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Copiar adreça al porta-retalls"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Aquesta és la teva clau privada (fes clic per a copiar)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Copia al porta-retalls"
|
||||
},
|
||||
@ -238,9 +235,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Error al registre de nom ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Introdueix contrasenya"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Introdueix la contrasenya per continuar"
|
||||
},
|
||||
@ -253,9 +247,6 @@
|
||||
"expandView": {
|
||||
"message": "Eixamplar Vista"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportar Clau Privada."
|
||||
},
|
||||
"failed": {
|
||||
"message": "Fallit"
|
||||
},
|
||||
@ -644,9 +635,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Selecciona això per a mostrar el camp de dades Hex a la pantalla d'enviament"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Mostrar Claus Privades"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Sol·licitud de Signatura"
|
||||
},
|
||||
@ -761,9 +749,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Torna-ho a provar"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Tecleja la teva contrasenya de MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Pendent d'aprovació"
|
||||
},
|
||||
|
15
app/_locales/cs/messages.json
generated
15
app/_locales/cs/messages.json
generated
@ -72,9 +72,6 @@
|
||||
"copiedExclamation": {
|
||||
"message": "Zkopírováno!"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopírovat do schránky"
|
||||
},
|
||||
@ -105,15 +102,9 @@
|
||||
"edit": {
|
||||
"message": "Upravit"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Zadejte heslo"
|
||||
},
|
||||
"etherscanView": {
|
||||
"message": "Prohlédněte si účet na Etherscan"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportovat privátní klíč"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Neúspěšné"
|
||||
},
|
||||
@ -304,9 +295,6 @@
|
||||
"settings": {
|
||||
"message": "Nastavení"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Zobrazit privátní klíče"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Požadavek podpisu"
|
||||
},
|
||||
@ -355,9 +343,6 @@
|
||||
"transactionError": {
|
||||
"message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu."
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Zadejte své heslo"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Neschváleno"
|
||||
},
|
||||
|
15
app/_locales/da/messages.json
generated
15
app/_locales/da/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopier adresse til udklipsholder"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Dette er din private nøgle (klik for at kopiere)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopiér til udklipsholderen"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Fejl i ENS-navneregistrering"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Indtast kodeord"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Indtast adgangskode for at fortsætte"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Udvis Visning"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksporter privat nøgle"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Mislykkedes"
|
||||
},
|
||||
@ -641,9 +632,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Vælg dette for at vise hex-datafeltet på send-skærmen"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Vis private nøgler"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Signaturforespørgsel"
|
||||
},
|
||||
@ -755,9 +743,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Prøv igen"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Skriv din MetaMask-adgangskode"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "Address"
|
||||
},
|
||||
"addressCopied": {
|
||||
"message": "Address copied!"
|
||||
},
|
||||
"advanced": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
@ -834,6 +837,9 @@
|
||||
"connectionRequest": {
|
||||
"message": "Connection request"
|
||||
},
|
||||
"connections": {
|
||||
"message": "Connections"
|
||||
},
|
||||
"contactUs": {
|
||||
"message": "Contact us"
|
||||
},
|
||||
@ -898,9 +904,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Copy address to clipboard"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "This is your private key (click to copy)"
|
||||
},
|
||||
"copyRawTransactionData": {
|
||||
"message": "Copy raw transaction data"
|
||||
},
|
||||
@ -1482,9 +1485,6 @@
|
||||
"enterOptionalPassword": {
|
||||
"message": "Enter optional password"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Enter password"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Enter password to continue"
|
||||
},
|
||||
@ -1561,11 +1561,8 @@
|
||||
"exploreMetaMaskSnaps": {
|
||||
"message": "Explore MetaMask Snaps"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Export private key"
|
||||
},
|
||||
"extendWalletWithSnaps": {
|
||||
"message": "Extend the wallet experience."
|
||||
"message": "Customize your wallet experience."
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "External extension"
|
||||
@ -2870,7 +2867,7 @@
|
||||
"message": "👓 We are making transactions easier to read."
|
||||
},
|
||||
"notificationsEmptyText": {
|
||||
"message": "Nothing to see here."
|
||||
"message": "This is where you can find notifications from your installed snaps."
|
||||
},
|
||||
"notificationsHeader": {
|
||||
"message": "Notifications"
|
||||
@ -3019,10 +3016,6 @@
|
||||
"onboardingPinExtensionTitle": {
|
||||
"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": {
|
||||
"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"
|
||||
@ -3872,9 +3865,6 @@
|
||||
"showPrivateKey": {
|
||||
"message": "Show private key"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Show Private Keys"
|
||||
},
|
||||
"showTestnetNetworks": {
|
||||
"message": "Show test networks"
|
||||
},
|
||||
@ -4377,6 +4367,9 @@
|
||||
"swap": {
|
||||
"message": "Swap"
|
||||
},
|
||||
"swapAdjustSlippage": {
|
||||
"message": "Adjust slippage"
|
||||
},
|
||||
"swapAggregator": {
|
||||
"message": "Aggregator"
|
||||
},
|
||||
@ -4435,9 +4428,6 @@
|
||||
"swapEditLimit": {
|
||||
"message": "Edit limit"
|
||||
},
|
||||
"swapEditTransactionSettings": {
|
||||
"message": "Edit transaction settings"
|
||||
},
|
||||
"swapEnableDescription": {
|
||||
"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."
|
||||
@ -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.",
|
||||
"description": "$1 is the selected network, e.g. Ethereum or BSC"
|
||||
},
|
||||
"swapHighSlippage": {
|
||||
"message": "High slippage"
|
||||
},
|
||||
"swapHighSlippageWarning": {
|
||||
"message": "Slippage amount is very high."
|
||||
},
|
||||
@ -4512,6 +4505,9 @@
|
||||
"swapLearnMore": {
|
||||
"message": "Learn more about Swaps"
|
||||
},
|
||||
"swapLowSlippage": {
|
||||
"message": "Low slippage"
|
||||
},
|
||||
"swapLowSlippageError": {
|
||||
"message": "Transaction may fail, max slippage too low."
|
||||
},
|
||||
@ -4622,6 +4618,20 @@
|
||||
"swapShowLatestQuotes": {
|
||||
"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": {
|
||||
"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."
|
||||
},
|
||||
"swapSlippageOverLimitTitle": {
|
||||
"message": "Reduce slippage to continue"
|
||||
"message": "Very high slippage"
|
||||
},
|
||||
"swapSlippagePercent": {
|
||||
"message": "$1%",
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "There are fewer zero-slippage quote providers which will result in a less competitive quote."
|
||||
},
|
||||
@ -5134,9 +5132,6 @@
|
||||
"txInsightsNotSupported": {
|
||||
"message": "Transaction insights not supported for this contract at this time."
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Type your MetaMask password"
|
||||
},
|
||||
"typeYourSRP": {
|
||||
"message": "Type your Secret Recovery Phrase"
|
||||
},
|
||||
@ -5211,7 +5206,7 @@
|
||||
"message": "Decode smart contracts"
|
||||
},
|
||||
"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": {
|
||||
"message": "Batch account balance requests"
|
||||
@ -5324,6 +5319,9 @@
|
||||
"visitWebSite": {
|
||||
"message": "Visit our website"
|
||||
},
|
||||
"wallet": {
|
||||
"message": "Wallet"
|
||||
},
|
||||
"walletConnectionGuide": {
|
||||
"message": "our hardware wallet connection guide"
|
||||
},
|
||||
@ -5351,8 +5349,7 @@
|
||||
"message": "Want to add this network?"
|
||||
},
|
||||
"wantsToAddThisAsset": {
|
||||
"message": "$1 wants to add this asset to your wallet",
|
||||
"description": "$1 is the name of the website that wants to add an asset to your wallet"
|
||||
"message": "This allows the following asset to be added to your wallet."
|
||||
},
|
||||
"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": {
|
||||
"message": "Copiar dirección al Portapapeles"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Esta es su clave privada (haga clic para copiarla)"
|
||||
},
|
||||
"copyRawTransactionData": {
|
||||
"message": "Copiar los datos de las transacciones en bruto"
|
||||
},
|
||||
@ -767,9 +764,6 @@
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "Escribir límite máximo de gastos"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Escribir contraseña"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Escribir contraseña para continuar"
|
||||
},
|
||||
@ -826,9 +820,6 @@
|
||||
"experimental": {
|
||||
"message": "Experimental"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportar clave privada"
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "Extensión externa"
|
||||
},
|
||||
@ -1623,14 +1614,6 @@
|
||||
"onboardingPinExtensionTitle": {
|
||||
"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": {
|
||||
"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": {
|
||||
"message": "Mostrar permisos"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Mostrar claves privadas"
|
||||
},
|
||||
"showTestnetNetworks": {
|
||||
"message": "Mostrar redes de prueba"
|
||||
},
|
||||
@ -2637,9 +2617,6 @@
|
||||
"txInsightsNotSupported": {
|
||||
"message": "En este momento no se admiten informaciones sobre las transacciones para este contrato."
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Escriba su contraseña de MetaMask"
|
||||
},
|
||||
"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."
|
||||
|
15
app/_locales/et/messages.json
generated
15
app/_locales/et/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopeeri aadress lõikelauale"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "See on teie privaatne võti (klõpsake kopeerimiseks)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopeeri lõikelauale"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Tõrge ENS-i nime registreerimisel"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Sisestage parool"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Jätkamiseks sisestage parool"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Laienda vaadet"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Ekspordi privaatvõti"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Nurjus"
|
||||
},
|
||||
@ -653,9 +644,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Valige see, et kuvada saatmisekraanil hex-andmete väli"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Kuva privaatvõtmed"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Allkirja taotlus"
|
||||
},
|
||||
@ -776,9 +764,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Proovi uuesti"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Sisestage oma MetaMaski parool"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Kinnitamata"
|
||||
},
|
||||
|
15
app/_locales/fa/messages.json
generated
15
app/_locales/fa/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "کاپی آدرس به کلیپ بورد"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "این کلید خصوصی شما است (برای کاپی نمودن کلیک کنید)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "کپی در بریدهدان"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "خطا در ثبت نام ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "رمز عبور را وارد کنید"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "برای ادامه رمز عبور را وارد کنید"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "توسعه ساحه دید"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "صدور کلید شخصی"
|
||||
},
|
||||
"failed": {
|
||||
"message": "ناموفق شد"
|
||||
},
|
||||
@ -663,9 +654,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "برای نمایش بخش اطلاعات hex در صفحه ارسال این را انتخاب نمایید"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "نمایش کلید های شخصی"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "درخواست امضاء"
|
||||
},
|
||||
@ -786,9 +774,6 @@
|
||||
"tryAgain": {
|
||||
"message": "امتحان مجدد"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "رمز عبور MetaMask تان را تایپ نمایید"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "تصدیق ناشده"
|
||||
},
|
||||
|
15
app/_locales/fi/messages.json
generated
15
app/_locales/fi/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopioi osoite leikepöydälle"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Tämä on yksityinen avaimesi (kopioi napsauttamalla)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopioi leikepöydälle"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Virhe ENS-nimen rekisteröinnissä"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Kirjoita salasana"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Syötä salasana voidaksesi jatkaa"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Laajenna näkymää"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Vie yksityinen avain"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Epäonnistui"
|
||||
},
|
||||
@ -660,9 +651,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Näytä hex-tietokenttä lähetysnäytössä valitsemalla tämän"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Näytä yksityiset avaimet"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Allekirjoitus pyydetään"
|
||||
},
|
||||
@ -783,9 +771,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Yritä uudelleen"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Kirjoita MetaMask-salasanasi"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Ei hyväksytty"
|
||||
},
|
||||
|
15
app/_locales/fil/messages.json
generated
15
app/_locales/fil/messages.json
generated
@ -154,9 +154,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopyahin ang address sa clipboard"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Ito ang iyong pribadong private key (i-click para kopyahin)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopyahin sa clipboard"
|
||||
},
|
||||
@ -217,9 +214,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "May error sa pagrerehistro ng ENS name"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Ilagay ang password"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Ilagay ang password para magpatuloy"
|
||||
},
|
||||
@ -229,9 +223,6 @@
|
||||
"expandView": {
|
||||
"message": "I-expand ang View"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "I-export ang Private Key"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Nabigo"
|
||||
},
|
||||
@ -587,9 +578,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Piliin ito para ipakita ang hex data field sa screen ng pagpapadala"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Ipakita ang mga Private Key"
|
||||
},
|
||||
"sign": {
|
||||
"message": "I-sign"
|
||||
},
|
||||
@ -698,9 +686,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Subukang muli"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "I-type ang iyong password sa MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "העתק כתובת ללוח"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "זה המפתח הפרטי שלך (נא להקיש כדי להעתיק)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "העתק ללוח"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "שגיאה ברישום שם ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "יש להזין ססמה"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "יש להזין ססמה כדי להמשיך"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "הרחב תצוגה"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "יצא/י מפתח פרטי"
|
||||
},
|
||||
"failed": {
|
||||
"message": "נכשל"
|
||||
},
|
||||
@ -660,9 +651,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "בחר/י בזה כדי להציג את שדה הנתונים ההקסדצימאלים על מסך השליחה"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "הצג מפתחות פרטיים"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "בקשת חתימה"
|
||||
},
|
||||
@ -783,9 +771,6 @@
|
||||
"tryAgain": {
|
||||
"message": "ניסיון חוזר"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "נא להקליד את סיסמת MetaMask שלך"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "कॉपी कर दिया गया!"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "यह आपकी निजी कुंजी है (कॉपी करने के लिए क्लिक करें)।"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "क्लिपबोर्ड पर कॉपी करें"
|
||||
},
|
||||
@ -87,15 +84,9 @@
|
||||
"edit": {
|
||||
"message": "संपादित करें"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "पासवर्ड दर्ज करें"
|
||||
},
|
||||
"etherscanView": {
|
||||
"message": "ईथरस्कैन पर खाता देखें"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "निजी कुंजी निर्यात करें"
|
||||
},
|
||||
"failed": {
|
||||
"message": "विफल"
|
||||
},
|
||||
@ -281,9 +272,6 @@
|
||||
"settings": {
|
||||
"message": "सेटिंग्स"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "निजी कुंजी दिखाएँ"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "हस्ताक्षर अनुरोध"
|
||||
},
|
||||
@ -317,9 +305,6 @@
|
||||
"total": {
|
||||
"message": "कुल"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "अपना पासवर्ड टाइप करें"
|
||||
},
|
||||
"unknown": {
|
||||
"message": "अज्ञात नेटवर्क"
|
||||
},
|
||||
|
15
app/_locales/hr/messages.json
generated
15
app/_locales/hr/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopiraj adresu u međuspremnik"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Ovo je vaš privatni ključ (kliknite za kopiranje)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopiraj u međuspremnik"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Greška u registraciji naziva ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Upiši lozinku"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Upišite lozinku za nastavak"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Proširi prikaz"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Izvezi privatni ključ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Neuspješno"
|
||||
},
|
||||
@ -656,9 +647,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Odaberite ovu stavku za prikaz polja namijenjenog za podatke hex na zaslonu za slanje"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Prikaži privatne ključe"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Zahtjev za potpisom"
|
||||
},
|
||||
@ -776,9 +764,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Pokušaj ponovo"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Upišite svoju lozinku MetaMask."
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Neodobreno"
|
||||
},
|
||||
|
15
app/_locales/ht/messages.json
generated
15
app/_locales/ht/messages.json
generated
@ -108,9 +108,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopi adrès clipboard"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Sa a se kle prive ou (klike pou ou kopye)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopi clipboard"
|
||||
},
|
||||
@ -147,9 +144,6 @@
|
||||
"edit": {
|
||||
"message": "Korije"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Mete modpas"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Mete modpas pou kontinye"
|
||||
},
|
||||
@ -159,9 +153,6 @@
|
||||
"expandView": {
|
||||
"message": "Elaji Wè"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Voye Kòd Prive"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Tonbe"
|
||||
},
|
||||
@ -482,9 +473,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Pran sa pouw ka montre chan entèfas hex data a"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Montre Kle Prive"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Demann Siyati"
|
||||
},
|
||||
@ -554,9 +542,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Eseye anko"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Tape modpas ou"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Pa apwouve"
|
||||
},
|
||||
|
15
app/_locales/hu/messages.json
generated
15
app/_locales/hu/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"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": {
|
||||
"message": "Másolás a vágólapra"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Hiba történt az ENS név regisztrációjakor"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Adja meg a jelszót"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "A folytatáshoz adja meg a jelszót"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Nézet nagyítása"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Privát kulcs exportálása"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Sikertelen"
|
||||
},
|
||||
@ -656,9 +647,6 @@
|
||||
"showHexDataDescription": {
|
||||
"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": {
|
||||
"message": "Aláírás kérése"
|
||||
},
|
||||
@ -776,9 +764,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Újra"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Írd be MetaMask jelszavadat"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "Copia l'indirizzo"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Questa è la tua chiave privata (clicca per copiare)"
|
||||
},
|
||||
"copyRawTransactionData": {
|
||||
"message": "Copia i dati grezzi della transazione"
|
||||
},
|
||||
@ -840,9 +837,6 @@
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "Inserisici Limite Spesa"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Inserisci password"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Inserisci la tua password per continuare"
|
||||
},
|
||||
@ -875,9 +869,6 @@
|
||||
"expandView": {
|
||||
"message": "Espandi Vista"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Esporta Chiave Privata"
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "Estensione Esterna"
|
||||
},
|
||||
@ -1439,9 +1430,6 @@
|
||||
"showPermissions": {
|
||||
"message": "Mostra permessi"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Mostra Chiave Privata"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Firma Richiesta"
|
||||
},
|
||||
@ -1794,9 +1782,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Prova di nuovo"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Inserisci Password"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Non approvata"
|
||||
},
|
||||
|
1156
app/_locales/ja/messages.json
generated
1156
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": {
|
||||
"message": "ವಿಳಾಸವನ್ನು ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "ಇದು ನಿಮ್ಮ ಖಾಸಗಿ ಕೀ ಆಗಿದೆ (ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಿ"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "ENS ಹೆಸರಿನ ನೋಂದಣಿಯಲ್ಲಿ ದೋಷ"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ನಮೂದಿಸಿ"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "ಮುಂದುವರೆಯಲು ಪಾಸ್ವರ್ಡ್ ನಮೂದಿಸಿ"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "ವಿಸ್ತರಿಸಿದ ವೀಕ್ಷಣೆ"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "ಖಾಸಗಿ ಕೀಲಿಯನ್ನು ರಫ್ತು ಮಾಡಿ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "ವಿಫಲವಾಗಿದೆ"
|
||||
},
|
||||
@ -663,9 +654,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "ಕಳುಹಿಸುವ ಪರದೆಯಲ್ಲಿ ಹೆಕ್ಸ್ ಡೇಟಾ ಕ್ಷೇತ್ರವನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "ಖಾಸಗಿ ಕೀಗಳನ್ನು ತೋರಿಸಿ"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "ಸಹಿಯ ವಿನಂತಿ"
|
||||
},
|
||||
@ -786,9 +774,6 @@
|
||||
"tryAgain": {
|
||||
"message": "ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "ನಿಮ್ಮ MetaMask ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಟೈಪ್ ಮಾಡಿ"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "ಅನುಮೋದಿಸದಿರುವುದು"
|
||||
},
|
||||
|
1198
app/_locales/ko/messages.json
generated
1198
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": {
|
||||
"message": "Kopijuoti adresą į iškarpinę"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Tai yra jūsų asmeninis raktas (spustelėkite, kad nukopijuotumėte)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopijuoti į iškarpinę"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "ENS pavadinimo registracijos klaida"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Įveskite slaptažodį"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Norėdami tęsti, įveskite slaptažodį"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Išskleisti rodinį"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksportuoti asmeninį raktą"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Nepavyko"
|
||||
},
|
||||
@ -663,9 +654,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Pasirinkite tai, kad siuntimo ekrane būtų rodomas šešioliktainių duomenų laukas"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Rodyti asmeninius raktus"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Parašo užklausa"
|
||||
},
|
||||
@ -786,9 +774,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Bandyti dar kartą"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Įveskite savo „MetaMask“ slaptažodį"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Nepatvirtinta"
|
||||
},
|
||||
|
15
app/_locales/lv/messages.json
generated
15
app/_locales/lv/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Iekopēt adresi starpliktuvē"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Šī ir jūsu privātā atslēga (noklikšķiniet, lai nokopētu)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopēt starpliktuvē"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Kļūda ENS vārda reģistrācijā"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Ievadiet paroli"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Ievadiet paroli, lai turpinātu"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Izvērst skatījumu"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksportēt privāto atslēgu"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Neizdevās"
|
||||
},
|
||||
@ -659,9 +650,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Atlasiet šo, lai atvērtu hex datus sūtīšanas ekrānā"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Rādīt privātās atslēgas"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Paraksta pieprasījums"
|
||||
},
|
||||
@ -782,9 +770,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Mēģināt vēlreiz"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Ievadiet savu MetaMask paroli"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Nav apstiprināts"
|
||||
},
|
||||
|
15
app/_locales/ms/messages.json
generated
15
app/_locales/ms/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Salin alamat kepada papan klip"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Ini kunci persendirian anda (klik untuk menyalin)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Salin ke papan keratan"
|
||||
},
|
||||
@ -238,9 +235,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Ralat dalam pendaftaran nama ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Masukkan kata laluan"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Masukkan kata laluan untuk teruskan"
|
||||
},
|
||||
@ -253,9 +247,6 @@
|
||||
"expandView": {
|
||||
"message": "Kembangkan Paparan"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksport Kekunci Persendirian"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Gagal"
|
||||
},
|
||||
@ -643,9 +634,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Pilih ini untuk menunjukkan medan data hex pada skrin hantar"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Tunjukkan Kunci Persendirian"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Permintaan Tandatangan"
|
||||
},
|
||||
@ -763,9 +751,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Cuba lagi"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Taip kata laluan MetaMask anda"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Belum Diluluskan"
|
||||
},
|
||||
|
15
app/_locales/nl/messages.json
generated
15
app/_locales/nl/messages.json
generated
@ -60,9 +60,6 @@
|
||||
"copiedExclamation": {
|
||||
"message": "Gekopieerd!"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Dit is uw privésleutel (klik om te kopiëren)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopieer naar klembord"
|
||||
},
|
||||
@ -84,15 +81,9 @@
|
||||
"edit": {
|
||||
"message": "Bewerk"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Voer wachtwoord in"
|
||||
},
|
||||
"etherscanView": {
|
||||
"message": "Bekijk account op Etherscan"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exporteer privésleutel"
|
||||
},
|
||||
"failed": {
|
||||
"message": "mislukt"
|
||||
},
|
||||
@ -271,9 +262,6 @@
|
||||
"settings": {
|
||||
"message": "instellingen"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Privésleutels weergeven"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Ondertekeningsverzoek"
|
||||
},
|
||||
@ -307,9 +295,6 @@
|
||||
"total": {
|
||||
"message": "Totaal"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Typ uw wachtwoord"
|
||||
},
|
||||
"unknown": {
|
||||
"message": "Onbekend"
|
||||
},
|
||||
|
15
app/_locales/no/messages.json
generated
15
app/_locales/no/messages.json
generated
@ -172,9 +172,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopier adresse til utklippstavlen "
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Dette er din private nøkkel (klikk for å kopiere)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopiér til utklippstavlen"
|
||||
},
|
||||
@ -238,9 +235,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Feil i ENS-navneregistrering"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Skriv inn passord "
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Skriv inn passord for å fortsette"
|
||||
},
|
||||
@ -253,9 +247,6 @@
|
||||
"expandView": {
|
||||
"message": "Utvid visning"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksporter privat nøkkel"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Mislyktes"
|
||||
},
|
||||
@ -644,9 +635,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Velg dette for å vise hex-datafeltet på sendskjermen"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Vis private nøkler"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Signaturforespørsel "
|
||||
},
|
||||
@ -761,9 +749,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Prøv igjen"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Skriv inn MetaMask-passordet"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Ikke godkjent "
|
||||
},
|
||||
|
15
app/_locales/ph/messages.json
generated
15
app/_locales/ph/messages.json
generated
@ -338,9 +338,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopyahin ang address sa clipboard"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Ito ang iyong pribadong key (i-click para kopyahin)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopyahin sa clipboard"
|
||||
},
|
||||
@ -489,9 +486,6 @@
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "Ilagay ang Max na Limitasyon sa Paggastos"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Ilagay ang password"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Ilagay ang password para magpatuloy"
|
||||
},
|
||||
@ -542,9 +536,6 @@
|
||||
"expandView": {
|
||||
"message": "I-expand ang view"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "I-export ang Pribadong Key"
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "External Extension"
|
||||
},
|
||||
@ -1309,9 +1300,6 @@
|
||||
"showPermissions": {
|
||||
"message": "Ipakita ang mga pahintulot"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Ipakita ang Mga Private Key"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Request ng Signature"
|
||||
},
|
||||
@ -1764,9 +1752,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Subukan ulit"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Uri ng password ng iyong MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Hindi inaprubahan"
|
||||
},
|
||||
|
15
app/_locales/pl/messages.json
generated
15
app/_locales/pl/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Skopiuj adres do schowka"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "To jest Twój prywatny klucz (kliknij żeby skopiować)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Skopiuj do schowka"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Błąd w rejestracji nazwy ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Wpisz hasło"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Podaj hasło żeby kontynuować"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Rozwiń widok"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Eksportuj klucz prywatny"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Nie udało się"
|
||||
},
|
||||
@ -657,9 +648,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Pokaż prywatne klucze"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Prośba o podpis"
|
||||
},
|
||||
@ -774,9 +762,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Spróbuj ponownie"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Wpisz hasło"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "Copiar endereço para a área de transferência"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Essa é a sua chave privada (clique para copiar)"
|
||||
},
|
||||
"copyRawTransactionData": {
|
||||
"message": "Copiar dados brutos da transação"
|
||||
},
|
||||
@ -767,9 +764,6 @@
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "Digite um limite máximo de gastos"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Digite a senha"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Digite a senha para continuar"
|
||||
},
|
||||
@ -826,9 +820,6 @@
|
||||
"experimental": {
|
||||
"message": "Experimental"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportar chave privada"
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "Extensão externa"
|
||||
},
|
||||
@ -1623,10 +1614,6 @@
|
||||
"onboardingPinExtensionTitle": {
|
||||
"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": {
|
||||
"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"
|
||||
@ -2020,9 +2007,6 @@
|
||||
"showPermissions": {
|
||||
"message": "Mostrar permissões"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Mostrar chaves privadas"
|
||||
},
|
||||
"showTestnetNetworks": {
|
||||
"message": "Mostrar redes de teste"
|
||||
},
|
||||
@ -2637,9 +2621,6 @@
|
||||
"txInsightsNotSupported": {
|
||||
"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": {
|
||||
"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."
|
||||
|
15
app/_locales/ro/messages.json
generated
15
app/_locales/ro/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Copiere adresă în clipboard"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Aceasta este cheia dumneavoastră privată (clic pentru a copia)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Copiați în clipboard"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Eroare la înregistrarea numelui ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Introduceți parola"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Introduceți parola pentru a continua"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Extindeți vizualizarea"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportați cheia privată"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Eșuat"
|
||||
},
|
||||
@ -650,9 +641,6 @@
|
||||
"showHexDataDescription": {
|
||||
"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": {
|
||||
"message": "Solicitare de semnătură"
|
||||
},
|
||||
@ -767,9 +755,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Încearcă din nou"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Scrieți parola dvs. pentru MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "Kopírovať adresu do schránky"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopírovat do schránky"
|
||||
},
|
||||
@ -235,9 +232,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Chyba pri registrácii názvu ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Zadejte heslo"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Pokračujte zadaním hesla"
|
||||
},
|
||||
@ -250,9 +244,6 @@
|
||||
"expandView": {
|
||||
"message": "Rozbaliť zobrazenie"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportovat privátní klíč"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Neúspěšné"
|
||||
},
|
||||
@ -635,9 +626,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Vyberte toto, ak chcete, aby sa na obrazovke odosielania zobrazilo hex dátové pole"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Zobrazit privátní klíče"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Požadavek podpisu"
|
||||
},
|
||||
@ -752,9 +740,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Skúsiť znova"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Zadejte své heslo"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Neschváleno"
|
||||
},
|
||||
|
15
app/_locales/sl/messages.json
generated
15
app/_locales/sl/messages.json
generated
@ -175,9 +175,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopiraj naslov v odložišče"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "To je vaš zesebni ključ (kliknite za kopiranje)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopiraj v odložišče"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Napaka pri registraciji imena ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Vnesite geslo"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Za nadaljevanje vnesite geslo"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Razširi pogled"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Izvozi zasebni ključ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Ni uspelo"
|
||||
},
|
||||
@ -651,9 +642,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Izberite za prikaz hex podatkov na zaslonu za pošiljanje"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Pokaži zasebni ključ"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Zahteva za podpis"
|
||||
},
|
||||
@ -774,9 +762,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Poskusi znova"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Vnesite vaše MetaMask geslo"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Neodobreno"
|
||||
},
|
||||
|
15
app/_locales/sr/messages.json
generated
15
app/_locales/sr/messages.json
generated
@ -172,9 +172,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopirajte adresu u ostavu"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Ovo je vaš privatni ključ (kliknite kako biste ga kopirali)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Копирај у меморију"
|
||||
},
|
||||
@ -238,9 +235,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Greška u registraciji ENS imena."
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Unesite lozinku"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Unesite lozinku kako biste nastavili"
|
||||
},
|
||||
@ -253,9 +247,6 @@
|
||||
"expandView": {
|
||||
"message": "Proširite prikaz"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Izvezite privatni ključ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Neuspešno"
|
||||
},
|
||||
@ -654,9 +645,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Izaberite ovo da bi se pokazalo polje sa hex podacima na „Pošalji” ekranu "
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Prikažite privatne ključeve"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Zahtev za potpisom"
|
||||
},
|
||||
@ -774,9 +762,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Пробај поново"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Ukucajte svoju MetaMask šifru"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Neodobren"
|
||||
},
|
||||
|
15
app/_locales/sv/messages.json
generated
15
app/_locales/sv/messages.json
generated
@ -169,9 +169,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Kopiera adress till urklipp"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Det här är din privata nyckel (klicka för att kopiera)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Kopiera till Urklipp"
|
||||
},
|
||||
@ -235,9 +232,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Fel i ENS-namnregistrering"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Ange lösenord"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Ange lösenord för att fortsätta"
|
||||
},
|
||||
@ -250,9 +244,6 @@
|
||||
"expandView": {
|
||||
"message": "Expandera vy"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Exportera privat nyckel"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Misslyckades"
|
||||
},
|
||||
@ -647,9 +638,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Välj detta för att visa hex-datafältet på sändarskärmen"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Visa privata nycklar"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Signaturförfrågan"
|
||||
},
|
||||
@ -761,9 +749,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Försök igen"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Ange ditt MetaMask-lösenord"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Inte godkänd"
|
||||
},
|
||||
|
15
app/_locales/sw/messages.json
generated
15
app/_locales/sw/messages.json
generated
@ -169,9 +169,6 @@
|
||||
"copyAddress": {
|
||||
"message": "Nakili anwani kwenye ubao wa kunakilia"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Huu ni ufunguo wako wa kibinafsi (bofya ili unakili)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Nakili kwenye ubao wa kunakili"
|
||||
},
|
||||
@ -235,9 +232,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Hitilafu imetokea kwenye usajili wa jina la ENS"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Ingiza nenosiri"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Ingiza nenosiri ili uendelee"
|
||||
},
|
||||
@ -250,9 +244,6 @@
|
||||
"expandView": {
|
||||
"message": "Panua Mwonekano"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Panua Mwonekano"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Imeshindwa"
|
||||
},
|
||||
@ -641,9 +632,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Chagua hii ili uonyeshe sehemu ya data ya hex kwenye skrini ya tuma"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Onyesha Fungo Binafsi"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Ombi la Saini"
|
||||
},
|
||||
@ -764,9 +752,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Jaribu tena"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Andika nenosiri lako la MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "Haijaidhinishwa"
|
||||
},
|
||||
|
15
app/_locales/ta/messages.json
generated
15
app/_locales/ta/messages.json
generated
@ -87,9 +87,6 @@
|
||||
"copiedExclamation": {
|
||||
"message": "நகலெடுக்கப்பட்டன!"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "இது உங்கள் தனிப்பட்ட விசை (நகலெடுக்க கிளிக் செய்யவும்)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "கிளிப்போர்டுக்கு நகலெடு"
|
||||
},
|
||||
@ -126,15 +123,9 @@
|
||||
"edit": {
|
||||
"message": "திருத்து"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "கடவுச்சொல்லை உள்ளிடவும்"
|
||||
},
|
||||
"etherscanView": {
|
||||
"message": "Etherscan கணக்கைப் பார்க்கவும்"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "தனியார் விசை ஐ ஏற்றுமதி செய்க"
|
||||
},
|
||||
"failed": {
|
||||
"message": "தோல்வி"
|
||||
},
|
||||
@ -374,9 +365,6 @@
|
||||
"settings": {
|
||||
"message": "அமைப்புகள்"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "தனிப்பட்ட விசைகளைக் காண்பி"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "கையொப்பம் கோரிக்கை"
|
||||
},
|
||||
@ -425,9 +413,6 @@
|
||||
"tryAgain": {
|
||||
"message": "மீண்டும் முயல்க"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "உங்கள் கடவுச்சொல்லை தட்டச்சு செய்யவும்"
|
||||
},
|
||||
"unapproved": {
|
||||
"message": "அங்கீகரிக்கப்படாத"
|
||||
},
|
||||
|
15
app/_locales/th/messages.json
generated
15
app/_locales/th/messages.json
generated
@ -78,9 +78,6 @@
|
||||
"copiedExclamation": {
|
||||
"message": "คัดลอกแล้ว!"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "นี่คือคีย์ส่วนตัวของคุณ(กดเพื่อคัดลอก)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "คัดลอกไปคลิปบอร์ด"
|
||||
},
|
||||
@ -117,18 +114,12 @@
|
||||
"editContact": {
|
||||
"message": "แก้ไขผู้ติดต่อ"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "ใส่รหัสผ่าน"
|
||||
},
|
||||
"etherscanView": {
|
||||
"message": "ดูบัญชีบน Etherscan"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "ขยายมุมมอง"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "ส่งออกคีย์ส่วนตัว"
|
||||
},
|
||||
"failed": {
|
||||
"message": "ล้มเหลว"
|
||||
},
|
||||
@ -338,9 +329,6 @@
|
||||
"settings": {
|
||||
"message": "การตั้งค่า"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "แสดงคีย์ส่วนตัว"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "ขอลายเซ็น"
|
||||
},
|
||||
@ -392,9 +380,6 @@
|
||||
"transactionDropped": {
|
||||
"message": "ธุรกรรมถูกยกเลิกเมื่อ $2"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "พิมพ์รหัสผ่านของคุณ"
|
||||
},
|
||||
"unknown": {
|
||||
"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": {
|
||||
"message": "Копіювати адресу в буфер обміну"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Це ваш закритий ключ (натисніть, щоб скопіювати)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Копіювати в буфер"
|
||||
},
|
||||
@ -241,9 +238,6 @@
|
||||
"ensRegistrationError": {
|
||||
"message": "Помилка у реєстрації ENS ім'я"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Введіть пароль"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "Введіть пароль, щоб продовжити"
|
||||
},
|
||||
@ -256,9 +250,6 @@
|
||||
"expandView": {
|
||||
"message": "Розгорнути подання"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "Експортувати приватний ключ"
|
||||
},
|
||||
"failed": {
|
||||
"message": "Помилка"
|
||||
},
|
||||
@ -663,9 +654,6 @@
|
||||
"showHexDataDescription": {
|
||||
"message": "Оберіть це, щоб показати поле для шістнадцятирикових даних на екрані надсилання"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "Показати приватні ключі"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "Запит підпису"
|
||||
},
|
||||
@ -786,9 +774,6 @@
|
||||
"tryAgain": {
|
||||
"message": "Повторити"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "Введіть ваш пароль MetaMask"
|
||||
},
|
||||
"unapproved": {
|
||||
"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": {
|
||||
"message": "複製到剪貼簿"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "這是您的私鑰(點擊複製)"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "複製到剪貼簿"
|
||||
},
|
||||
@ -497,9 +494,6 @@
|
||||
"enterMaxSpendLimit": {
|
||||
"message": "輸入最大花費限制"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "請輸入密碼"
|
||||
},
|
||||
"enterPasswordContinue": {
|
||||
"message": "請輸入密碼"
|
||||
},
|
||||
@ -547,9 +541,6 @@
|
||||
"expandView": {
|
||||
"message": "展開畫面"
|
||||
},
|
||||
"exportPrivateKey": {
|
||||
"message": "匯出私鑰"
|
||||
},
|
||||
"externalExtension": {
|
||||
"message": "外部擴充功能"
|
||||
},
|
||||
@ -1231,9 +1222,6 @@
|
||||
"showPermissions": {
|
||||
"message": "顯示權限"
|
||||
},
|
||||
"showPrivateKeys": {
|
||||
"message": "顯示私鑰"
|
||||
},
|
||||
"sigRequest": {
|
||||
"message": "請求簽署"
|
||||
},
|
||||
@ -1440,9 +1428,6 @@
|
||||
"tryAgain": {
|
||||
"message": "再試一次"
|
||||
},
|
||||
"typePassword": {
|
||||
"message": "請輸入密碼"
|
||||
},
|
||||
"unapproved": {
|
||||
"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.
|
||||
*/
|
||||
|
||||
// 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.
|
||||
import './lib/setup-persisted-state-hook';
|
||||
import './lib/setup-initial-state-hooks';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import endOfStream from 'end-of-stream';
|
||||
@ -13,6 +15,7 @@ import debounce from 'debounce-stream';
|
||||
import log from 'loglevel';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { storeAsStream } from '@metamask/obs-store';
|
||||
import { isObject } from '@metamask/utils';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
import { ApprovalType } from '@metamask/controller-utils';
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
@ -41,7 +44,7 @@ import Migrator from './lib/migrator';
|
||||
import ExtensionPlatform from './platforms/extension';
|
||||
import LocalStore from './lib/local-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 NotificationManager, {
|
||||
@ -68,6 +71,12 @@ import DesktopManager from '@metamask/desktop/dist/desktop-manager';
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
/* 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 firstTimeState = { ...rawFirstTimeState };
|
||||
|
||||
@ -79,7 +88,7 @@ const metamaskInternalProcessHash = {
|
||||
|
||||
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 notificationManager = new NotificationManager();
|
||||
@ -90,10 +99,6 @@ let uiIsTriggering = false;
|
||||
const openMetamaskTabsIDs = {};
|
||||
const requestAccountTabIds = {};
|
||||
let controller;
|
||||
|
||||
// state persistence
|
||||
const inTest = process.env.IN_TEST;
|
||||
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
|
||||
let versionedData;
|
||||
|
||||
if (inTest || process.env.METAMASK_DEBUG) {
|
||||
@ -264,7 +269,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
|
||||
*/
|
||||
async function initialize() {
|
||||
try {
|
||||
const initState = await loadStateFromPersistence();
|
||||
const initData = await loadStateFromPersistence();
|
||||
const initState = initData.data;
|
||||
const initLangCode = await getFirstPreferredLangCode();
|
||||
|
||||
///: BEGIN:ONLY_INCLUDE_IN(desktop)
|
||||
@ -287,6 +293,7 @@ async function initialize() {
|
||||
initLangCode,
|
||||
{},
|
||||
isFirstMetaMaskControllerSetup,
|
||||
initData.meta,
|
||||
);
|
||||
if (!isManifestV3) {
|
||||
await loadPhishingWarningPage();
|
||||
@ -409,6 +416,19 @@ export async function loadStateFromPersistence() {
|
||||
versionedData = await migrator.migrateData(versionedData);
|
||||
if (!versionedData) {
|
||||
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
|
||||
localStore.setMetadata(versionedData.meta);
|
||||
@ -417,7 +437,7 @@ export async function loadStateFromPersistence() {
|
||||
localStore.set(versionedData.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 {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
|
||||
* @param isFirstMetaMaskControllerSetup
|
||||
* @param {object} stateMetadata - Metadata about the initial state and migrations, including the most recent migration version
|
||||
*/
|
||||
export function setupController(
|
||||
initState,
|
||||
initLangCode,
|
||||
overrides,
|
||||
isFirstMetaMaskControllerSetup,
|
||||
stateMetadata,
|
||||
) {
|
||||
//
|
||||
// MetaMask Controller
|
||||
@ -462,6 +484,7 @@ export function setupController(
|
||||
localStore,
|
||||
overrides,
|
||||
isFirstMetaMaskControllerSetup,
|
||||
currentMigrationVersion: stateMetadata.version,
|
||||
});
|
||||
|
||||
setupEnsIpfsResolver({
|
||||
@ -880,14 +903,9 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
|
||||
});
|
||||
|
||||
function setupSentryGetStateGlobal(store) {
|
||||
global.stateHooks.getSentryState = function () {
|
||||
const fullState = store.getState();
|
||||
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
|
||||
return {
|
||||
browser: window.navigator.userAgent,
|
||||
store: debugState,
|
||||
version: platform.getVersion(),
|
||||
};
|
||||
global.stateHooks.getSentryAppState = function () {
|
||||
const backgroundState = store.memStore.getState();
|
||||
return maskObject(backgroundState, SENTRY_BACKGROUND_STATE);
|
||||
};
|
||||
}
|
||||
|
||||
|
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,
|
||||
onInfuraIsBlocked: sinon.stub(),
|
||||
onInfuraIsUnblocked: sinon.stub(),
|
||||
networkConfigurations: {},
|
||||
});
|
||||
preferences.setAddresses([
|
||||
'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(
|
||||
async () => await this.keyringController.addNewAccount(keyring),
|
||||
);
|
||||
for (let i = 0; i < newAccounts.length; i++) {
|
||||
await this.keyringController.addNewAccount(keyring);
|
||||
}
|
||||
|
||||
const allAccounts = await this.keyringController.getAccounts();
|
||||
|
||||
@ -303,13 +303,34 @@ export default class MMIController extends EventEmitter {
|
||||
...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) => {
|
||||
if (!oldAccounts.includes(address.toLowerCase())) {
|
||||
const label = newAccounts
|
||||
.filter((item) => item.toLowerCase() === address)
|
||||
.map((item) => accounts[item].name)[0];
|
||||
// Convert the address to lowercase for consistent comparisons
|
||||
const lowercasedAddress = address.toLowerCase();
|
||||
|
||||
// If the address is not in oldAccounts
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.accountTracker.syncWithAddresses(accountsToTrack);
|
||||
@ -569,7 +590,7 @@ export default class MMIController extends EventEmitter {
|
||||
const mmiDashboardData = await this.handleMmiDashboardData();
|
||||
const cookieSetUrls =
|
||||
this.mmiConfigurationController.store.mmiConfiguration?.portfolio
|
||||
?.cookieSetUrls;
|
||||
?.cookieSetUrls || [];
|
||||
setDashboardCookie(mmiDashboardData, cookieSetUrls);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -578,7 +599,12 @@ export default class MMIController extends EventEmitter {
|
||||
}
|
||||
|
||||
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')) {
|
||||
return await this.signatureController.newUnsignedTypedMessage(
|
||||
|
@ -53,6 +53,7 @@ describe('MMIController', function () {
|
||||
onInfuraIsBlocked: jest.fn(),
|
||||
onInfuraIsUnblocked: jest.fn(),
|
||||
provider: {},
|
||||
networkConfigurations: {},
|
||||
}),
|
||||
appStateController: new AppStateController({
|
||||
addUnlockListener: jest.fn(),
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
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 { ThemeType } from '../../../shared/constants/preferences';
|
||||
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';
|
||||
///: 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 {
|
||||
/**
|
||||
*
|
||||
@ -25,6 +39,13 @@ export default class PreferencesController {
|
||||
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
|
||||
*/
|
||||
constructor(opts = {}) {
|
||||
const addedNonMainNetwork = Object.values(
|
||||
opts.networkConfigurations,
|
||||
).reduce((acc, element) => {
|
||||
acc[element.chainId] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const initState = {
|
||||
useBlockie: false,
|
||||
useNonceField: false,
|
||||
@ -51,8 +72,11 @@ export default class PreferencesController {
|
||||
// Feature flag toggling is available in the global namespace
|
||||
// for convenient testing of pre-release features, and should never
|
||||
// perform sensitive operations.
|
||||
featureFlags: {
|
||||
showIncomingTransactions: true,
|
||||
featureFlags: {},
|
||||
incomingTransactionsPreferences: {
|
||||
...mainNetworks,
|
||||
...addedNonMainNetwork,
|
||||
...testNetworks,
|
||||
},
|
||||
knownMethodData: {},
|
||||
currentLocale: opts.initLangCode,
|
||||
@ -84,6 +108,7 @@ export default class PreferencesController {
|
||||
};
|
||||
|
||||
this.network = opts.network;
|
||||
|
||||
this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
|
||||
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
|
||||
this.store = new ObservableStore(initState);
|
||||
@ -448,7 +473,7 @@ export default class PreferencesController {
|
||||
* found in the settings page.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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() {
|
||||
return this.store.getState().disabledRpcMethodPreferences;
|
||||
}
|
||||
@ -570,6 +607,7 @@ export default class PreferencesController {
|
||||
}
|
||||
this.store.updateState({ snapRegistryList: snapRegistry });
|
||||
}
|
||||
|
||||
///: 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 { TokenListController } from '@metamask/assets-controllers';
|
||||
import { CHAIN_IDS } from '../../../shared/constants/network';
|
||||
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 tokenListController;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
const tokenListMessenger = new ControllerMessenger().getRestricted({
|
||||
name: 'TokenListController',
|
||||
});
|
||||
tokenListController = new TokenListController({
|
||||
chainId: '1',
|
||||
preventPollingOnNetworkRestart: false,
|
||||
onNetworkStateChange: sinon.spy(),
|
||||
onPreferencesStateChange: sinon.spy(),
|
||||
onNetworkStateChange: jest.fn(),
|
||||
onPreferencesStateChange: jest.fn(),
|
||||
messenger: tokenListMessenger,
|
||||
});
|
||||
|
||||
preferencesController = new PreferencesController({
|
||||
initLangCode: 'en_US',
|
||||
tokenListController,
|
||||
onInfuraIsBlocked: sinon.spy(),
|
||||
onInfuraIsUnblocked: sinon.spy(),
|
||||
onInfuraIsBlocked: jest.fn(),
|
||||
onInfuraIsUnblocked: jest.fn(),
|
||||
networkConfigurations: NETWORK_CONFIGURATION_DATA,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
describe('useBlockie', () => {
|
||||
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);
|
||||
assert.equal(preferencesController.store.getState().useBlockie, true);
|
||||
expect(preferencesController.store.getState().useBlockie).toStrictEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCurrentLocale', function () {
|
||||
it('checks the default currentLocale', function () {
|
||||
describe('setCurrentLocale', () => {
|
||||
it('checks the default currentLocale', () => {
|
||||
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');
|
||||
const { currentLocale } = preferencesController.store.getState();
|
||||
assert.equal(currentLocale, 'ja');
|
||||
expect(currentLocale).toStrictEqual('ja');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAddresses', function () {
|
||||
it('should keep a map of addresses to names and addresses in the store', function () {
|
||||
describe('setAddresses', () => {
|
||||
it('should keep a map of addresses to names and addresses in the store', () => {
|
||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||
|
||||
const { identities } = preferencesController.store.getState();
|
||||
assert.deepEqual(identities, {
|
||||
expect(identities).toStrictEqual({
|
||||
'0xda22le': {
|
||||
name: 'Account 1',
|
||||
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(['0xda22le77', '0x7e57e277']);
|
||||
|
||||
const { identities } = preferencesController.store.getState();
|
||||
assert.deepEqual(identities, {
|
||||
expect(identities).toStrictEqual({
|
||||
'0xda22le77': {
|
||||
name: 'Account 1',
|
||||
address: '0xda22le77',
|
||||
@ -91,237 +109,235 @@ describe('preferences controller', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAddress', function () {
|
||||
it('should remove an address from state', function () {
|
||||
describe('removeAddress', () => {
|
||||
it('should remove an address from state', () => {
|
||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||
|
||||
preferencesController.removeAddress('0xda22le');
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
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.setSelectedAddress('0x7e57e2');
|
||||
preferencesController.removeAddress('0x7e57e2');
|
||||
|
||||
assert.equal(preferencesController.getSelectedAddress(), '0xda22le');
|
||||
expect(preferencesController.getSelectedAddress()).toStrictEqual(
|
||||
'0xda22le',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAccountLabel', function () {
|
||||
it('should update a label for the given account', function () {
|
||||
describe('setAccountLabel', () => {
|
||||
it('should update a label for the given account', () => {
|
||||
preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
|
||||
|
||||
assert.deepEqual(
|
||||
expect(
|
||||
preferencesController.store.getState().identities['0xda22le'],
|
||||
{
|
||||
).toStrictEqual({
|
||||
name: 'Account 1',
|
||||
address: '0xda22le',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
preferencesController.setAccountLabel('0xda22le', 'Dazzle');
|
||||
assert.deepEqual(
|
||||
expect(
|
||||
preferencesController.store.getState().identities['0xda22le'],
|
||||
{
|
||||
).toStrictEqual({
|
||||
name: 'Dazzle',
|
||||
address: '0xda22le',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPasswordForgotten', function () {
|
||||
it('should default to false', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.forgottenPassword, false);
|
||||
});
|
||||
|
||||
it('should set the forgottenPassword property in state', function () {
|
||||
assert.equal(
|
||||
describe('setPasswordForgotten', () => {
|
||||
it('should default to false', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().forgottenPassword,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('should set the forgottenPassword property in state', () => {
|
||||
preferencesController.setPasswordForgotten(true);
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().forgottenPassword,
|
||||
true,
|
||||
);
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUsePhishDetect', function () {
|
||||
it('should default to true', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
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(
|
||||
describe('setUsePhishDetect', () => {
|
||||
it('should default to true', () => {
|
||||
expect(
|
||||
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 () {
|
||||
it('should default to true', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.useMultiAccountBalanceChecker, true);
|
||||
});
|
||||
|
||||
it('should set the setUseMultiAccountBalanceChecker property in state', function () {
|
||||
assert.equal(
|
||||
describe('setUseMultiAccountBalanceChecker', () => {
|
||||
it('should default to true', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
||||
true,
|
||||
);
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should set the setUseMultiAccountBalanceChecker property in state', () => {
|
||||
preferencesController.setUseMultiAccountBalanceChecker(false);
|
||||
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().useMultiAccountBalanceChecker,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUseTokenDetection', function () {
|
||||
it('should default to false', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.useTokenDetection, false);
|
||||
});
|
||||
|
||||
it('should set the useTokenDetection property in state', function () {
|
||||
assert.equal(
|
||||
describe('setUseTokenDetection', () => {
|
||||
it('should default to false', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().useTokenDetection,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('should set the useTokenDetection property in state', () => {
|
||||
preferencesController.setUseTokenDetection(true);
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().useTokenDetection,
|
||||
true,
|
||||
);
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUseNftDetection', function () {
|
||||
it('should default to false', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.useNftDetection, false);
|
||||
});
|
||||
|
||||
it('should set the useNftDetection property in state', function () {
|
||||
assert.equal(
|
||||
describe('setUseNftDetection', () => {
|
||||
it('should default to false', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().useNftDetection,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('should set the useNftDetection property in state', () => {
|
||||
preferencesController.setOpenSeaEnabled(true);
|
||||
preferencesController.setUseNftDetection(true);
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().useNftDetection,
|
||||
true,
|
||||
);
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUse4ByteResolution', function () {
|
||||
it('should default to true', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.use4ByteResolution, true);
|
||||
expect(
|
||||
preferencesController.store.getState().use4ByteResolution,
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should set the use4ByteResolution property in state', function () {
|
||||
assert.equal(
|
||||
preferencesController.store.getState().use4ByteResolution,
|
||||
true,
|
||||
);
|
||||
preferencesController.setUse4ByteResolution(false);
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().use4ByteResolution,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOpenSeaEnabled', function () {
|
||||
it('should default to false', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.openSeaEnabled, false);
|
||||
});
|
||||
|
||||
it('should set the openSeaEnabled property in state', function () {
|
||||
assert.equal(
|
||||
describe('setOpenSeaEnabled', () => {
|
||||
it('should default to false', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().openSeaEnabled,
|
||||
false,
|
||||
);
|
||||
).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it('should set the openSeaEnabled property in state', () => {
|
||||
preferencesController.setOpenSeaEnabled(true);
|
||||
assert.equal(preferencesController.store.getState().openSeaEnabled, true);
|
||||
expect(
|
||||
preferencesController.store.getState().openSeaEnabled,
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAdvancedGasFee', function () {
|
||||
it('should default to null', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.advancedGasFee, null);
|
||||
describe('setAdvancedGasFee', () => {
|
||||
it('should default to null', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().advancedGasFee,
|
||||
).toStrictEqual(null);
|
||||
});
|
||||
|
||||
it('should set the setAdvancedGasFee property in state', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.advancedGasFee, null);
|
||||
it('should set the setAdvancedGasFee property in state', () => {
|
||||
preferencesController.setAdvancedGasFee({
|
||||
maxBaseFee: '1.5',
|
||||
priorityFee: '2',
|
||||
});
|
||||
assert.equal(
|
||||
expect(
|
||||
preferencesController.store.getState().advancedGasFee.maxBaseFee,
|
||||
'1.5',
|
||||
);
|
||||
assert.equal(
|
||||
).toStrictEqual('1.5');
|
||||
expect(
|
||||
preferencesController.store.getState().advancedGasFee.priorityFee,
|
||||
'2',
|
||||
);
|
||||
).toStrictEqual('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTheme', function () {
|
||||
it('should default to value "OS"', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.theme, 'os');
|
||||
describe('setTheme', () => {
|
||||
it('should default to value "OS"', () => {
|
||||
expect(preferencesController.store.getState().theme).toStrictEqual('os');
|
||||
});
|
||||
|
||||
it('should set the setTheme property in state', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.theme, 'os');
|
||||
it('should set the setTheme property in state', () => {
|
||||
preferencesController.setTheme('dark');
|
||||
assert.equal(preferencesController.store.getState().theme, 'dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUseCurrencyRateCheck', function () {
|
||||
it('should default to false', function () {
|
||||
const state = preferencesController.store.getState();
|
||||
assert.equal(state.useCurrencyRateCheck, true);
|
||||
});
|
||||
|
||||
it('should set the useCurrencyRateCheck property in state', function () {
|
||||
assert.equal(
|
||||
preferencesController.store.getState().useCurrencyRateCheck,
|
||||
true,
|
||||
expect(preferencesController.store.getState().theme).toStrictEqual(
|
||||
'dark',
|
||||
);
|
||||
preferencesController.setUseCurrencyRateCheck(false);
|
||||
assert.equal(
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUseCurrencyRateCheck', () => {
|
||||
it('should default to false', () => {
|
||||
expect(
|
||||
preferencesController.store.getState().useCurrencyRateCheck,
|
||||
).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should set the useCurrencyRateCheck property in state', () => {
|
||||
preferencesController.setUseCurrencyRateCheck(false);
|
||||
expect(
|
||||
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,
|
||||
);
|
||||
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 PendingTransactionTracker from './pending-tx-tracker';
|
||||
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 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 {Function} opts.getNetworkId - Get the current network ID.
|
||||
* @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 {object} opts.blockTracker - An instance of eth-blocktracker
|
||||
* @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 {Function} opts.signTransaction - ethTx signer that returns a rawTx
|
||||
* @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
|
||||
*/
|
||||
|
||||
@ -142,6 +146,7 @@ export default class TransactionController extends EventEmitter {
|
||||
super();
|
||||
this.getNetworkId = opts.getNetworkId;
|
||||
this.getNetworkStatus = opts.getNetworkStatus;
|
||||
this._getNetworkState = opts.getNetworkState;
|
||||
this._getCurrentChainId = opts.getCurrentChainId;
|
||||
this.getProviderConfig = opts.getProviderConfig;
|
||||
this._getCurrentNetworkEIP1559Compatibility =
|
||||
@ -166,6 +171,7 @@ export default class TransactionController extends EventEmitter {
|
||||
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
|
||||
this.securityProviderRequest = opts.securityProviderRequest;
|
||||
this.messagingSystem = opts.messenger;
|
||||
this._hasCompletedOnboarding = opts.hasCompletedOnboarding;
|
||||
|
||||
this.memStore = new ObservableStore({});
|
||||
|
||||
@ -216,6 +222,33 @@ export default class TransactionController extends EventEmitter {
|
||||
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.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
|
||||
//
|
||||
@ -1779,7 +1824,11 @@ export default class TransactionController extends EventEmitter {
|
||||
// MMI does not broadcast transactions, as that is the responsibility of the custodian
|
||||
if (txMeta.custodyStatus) {
|
||||
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);
|
||||
// MMI relies on custodian to publish transactions so exits this code path early
|
||||
return;
|
||||
}
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
@ -2082,11 +2131,18 @@ export default class TransactionController extends EventEmitter {
|
||||
* Updates the memStore in transaction controller
|
||||
*/
|
||||
_updateMemstore() {
|
||||
const { transactions } = this.store.getState();
|
||||
const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
|
||||
|
||||
const currentNetworkTxList = this.txStateManager.getTransactions({
|
||||
limit: MAX_MEMSTORE_TX_LIST_SIZE,
|
||||
});
|
||||
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList });
|
||||
|
||||
this.memStore.updateState({
|
||||
unapprovedTxs,
|
||||
currentNetworkTxList,
|
||||
transactions,
|
||||
});
|
||||
}
|
||||
|
||||
_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
|
||||
|
||||
async _requestTransactionApproval(
|
||||
|
@ -38,6 +38,7 @@ import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
|
||||
import { NetworkStatus } from '../../../../shared/constants/network';
|
||||
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
|
||||
import TxGasUtil from './tx-gas-utils';
|
||||
import * as IncomingTransactionHelperClass from './IncomingTransactionHelper';
|
||||
import TransactionController from '.';
|
||||
|
||||
const noop = () => true;
|
||||
@ -51,6 +52,16 @@ const actionId = 'DUMMY_ACTION_ID';
|
||||
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
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() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
@ -65,7 +76,9 @@ describe('Transaction Controller', function () {
|
||||
getCurrentChainId,
|
||||
messengerMock,
|
||||
resultCallbacksMock,
|
||||
updateSpy;
|
||||
updateSpy,
|
||||
incomingTransactionHelperClassMock,
|
||||
incomingTransactionHelperEventMock;
|
||||
|
||||
beforeEach(function () {
|
||||
fragmentExists = false;
|
||||
@ -101,6 +114,16 @@ describe('Transaction Controller', function () {
|
||||
call: sinon.stub(),
|
||||
};
|
||||
|
||||
incomingTransactionHelperEventMock = sinon.spy();
|
||||
|
||||
incomingTransactionHelperClassMock = sinon
|
||||
.stub(IncomingTransactionHelperClass, 'IncomingTransactionHelper')
|
||||
.returns({
|
||||
hub: {
|
||||
on: incomingTransactionHelperEventMock,
|
||||
},
|
||||
});
|
||||
|
||||
txController = new TransactionController({
|
||||
provider,
|
||||
getGasPrice() {
|
||||
@ -148,6 +171,10 @@ describe('Transaction Controller', function () {
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
incomingTransactionHelperClassMock.restore();
|
||||
});
|
||||
|
||||
function getLastTxMeta() {
|
||||
return updateSpy.lastCall.args[0];
|
||||
}
|
||||
@ -3374,4 +3401,78 @@ describe('Transaction Controller', function () {
|
||||
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) {
|
||||
this.config = config;
|
||||
this.removeAllListeners();
|
||||
const initialState = {};
|
||||
for (const key of Object.keys(config)) {
|
||||
if (!config[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', () => {
|
||||
const controllerMessenger = new ControllerMessenger();
|
||||
const fooStore = new ObservableStore({ foo: 'foo' });
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { prependZero } from '../../../shared/modules/string-utils';
|
||||
|
||||
export default class BackupController {
|
||||
export default class Backup {
|
||||
constructor(opts = {}) {
|
||||
const {
|
||||
preferencesController,
|
@ -1,6 +1,6 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import BackupController from './backup';
|
||||
import Backup from './backup';
|
||||
|
||||
function getMockPreferencesController() {
|
||||
const mcState = {
|
||||
@ -131,7 +131,6 @@ const jsonData = JSON.stringify({
|
||||
advancedGasFee: null,
|
||||
featureFlags: {
|
||||
sendHexData: true,
|
||||
showIncomingTransactions: true,
|
||||
},
|
||||
knownMethodData: {},
|
||||
currentLocale: 'en',
|
||||
@ -151,9 +150,9 @@ const jsonData = JSON.stringify({
|
||||
},
|
||||
});
|
||||
|
||||
describe('BackupController', function () {
|
||||
const getBackupController = () => {
|
||||
return new BackupController({
|
||||
describe('Backup', function () {
|
||||
const getBackup = () => {
|
||||
return new Backup({
|
||||
preferencesController: getMockPreferencesController(),
|
||||
addressBookController: getMockAddressBookController(),
|
||||
networkController: getMockNetworkController(),
|
||||
@ -163,85 +162,81 @@ describe('BackupController', function () {
|
||||
|
||||
describe('constructor', function () {
|
||||
it('should setup correctly', async function () {
|
||||
const backupController = getBackupController();
|
||||
const selectedAddress =
|
||||
backupController.preferencesController.getSelectedAddress();
|
||||
const backup = getBackup();
|
||||
const selectedAddress = backup.preferencesController.getSelectedAddress();
|
||||
assert.equal(selectedAddress, '0x01');
|
||||
});
|
||||
|
||||
it('should restore backup', async function () {
|
||||
const backupController = getBackupController();
|
||||
await backupController.restoreUserData(jsonData);
|
||||
const backup = getBackup();
|
||||
await backup.restoreUserData(jsonData);
|
||||
// check networks backup
|
||||
assert.equal(
|
||||
backupController.networkController.state.networkConfigurations[
|
||||
backup.networkController.state.networkConfigurations[
|
||||
'network-configuration-id-1'
|
||||
].chainId,
|
||||
'0x539',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.networkController.state.networkConfigurations[
|
||||
backup.networkController.state.networkConfigurations[
|
||||
'network-configuration-id-2'
|
||||
].chainId,
|
||||
'0x38',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.networkController.state.networkConfigurations[
|
||||
backup.networkController.state.networkConfigurations[
|
||||
'network-configuration-id-3'
|
||||
].chainId,
|
||||
'0x61',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.networkController.state.networkConfigurations[
|
||||
backup.networkController.state.networkConfigurations[
|
||||
'network-configuration-id-4'
|
||||
].chainId,
|
||||
'0x89',
|
||||
);
|
||||
// make sure identities are not lost after restore
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.identities[
|
||||
backup.preferencesController.store.identities[
|
||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||
].lastSelected,
|
||||
1655380342907,
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.identities[
|
||||
backup.preferencesController.store.identities[
|
||||
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
|
||||
].name,
|
||||
'Account 3',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.lostIdentities[
|
||||
backup.preferencesController.store.lostIdentities[
|
||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||
].lastSelected,
|
||||
1655379648197,
|
||||
);
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.lostIdentities[
|
||||
backup.preferencesController.store.lostIdentities[
|
||||
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
|
||||
].name,
|
||||
'Ledger 1',
|
||||
);
|
||||
// make sure selected address is not lost after restore
|
||||
assert.equal(
|
||||
backupController.preferencesController.store.selectedAddress,
|
||||
'0x01',
|
||||
);
|
||||
assert.equal(backup.preferencesController.store.selectedAddress, '0x01');
|
||||
// check address book backup
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
backup.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].chainId,
|
||||
'0x61',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
backup.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].address,
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06',
|
||||
);
|
||||
assert.equal(
|
||||
backupController.addressBookController.store.addressBook['0x61'][
|
||||
backup.addressBookController.store.addressBook['0x61'][
|
||||
'0x42EB768f2244C8811C63729A21A3569731535f06'
|
||||
].isEns,
|
||||
false,
|
@ -16,6 +16,7 @@ export default class ExtensionStore {
|
||||
// once data persistence fails once and it flips true we don't send further
|
||||
// data persistence errors to sentry
|
||||
this.dataPersistenceFailing = false;
|
||||
this.mostRecentRetrievedState = null;
|
||||
}
|
||||
|
||||
setMetadata(initMetaData) {
|
||||
@ -66,8 +67,10 @@ export default class ExtensionStore {
|
||||
// extension.storage.local always returns an obj
|
||||
// if the object is empty, treat it as undefined
|
||||
if (isEmpty(result)) {
|
||||
this.mostRecentRetrievedState = null;
|
||||
return undefined;
|
||||
}
|
||||
this.mostRecentRetrievedState = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,12 @@ import browser from 'webextension-polyfill';
|
||||
import LocalStore from './local-store';
|
||||
|
||||
jest.mock('webextension-polyfill', () => ({
|
||||
runtime: { lastError: null },
|
||||
storage: { local: true },
|
||||
}));
|
||||
|
||||
const setup = ({ isSupported }) => {
|
||||
browser.storage.local = isSupported;
|
||||
const setup = ({ localMock = jest.fn() } = {}) => {
|
||||
browser.storage.local = localMock;
|
||||
return new LocalStore();
|
||||
};
|
||||
describe('LocalStore', () => {
|
||||
@ -15,21 +16,27 @@ describe('LocalStore', () => {
|
||||
});
|
||||
describe('contructor', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('should initialize mostRecentRetrievedState to null', () => {
|
||||
const localStore = setup({ localMock: false });
|
||||
|
||||
expect(localStore.mostRecentRetrievedState).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMetadata', () => {
|
||||
it('should set the metadata property on LocalStore', () => {
|
||||
const metadata = { version: 74 };
|
||||
const localStore = setup({ isSupported: true });
|
||||
const localStore = setup();
|
||||
localStore.setMetadata(metadata);
|
||||
|
||||
expect(localStore.metadata).toStrictEqual(metadata);
|
||||
@ -38,21 +45,21 @@ describe('LocalStore', () => {
|
||||
|
||||
describe('set', () => {
|
||||
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(
|
||||
'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 () => {
|
||||
const localStore = setup({ isSupported: true });
|
||||
const localStore = setup();
|
||||
await expect(() => localStore.set()).rejects.toThrow(
|
||||
'MetaMask - updated state is missing',
|
||||
);
|
||||
});
|
||||
|
||||
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(() =>
|
||||
localStore.set({ appState: { test: true } }),
|
||||
).rejects.toThrow(
|
||||
@ -61,7 +68,7 @@ describe('LocalStore', () => {
|
||||
});
|
||||
|
||||
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 });
|
||||
await expect(async function () {
|
||||
localStore.set({ appState: { test: true } });
|
||||
@ -71,9 +78,39 @@ describe('LocalStore', () => {
|
||||
|
||||
describe('get', () => {
|
||||
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();
|
||||
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 log from 'loglevel';
|
||||
|
||||
/**
|
||||
* @typedef {object} Migration
|
||||
@ -36,6 +37,8 @@ export default class Migrator extends EventEmitter {
|
||||
// perform each migration
|
||||
for (const migration of pendingMigrations) {
|
||||
try {
|
||||
log.info(`Running migration ${migration.version}...`);
|
||||
|
||||
// attempt migration and validate
|
||||
const migratedData = await migration.migrate(versionedData);
|
||||
if (!migratedData.data) {
|
||||
@ -52,6 +55,8 @@ export default class Migrator extends EventEmitter {
|
||||
// accept the migration as good
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
versionedData = migratedData;
|
||||
|
||||
log.info(`Migration ${migration.version} complete`);
|
||||
} catch (err) {
|
||||
// rewrite error message to add context without clobbering stack
|
||||
const originalErrorMessage = err.message;
|
||||
|
@ -15,6 +15,7 @@ export default class ReadOnlyNetworkStore {
|
||||
this._initialized = false;
|
||||
this._initializing = this._init();
|
||||
this._state = undefined;
|
||||
this.mostRecentRetrievedState = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,6 +48,11 @@ export default class ReadOnlyNetworkStore {
|
||||
if (!this._initialized) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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 { errorCodes, ethErrors } from 'eth-rpc-errors';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
MESSAGE_TYPE,
|
||||
UNKNOWN_TICKER_SYMBOL,
|
||||
} from '../../../../../shared/constants/app';
|
||||
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
|
||||
import {
|
||||
isPrefixedFormattedHexString,
|
||||
isSafeChainId,
|
||||
} from '../../../../../shared/modules/network.utils';
|
||||
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
|
||||
import { getValidUrl } from '../../util';
|
||||
|
||||
const addEthereumChain = {
|
||||
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
||||
@ -83,27 +83,25 @@ async function addEthereumChainHandler(
|
||||
);
|
||||
}
|
||||
|
||||
const isLocalhost = (strUrl) => {
|
||||
try {
|
||||
const url = new URL(strUrl);
|
||||
return url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
||||
} catch (error) {
|
||||
return false;
|
||||
function isLocalhostOrHttps(urlString) {
|
||||
const url = getValidUrl(urlString);
|
||||
|
||||
return (
|
||||
url !== null &&
|
||||
(url.hostname === 'localhost' ||
|
||||
url.hostname === '127.0.0.1' ||
|
||||
url.protocol === 'https:')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const firstValidRPCUrl = Array.isArray(rpcUrls)
|
||||
? rpcUrls.find(
|
||||
(rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl),
|
||||
)
|
||||
? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl))
|
||||
: null;
|
||||
|
||||
const firstValidBlockExplorerUrl =
|
||||
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
|
||||
? blockExplorerUrls.find(
|
||||
(blockExplorerUrl) =>
|
||||
isLocalhost(blockExplorerUrl) ||
|
||||
validUrl.isHttpsUri(blockExplorerUrl),
|
||||
? blockExplorerUrls.find((blockExplorerUrl) =>
|
||||
isLocalhostOrHttps(blockExplorerUrl),
|
||||
)
|
||||
: null;
|
||||
|
||||
|
@ -1,18 +1,15 @@
|
||||
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
|
||||
* accessing of a non-existent property on our window.web3 shim.
|
||||
* We collect this data to understand which sites are breaking due to the
|
||||
* removal of our window.web3.
|
||||
* accessing of a non-existent property on our window.web3 shim. We use this
|
||||
* to alert the user that they are using a legacy dapp, and will have to take
|
||||
* further steps to be able to use it.
|
||||
*/
|
||||
|
||||
const logWeb3ShimUsage = {
|
||||
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
||||
implementation: logWeb3ShimUsageHandler,
|
||||
hookNames: {
|
||||
sendMetrics: true,
|
||||
getWeb3ShimUsageState: true,
|
||||
setWeb3ShimUsageRecorded: true,
|
||||
},
|
||||
@ -21,7 +18,6 @@ export default logWeb3ShimUsage;
|
||||
|
||||
/**
|
||||
* @typedef {object} LogWeb3ShimUsageOptions
|
||||
* @property {Function} sendMetrics - A function that registers a metrics event.
|
||||
* @property {Function} getWeb3ShimUsageState - A function that gets web3 shim
|
||||
* usage state for the given origin.
|
||||
* @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim
|
||||
@ -40,24 +36,11 @@ function logWeb3ShimUsageHandler(
|
||||
res,
|
||||
_next,
|
||||
end,
|
||||
{ sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
|
||||
{ getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
|
||||
) {
|
||||
const { origin } = req;
|
||||
if (getWeb3ShimUsageState(origin) === undefined) {
|
||||
setWeb3ShimUsageRecorded(origin);
|
||||
|
||||
sendMetrics(
|
||||
{
|
||||
event: `Website Accessed window.web3 Shim`,
|
||||
category: MetaMetricsEventCategory.InpageProvider,
|
||||
referrer: {
|
||||
url: origin,
|
||||
},
|
||||
},
|
||||
{
|
||||
excludeMetaMetricsId: 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',
|
||||
};
|
||||
|
||||
// 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_STATE = {
|
||||
gas: true,
|
||||
history: true,
|
||||
metamask: {
|
||||
// This describes the subset of background controller 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_BACKGROUND_STATE = {
|
||||
AccountTracker: {
|
||||
currentBlockGasLimit: true,
|
||||
},
|
||||
AlertController: {
|
||||
alertEnabledness: true,
|
||||
completedOnboarding: true,
|
||||
},
|
||||
AppMetadataController: {
|
||||
currentAppVersion: true,
|
||||
previousAppVersion: true,
|
||||
previousMigrationVersion: true,
|
||||
currentMigrationVersion: true,
|
||||
},
|
||||
AppStateController: {
|
||||
connectedStatusPopoverHasBeenShown: true,
|
||||
defaultHomeActiveTabName: true,
|
||||
},
|
||||
CurrencyController: {
|
||||
conversionDate: true,
|
||||
conversionRate: true,
|
||||
currentBlockGasLimit: 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,
|
||||
},
|
||||
DecryptMessageController: {
|
||||
unapprovedDecryptMsgCount: true,
|
||||
},
|
||||
DesktopController: {
|
||||
desktopEnabled: true,
|
||||
},
|
||||
EncryptionPublicKeyController: {
|
||||
unapprovedEncryptionPublicKeyMsgCount: true,
|
||||
},
|
||||
KeyringController: {
|
||||
isUnlocked: true,
|
||||
},
|
||||
MetaMetricsController: {
|
||||
metaMetricsId: true,
|
||||
participateInMetaMetrics: true,
|
||||
},
|
||||
NetworkController: {
|
||||
networkId: true,
|
||||
networkStatus: true,
|
||||
nextNonce: true,
|
||||
participateInMetaMetrics: true,
|
||||
preferences: true,
|
||||
providerConfig: {
|
||||
nickname: true,
|
||||
ticker: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
OnboardingController: {
|
||||
completedOnboarding: true,
|
||||
firstTimeFlowType: true,
|
||||
seedPhraseBackedUp: true,
|
||||
unapprovedDecryptMsgCount: true,
|
||||
unapprovedEncryptionPublicKeyMsgCount: true,
|
||||
unapprovedMsgCount: true,
|
||||
unapprovedPersonalMsgCount: true,
|
||||
unapprovedTypedMessagesCount: true,
|
||||
},
|
||||
PreferencesController: {
|
||||
currentLocale: true,
|
||||
featureFlags: true,
|
||||
forgottenPassword: true,
|
||||
ipfsGateway: true,
|
||||
preferences: true,
|
||||
useBlockie: true,
|
||||
useNonceField: 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,
|
||||
},
|
||||
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 }) {
|
||||
if (!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
|
||||
* return `false` if state has not yet been initialzed.
|
||||
* Returns whether MetaMetrics is enabled. If the application hasn't yet
|
||||
* been initialized, the persisted state will be used (if any).
|
||||
*
|
||||
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||
* is enabled, `false` otherwise.
|
||||
* @returns `true` if MetaMetrics is enabled, `false` otherwise.
|
||||
*/
|
||||
async function getMetaMetricsEnabled() {
|
||||
const appState = getState();
|
||||
if (Object.keys(appState) > 0) {
|
||||
return Boolean(appState?.store?.metamask?.participateInMetaMetrics);
|
||||
if (appState.state || appState.persistedState) {
|
||||
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 {
|
||||
const persistedState = await globalThis.stateHooks.getPersistedState();
|
||||
return Boolean(
|
||||
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
||||
);
|
||||
return getMetaMetricsEnabledFromPersistedState(persistedState);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
@ -269,17 +382,15 @@ function hideUrlIfNotInternal(url) {
|
||||
*/
|
||||
export function beforeBreadcrumb(getState) {
|
||||
return (breadcrumb) => {
|
||||
if (getState) {
|
||||
const appState = getState();
|
||||
if (
|
||||
Object.values(appState).length &&
|
||||
(!appState?.store?.metamask?.participateInMetaMetrics ||
|
||||
!appState?.store?.metamask?.completedOnboarding ||
|
||||
breadcrumb?.category === 'ui.input')
|
||||
) {
|
||||
if (!getState) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const appState = getState();
|
||||
if (
|
||||
!getMetaMetricsEnabledFromAppState(appState) ||
|
||||
!getOnboardingCompleteFromAppState(appState) ||
|
||||
breadcrumb?.category === 'ui.input'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);
|
||||
|
@ -1,24 +1,27 @@
|
||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||
import {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
ENVIRONMENT_TYPE_BACKGROUND,
|
||||
PLATFORM_FIREFOX,
|
||||
PLATFORM_OPERA,
|
||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
PLATFORM_CHROME,
|
||||
PLATFORM_EDGE,
|
||||
PLATFORM_FIREFOX,
|
||||
PLATFORM_OPERA,
|
||||
} from '../../../shared/constants/app';
|
||||
import {
|
||||
TransactionEnvelopeType,
|
||||
TransactionStatus,
|
||||
TransactionType,
|
||||
TransactionEnvelopeType,
|
||||
} from '../../../shared/constants/transaction';
|
||||
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
|
||||
import {
|
||||
addUrlProtocolPrefix,
|
||||
deferredPromise,
|
||||
formatTxMetaForRpcResult,
|
||||
getEnvironmentType,
|
||||
getPlatform,
|
||||
formatTxMetaForRpcResult,
|
||||
getValidUrl,
|
||||
isWebUrl,
|
||||
} from './util';
|
||||
|
||||
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', () => {
|
||||
it('should return true for valid hex strings', () => {
|
||||
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);
|
||||
|
@ -1,24 +1,24 @@
|
||||
import urlLib from 'url';
|
||||
import { AccessList } from '@ethereumjs/tx';
|
||||
import BN from 'bn.js';
|
||||
import { memoize } from 'lodash';
|
||||
import { AccessList } from '@ethereumjs/tx';
|
||||
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
|
||||
|
||||
import {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
ENVIRONMENT_TYPE_BACKGROUND,
|
||||
PLATFORM_FIREFOX,
|
||||
PLATFORM_OPERA,
|
||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
PLATFORM_BRAVE,
|
||||
PLATFORM_CHROME,
|
||||
PLATFORM_EDGE,
|
||||
PLATFORM_BRAVE,
|
||||
PLATFORM_FIREFOX,
|
||||
PLATFORM_OPERA,
|
||||
} from '../../../shared/constants/app';
|
||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
||||
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
|
||||
import {
|
||||
TransactionEnvelopeType,
|
||||
TransactionMeta,
|
||||
} from '../../../shared/constants/transaction';
|
||||
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
|
||||
|
||||
/**
|
||||
* @see {@link getEnvironmentType}
|
||||
@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) {
|
||||
}
|
||||
|
||||
export {
|
||||
getPlatform,
|
||||
getEnvironmentType,
|
||||
hexToBn,
|
||||
BnMultiplyByFraction,
|
||||
addHexPrefix,
|
||||
getChainType,
|
||||
checkAlarmExists,
|
||||
getChainType,
|
||||
getEnvironmentType,
|
||||
getPlatform,
|
||||
hexToBn,
|
||||
};
|
||||
|
||||
// Taken from https://stackoverflow.com/a/1349426/3696652
|
||||
@ -235,10 +235,43 @@ export function previousValueComparator<A>(
|
||||
}
|
||||
|
||||
export function addUrlProtocolPrefix(urlString: string) {
|
||||
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) {
|
||||
return `https://${urlString}`;
|
||||
let trimmed = urlString.trim();
|
||||
|
||||
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 {
|
||||
|
@ -145,27 +145,21 @@ describe('MetaMaskController', function () {
|
||||
metamaskController.addNewAccount(1),
|
||||
metamaskController.addNewAccount(1),
|
||||
]);
|
||||
assert.deepEqual(
|
||||
Object.keys(addNewAccountResult1.identities),
|
||||
Object.keys(addNewAccountResult2.identities),
|
||||
);
|
||||
assert.equal(addNewAccountResult1, addNewAccountResult2);
|
||||
});
|
||||
|
||||
it('two successive calls with same accountCount give same result', async function () {
|
||||
await metamaskController.createNewVaultAndKeychain('test@123');
|
||||
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
||||
const addNewAccountResult2 = await metamaskController.addNewAccount(1);
|
||||
assert.deepEqual(
|
||||
Object.keys(addNewAccountResult1.identities),
|
||||
Object.keys(addNewAccountResult2.identities),
|
||||
);
|
||||
assert.equal(addNewAccountResult1, addNewAccountResult2);
|
||||
});
|
||||
|
||||
it('two successive calls with different accountCount give different results', async function () {
|
||||
await metamaskController.createNewVaultAndKeychain('test@123');
|
||||
const addNewAccountResult1 = await metamaskController.addNewAccount(1);
|
||||
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 Promise.all([
|
||||
metamaskController.importAccountWithStrategy('Private Key', [
|
||||
metamaskController.importAccountWithStrategy('privateKey', [
|
||||
importPrivkey,
|
||||
]),
|
||||
Promise.resolve(1).then(() => {
|
||||
keyringControllerState1 = JSON.stringify(
|
||||
metamaskController.keyringController.memStore.getState(),
|
||||
);
|
||||
metamaskController.importAccountWithStrategy('Private Key', [
|
||||
metamaskController.importAccountWithStrategy('privateKey', [
|
||||
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 () {
|
||||
const address = '0x514910771af9ca656af840dff83e8264ecf986ca';
|
||||
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