1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal

This commit is contained in:
Matthias Kretschmann 2023-08-25 17:39:52 +01:00
commit a2839ce7aa
Signed by: m
GPG Key ID: 606EEEF3C479A91F
376 changed files with 28140 additions and 8774 deletions

View File

@ -152,8 +152,14 @@ workflows:
- prep-build-test - prep-build-test
- test-e2e-chrome-snaps: - test-e2e-chrome-snaps:
requires: requires:
- prep-build-test-flask - prep-build-test
- test-e2e-firefox-snaps: - test-e2e-firefox-snaps:
requires:
- prep-build-test
- test-e2e-chrome-snaps-flask:
requires:
- prep-build-test-flask
- test-e2e-firefox-snaps-flask:
requires: requires:
- prep-build-test-flask - prep-build-test-flask
- test-e2e-chrome-mv3: - test-e2e-chrome-mv3:
@ -215,6 +221,7 @@ workflows:
- prep-build-flask - prep-build-flask
- all-tests-pass: - all-tests-pass:
requires: requires:
- test-deps-depcheck
- validate-lavamoat-allow-scripts - validate-lavamoat-allow-scripts
- validate-lavamoat-policy-build - validate-lavamoat-policy-build
- validate-lavamoat-policy-webapp - validate-lavamoat-policy-webapp
@ -847,6 +854,80 @@ jobs:
path: test/test-results/e2e.xml path: test/test-results/e2e.xml
test-e2e-firefox-snaps: test-e2e-firefox-snaps:
executor: node-browsers
parallelism: 4
steps:
- run: *shallow-git-clone
- run:
name: Install Firefox
command: ./.circleci/scripts/firefox-install.sh
- attach_workspace:
at: .
- run:
name: Move test build to dist
command: mv ./dist-test ./dist
- run:
name: Move test zips to builds
command: mv ./builds-test ./builds
- run:
name: test:e2e:firefox:snaps
command: |
if .circleci/scripts/test-run-e2e.sh
then
yarn test:e2e:firefox:snaps --retries 2 --debug --build-type=main
fi
no_output_timeout: 20m
- run:
name: Merge JUnit report
command: |
if [ "$(ls -A test/test-results/e2e)" ]; then
yarn test:e2e:report
fi
when: always
- store_artifacts:
path: test-artifacts
destination: test-artifacts
- store_test_results:
path: test/test-results/e2e.xml
test-e2e-chrome-snaps:
executor: node-browsers
parallelism: 4
steps:
- run: *shallow-git-clone
- run:
name: Re-Install Chrome
command: ./.circleci/scripts/chrome-install.sh
- attach_workspace:
at: .
- run:
name: Move test build to dist
command: mv ./dist-test ./dist
- run:
name: Move test zips to builds
command: mv ./builds-test ./builds
- run:
name: test:e2e:chrome:snaps
command: |
if .circleci/scripts/test-run-e2e.sh
then
yarn test:e2e:chrome:snaps --retries 2 --debug --build-type=main
fi
no_output_timeout: 20m
- run:
name: Merge JUnit report
command: |
if [ "$(ls -A test/test-results/e2e)" ]; then
yarn test:e2e:report
fi
when: always
- store_artifacts:
path: test-artifacts
destination: test-artifacts
- store_test_results:
path: test/test-results/e2e.xml
test-e2e-firefox-snaps-flask:
executor: node-browsers executor: node-browsers
parallelism: 4 parallelism: 4
steps: steps:
@ -883,7 +964,7 @@ jobs:
- store_test_results: - store_test_results:
path: test/test-results/e2e.xml path: test/test-results/e2e.xml
test-e2e-chrome-snaps: test-e2e-chrome-snaps-flask:
executor: node-browsers executor: node-browsers
parallelism: 4 parallelism: 4
steps: steps:

View File

@ -239,6 +239,7 @@ module.exports = {
'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/app-state.test.js',
'app/scripts/controllers/mmi-controller.test.js', 'app/scripts/controllers/mmi-controller.test.js',
'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/controllers/permissions/**/*.test.js',
'app/scripts/controllers/preferences.test.js',
'app/scripts/lib/**/*.test.js', 'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js', 'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js', 'app/scripts/platforms/*.test.js',
@ -268,6 +269,7 @@ module.exports = {
'app/scripts/controllers/app-state.test.js', 'app/scripts/controllers/app-state.test.js',
'app/scripts/controllers/mmi-controller.test.js', 'app/scripts/controllers/mmi-controller.test.js',
'app/scripts/controllers/permissions/**/*.test.js', 'app/scripts/controllers/permissions/**/*.test.js',
'app/scripts/controllers/preferences.test.js',
'app/scripts/lib/**/*.test.js', 'app/scripts/lib/**/*.test.js',
'app/scripts/migrations/*.test.js', 'app/scripts/migrations/*.test.js',
'app/scripts/platforms/*.test.js', 'app/scripts/platforms/*.test.js',

View 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;
}

View File

@ -37,4 +37,4 @@ jobs:
env: env:
RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }} RELEASE_LABEL_TOKEN: ${{ secrets.RELEASE_LABEL_TOKEN }}
NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }} NEXT_SEMVER_VERSION: ${{ env.NEXT_SEMVER_VERSION }}
run: npm run add-release-label-to-pr-and-linked-issues run: yarn run add-release-label-to-pr-and-linked-issues

View File

@ -35,4 +35,4 @@ jobs:
id: check-pr-has-required-labels id: check-pr-has-required-labels
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run check-pr-has-required-labels run: yarn run check-pr-has-required-labels

34
.github/workflows/close-bug-report.yml vendored Normal file
View 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
View 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"

View File

@ -8,6 +8,7 @@ module.exports = {
'./app/scripts/controllers/app-state.test.js', './app/scripts/controllers/app-state.test.js',
'./app/scripts/controllers/permissions/**/*.test.js', './app/scripts/controllers/permissions/**/*.test.js',
'./app/scripts/controllers/mmi-controller.test.js', './app/scripts/controllers/mmi-controller.test.js',
'./app/scripts/controllers/preferences.test.js',
'./app/scripts/constants/error-utils.test.js', './app/scripts/constants/error-utils.test.js',
'./development/fitness-functions/**/*.test.ts', './development/fitness-functions/**/*.test.ts',
'./test/e2e/helpers.test.js', './test/e2e/helpers.test.js',

View File

@ -2,6 +2,7 @@ import { draftTransactionInitialState } from '../ui/ducks/send';
import { KeyringType } from '../shared/constants/keyring'; import { KeyringType } from '../shared/constants/keyring';
import { NetworkType } from '@metamask/controller-utils'; import { NetworkType } from '@metamask/controller-utils';
import { NetworkStatus } from '@metamask/network-controller'; import { NetworkStatus } from '@metamask/network-controller';
import { CHAIN_IDS } from '../shared/constants/network';
const state = { const state = {
invalidCustomNetwork: { invalidCustomNetwork: {
@ -529,6 +530,12 @@ const state = {
preferences: { preferences: {
useNativeCurrencyAsPrimaryCurrency: true, useNativeCurrencyAsPrimaryCurrency: true,
}, },
incomingTransactionsPreferences: {
[CHAIN_IDS.MAINNET]: true,
[CHAIN_IDS.GOERLI]: false,
[CHAIN_IDS.OPTIMISM_TESTNET]: false,
[CHAIN_IDS.AVALANCHE_TESTNET]: true,
},
firstTimeFlowType: 'create', firstTimeFlowType: 'create',
completedOnboarding: true, completedOnboarding: true,
knownMethodData: { knownMethodData: {

View File

@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [10.34.5]
### Changed
- Improve error diagnostic information
- Add additional logging for state migrations ([#20424](https://github.com/MetaMask/metamask-extension/pull/20424), [#20517](https://github.com/MetaMask/metamask-extension/pull/20517), [#20521](https://github.com/MetaMask/metamask-extension/pull/20521))
- Improve diagnostic state snapshot ([#20457](https://github.com/MetaMask/metamask-extension/pull/20457), [#20491](https://github.com/MetaMask/metamask-extension/pull/20491), [#20428](https://github.com/MetaMask/metamask-extension/pull/20428), [#20458](https://github.com/MetaMask/metamask-extension/pull/20458))
- Capture additional errors when state migrations fail ([#20427](https://github.com/MetaMask/metamask-extension/pull/20427))
### Fixed
- Fix bug where state was temporarily incomplete upon initial load ([#20468](https://github.com/MetaMask/metamask-extension/pull/20468))
- In rare circumstances, this bug may have resulted in data loss (of preferences, permissions, or tracked NFTs/tokens) or UI crashes.
## [10.34.4] ## [10.34.4]
### Changed ### Changed
- Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420)) - Updated snaps execution environment ([#20420](https://github.com/MetaMask/metamask-extension/pull/20420))
@ -3885,7 +3896,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized ### Uncategorized
- Added the ability to restore accounts from seed words. - Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...HEAD [Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.34.5...HEAD
[10.34.5]: https://github.com/MetaMask/metamask-extension/compare/v10.34.4...v10.34.5
[10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4 [10.34.4]: https://github.com/MetaMask/metamask-extension/compare/v10.34.3...v10.34.4
[10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3 [10.34.3]: https://github.com/MetaMask/metamask-extension/compare/v10.34.2...v10.34.3
[10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2 [10.34.2]: https://github.com/MetaMask/metamask-extension/compare/v10.34.1...v10.34.2

View File

@ -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 --leave-running Leaves the browser running after a test fails, along with
anything else that the test used (ganache, the test dapp, anything else that the test used (ganache, the test dapp,
etc.) [boolean] [default: false] etc.) [boolean] [default: false]
--update-snapshot Update E2E test snapshots
[alias: -u] [boolean] [default: false]
``` ```
For example, to run the `account-details` tests using Chrome, with debug logging and with the browser set to remain open upon failure, you would use: For example, to run the `account-details` tests using Chrome, with debug logging and with the browser set to remain open upon failure, you would use:

View File

@ -169,9 +169,6 @@
"copyAddress": { "copyAddress": {
"message": "አድራሻን ወደ ቅንጥብ ሰሌዳ ቅዳ" "message": "አድራሻን ወደ ቅንጥብ ሰሌዳ ቅዳ"
}, },
"copyPrivateKey": {
"message": "የግል ቁልፍዎ ይህ ነው (ለመቅዳት ጠቅ ያድርጉ)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "ወደ ቅንጥብ ሰሌዳ ገልብጥ" "message": "ወደ ቅንጥብ ሰሌዳ ገልብጥ"
}, },
@ -232,9 +229,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "በ ENS የስም ምዝገባ ላይ የተፈጠረ ስህተት" "message": "በ ENS የስም ምዝገባ ላይ የተፈጠረ ስህተት"
}, },
"enterPassword": {
"message": "የይለፍ ቃል ያስገቡ"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "ለመቀጠል የይለፍ ቃል ያስገቡ" "message": "ለመቀጠል የይለፍ ቃል ያስገቡ"
}, },
@ -247,9 +241,6 @@
"expandView": { "expandView": {
"message": "እይታን ዘርጋ" "message": "እይታን ዘርጋ"
}, },
"exportPrivateKey": {
"message": "የግል ቁልፍን ላክ"
},
"failed": { "failed": {
"message": "አልተሳካም" "message": "አልተሳካም"
}, },
@ -648,9 +639,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "በመላኪያ ማያ ላይ የ hex ውሂብ መስክን ለማሳየት ይህን ይምረጡ" "message": "በመላኪያ ማያ ላይ የ hex ውሂብ መስክን ለማሳየት ይህን ይምረጡ"
}, },
"showPrivateKeys": {
"message": "የግል ቁልፎችን አሳይ"
},
"sigRequest": { "sigRequest": {
"message": "የፊርማ ጥያቄ" "message": "የፊርማ ጥያቄ"
}, },
@ -771,9 +759,6 @@
"tryAgain": { "tryAgain": {
"message": "እንደገና ሞክር" "message": "እንደገና ሞክር"
}, },
"typePassword": {
"message": "የ MetaMask የይለፍ ቃልዎን ይጻፉ"
},
"unapproved": { "unapproved": {
"message": "ያልተፈቀደ" "message": "ያልተፈቀደ"
}, },

View File

@ -179,9 +179,6 @@
"copyAddress": { "copyAddress": {
"message": "نسخ العنوان إلى الحافظة" "message": "نسخ العنوان إلى الحافظة"
}, },
"copyPrivateKey": {
"message": "هذا هو مفتاحك الخاص (انقر للنسخ)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "نسخ إلى الحافظة" "message": "نسخ إلى الحافظة"
}, },
@ -245,9 +242,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "خطأ في تسجيل اسم ENS" "message": "خطأ في تسجيل اسم ENS"
}, },
"enterPassword": {
"message": "أدخل كلمة مرور"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "أدخل كلمة المرور للمتابعة" "message": "أدخل كلمة المرور للمتابعة"
}, },
@ -260,9 +254,6 @@
"expandView": { "expandView": {
"message": "توسيع العرض" "message": "توسيع العرض"
}, },
"exportPrivateKey": {
"message": "تصدير المفتاح الخاص"
},
"failed": { "failed": {
"message": "فشل" "message": "فشل"
}, },
@ -660,9 +651,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "حدد هذا لإظهار حقل بيانات سداسي عشرية على شاشة الإرسال" "message": "حدد هذا لإظهار حقل بيانات سداسي عشرية على شاشة الإرسال"
}, },
"showPrivateKeys": {
"message": "عرض المفاتيح الخاصة"
},
"sigRequest": { "sigRequest": {
"message": "طلب التوقيع" "message": "طلب التوقيع"
}, },
@ -783,9 +771,6 @@
"tryAgain": { "tryAgain": {
"message": "إعادة المحاولة" "message": "إعادة المحاولة"
}, },
"typePassword": {
"message": "أدخل كلمة مرور MetaMask الخاصة بك"
},
"unapproved": { "unapproved": {
"message": "تم الرفض" "message": "تم الرفض"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Копирайте адреса в клипборда" "message": "Копирайте адреса в клипборда"
}, },
"copyPrivateKey": {
"message": "Това е Вашият личен ключ (кликнете, за да го копирате)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Копиране в буферната памет" "message": "Копиране в буферната памет"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Грешка при регистрацията на име на ENS" "message": "Грешка при регистрацията на име на ENS"
}, },
"enterPassword": {
"message": "Въведете парола"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Въведете парола, за да продължите" "message": "Въведете парола, за да продължите"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Разгъване на изглед" "message": "Разгъване на изглед"
}, },
"exportPrivateKey": {
"message": "Експортиране на частен ключ"
},
"failed": { "failed": {
"message": "Неуспешно" "message": "Неуспешно"
}, },
@ -659,9 +650,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Изберете това, за да се покаже полето с шестнадесетични данни на екрана за изпращане" "message": "Изберете това, за да се покаже полето с шестнадесетични данни на екрана за изпращане"
}, },
"showPrivateKeys": {
"message": "Показване на частни ключове"
},
"sigRequest": { "sigRequest": {
"message": "Заявка за подпис" "message": "Заявка за подпис"
}, },
@ -782,9 +770,6 @@
"tryAgain": { "tryAgain": {
"message": "Нов опит" "message": "Нов опит"
}, },
"typePassword": {
"message": "Въведете паролата си за MetaMask"
},
"unapproved": { "unapproved": {
"message": "Неодобрено" "message": "Неодобрено"
}, },

View File

@ -172,9 +172,6 @@
"copyAddress": { "copyAddress": {
"message": "ক্লিপবোর্ডে ঠিকানা কপি করুন" "message": "ক্লিপবোর্ডে ঠিকানা কপি করুন"
}, },
"copyPrivateKey": {
"message": "এটি হল আপনার গোপন কী (কপি করতে ক্লিক করুন)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "ক্লিপবোর্ডে কপি করুন" "message": "ক্লিপবোর্ডে কপি করুন"
}, },
@ -238,9 +235,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "ENS নাম নিবন্ধীকরণে ত্রুটি হয়েছে" "message": "ENS নাম নিবন্ধীকরণে ত্রুটি হয়েছে"
}, },
"enterPassword": {
"message": "পাসওয়ার্ড লিখুন"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "অবিরত রাখতে পাসওয়ার্ড লিখুন" "message": "অবিরত রাখতে পাসওয়ার্ড লিখুন"
}, },
@ -253,9 +247,6 @@
"expandView": { "expandView": {
"message": "ভিউ সম্প্রসারিত করুন" "message": "ভিউ সম্প্রসারিত করুন"
}, },
"exportPrivateKey": {
"message": "ব্যক্তিগত কী রপ্তানি করুন"
},
"failed": { "failed": {
"message": "ব্যর্থ হয়েছে" "message": "ব্যর্থ হয়েছে"
}, },
@ -657,9 +648,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "পাঠাবার স্ক্রিনে হেক্স ডেটা ফিল্ডটি দেখাবার জন্য এটি নির্বাচন করুন" "message": "পাঠাবার স্ক্রিনে হেক্স ডেটা ফিল্ডটি দেখাবার জন্য এটি নির্বাচন করুন"
}, },
"showPrivateKeys": {
"message": "গোপনীয় কীগুলি দেখান"
},
"sigRequest": { "sigRequest": {
"message": "স্বাক্ষরের অনুরোধ" "message": "স্বাক্ষরের অনুরোধ"
}, },
@ -780,9 +768,6 @@
"tryAgain": { "tryAgain": {
"message": "আবার করুন" "message": "আবার করুন"
}, },
"typePassword": {
"message": "আপনার MetaMask পাসওয়ার্ড টাইপ করুন"
},
"unapproved": { "unapproved": {
"message": "অননুমোদিত" "message": "অননুমোদিত"
}, },

View File

@ -172,9 +172,6 @@
"copyAddress": { "copyAddress": {
"message": "Copiar adreça al porta-retalls" "message": "Copiar adreça al porta-retalls"
}, },
"copyPrivateKey": {
"message": "Aquesta és la teva clau privada (fes clic per a copiar)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copia al porta-retalls" "message": "Copia al porta-retalls"
}, },
@ -238,9 +235,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Error al registre de nom ENS" "message": "Error al registre de nom ENS"
}, },
"enterPassword": {
"message": "Introdueix contrasenya"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Introdueix la contrasenya per continuar" "message": "Introdueix la contrasenya per continuar"
}, },
@ -253,9 +247,6 @@
"expandView": { "expandView": {
"message": "Eixamplar Vista" "message": "Eixamplar Vista"
}, },
"exportPrivateKey": {
"message": "Exportar Clau Privada."
},
"failed": { "failed": {
"message": "Fallit" "message": "Fallit"
}, },
@ -644,9 +635,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Selecciona això per a mostrar el camp de dades Hex a la pantalla d'enviament" "message": "Selecciona això per a mostrar el camp de dades Hex a la pantalla d'enviament"
}, },
"showPrivateKeys": {
"message": "Mostrar Claus Privades"
},
"sigRequest": { "sigRequest": {
"message": "Sol·licitud de Signatura" "message": "Sol·licitud de Signatura"
}, },
@ -761,9 +749,6 @@
"tryAgain": { "tryAgain": {
"message": "Torna-ho a provar" "message": "Torna-ho a provar"
}, },
"typePassword": {
"message": "Tecleja la teva contrasenya de MetaMask"
},
"unapproved": { "unapproved": {
"message": "Pendent d'aprovació" "message": "Pendent d'aprovació"
}, },

View File

@ -72,9 +72,6 @@
"copiedExclamation": { "copiedExclamation": {
"message": "Zkopírováno!" "message": "Zkopírováno!"
}, },
"copyPrivateKey": {
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopírovat do schránky" "message": "Kopírovat do schránky"
}, },
@ -105,15 +102,9 @@
"edit": { "edit": {
"message": "Upravit" "message": "Upravit"
}, },
"enterPassword": {
"message": "Zadejte heslo"
},
"etherscanView": { "etherscanView": {
"message": "Prohlédněte si účet na Etherscan" "message": "Prohlédněte si účet na Etherscan"
}, },
"exportPrivateKey": {
"message": "Exportovat privátní klíč"
},
"failed": { "failed": {
"message": "Neúspěšné" "message": "Neúspěšné"
}, },
@ -304,9 +295,6 @@
"settings": { "settings": {
"message": "Nastavení" "message": "Nastavení"
}, },
"showPrivateKeys": {
"message": "Zobrazit privátní klíče"
},
"sigRequest": { "sigRequest": {
"message": "Požadavek podpisu" "message": "Požadavek podpisu"
}, },
@ -355,9 +343,6 @@
"transactionError": { "transactionError": {
"message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu." "message": "Chyba transakce. Vyhozena výjimka v kódu kontraktu."
}, },
"typePassword": {
"message": "Zadejte své heslo"
},
"unapproved": { "unapproved": {
"message": "Neschváleno" "message": "Neschváleno"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopier adresse til udklipsholder" "message": "Kopier adresse til udklipsholder"
}, },
"copyPrivateKey": {
"message": "Dette er din private nøgle (klik for at kopiere)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopiér til udklipsholderen" "message": "Kopiér til udklipsholderen"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Fejl i ENS-navneregistrering" "message": "Fejl i ENS-navneregistrering"
}, },
"enterPassword": {
"message": "Indtast kodeord"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Indtast adgangskode for at fortsætte" "message": "Indtast adgangskode for at fortsætte"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Udvis Visning" "message": "Udvis Visning"
}, },
"exportPrivateKey": {
"message": "Eksporter privat nøgle"
},
"failed": { "failed": {
"message": "Mislykkedes" "message": "Mislykkedes"
}, },
@ -641,9 +632,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Vælg dette for at vise hex-datafeltet på send-skærmen" "message": "Vælg dette for at vise hex-datafeltet på send-skærmen"
}, },
"showPrivateKeys": {
"message": "Vis private nøgler"
},
"sigRequest": { "sigRequest": {
"message": "Signaturforespørgsel" "message": "Signaturforespørgsel"
}, },
@ -755,9 +743,6 @@
"tryAgain": { "tryAgain": {
"message": "Prøv igen" "message": "Prøv igen"
}, },
"typePassword": {
"message": "Skriv din MetaMask-adgangskode"
},
"unapproved": { "unapproved": {
"message": "Ikke godkendt" "message": "Ikke godkendt"
}, },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -287,6 +287,9 @@
"address": { "address": {
"message": "Address" "message": "Address"
}, },
"addressCopied": {
"message": "Address copied!"
},
"advanced": { "advanced": {
"message": "Advanced" "message": "Advanced"
}, },
@ -834,6 +837,9 @@
"connectionRequest": { "connectionRequest": {
"message": "Connection request" "message": "Connection request"
}, },
"connections": {
"message": "Connections"
},
"contactUs": { "contactUs": {
"message": "Contact us" "message": "Contact us"
}, },
@ -898,9 +904,6 @@
"copyAddress": { "copyAddress": {
"message": "Copy address to clipboard" "message": "Copy address to clipboard"
}, },
"copyPrivateKey": {
"message": "This is your private key (click to copy)"
},
"copyRawTransactionData": { "copyRawTransactionData": {
"message": "Copy raw transaction data" "message": "Copy raw transaction data"
}, },
@ -1482,9 +1485,6 @@
"enterOptionalPassword": { "enterOptionalPassword": {
"message": "Enter optional password" "message": "Enter optional password"
}, },
"enterPassword": {
"message": "Enter password"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Enter password to continue" "message": "Enter password to continue"
}, },
@ -1561,11 +1561,8 @@
"exploreMetaMaskSnaps": { "exploreMetaMaskSnaps": {
"message": "Explore MetaMask Snaps" "message": "Explore MetaMask Snaps"
}, },
"exportPrivateKey": {
"message": "Export private key"
},
"extendWalletWithSnaps": { "extendWalletWithSnaps": {
"message": "Extend the wallet experience." "message": "Customize your wallet experience."
}, },
"externalExtension": { "externalExtension": {
"message": "External extension" "message": "External extension"
@ -2870,7 +2867,7 @@
"message": "👓 We are making transactions easier to read." "message": "👓 We are making transactions easier to read."
}, },
"notificationsEmptyText": { "notificationsEmptyText": {
"message": "Nothing to see here." "message": "This is where you can find notifications from your installed snaps."
}, },
"notificationsHeader": { "notificationsHeader": {
"message": "Notifications" "message": "Notifications"
@ -3019,10 +3016,6 @@
"onboardingPinExtensionTitle": { "onboardingPinExtensionTitle": {
"message": "Your MetaMask install is complete!" "message": "Your MetaMask install is complete!"
}, },
"onboardingShowIncomingTransactionsDescription": {
"message": "Showing incoming transactions in your wallet relies on communication with $1. Etherscan will have access to your Ethereum address and your IP address. View $2.",
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
},
"onboardingUsePhishingDetectionDescription": { "onboardingUsePhishingDetectionDescription": {
"message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.", "message": "Phishing detection alerts rely on communication with $1. jsDeliver will have access to your IP address. View $2.",
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
@ -3872,9 +3865,6 @@
"showPrivateKey": { "showPrivateKey": {
"message": "Show private key" "message": "Show private key"
}, },
"showPrivateKeys": {
"message": "Show Private Keys"
},
"showTestnetNetworks": { "showTestnetNetworks": {
"message": "Show test networks" "message": "Show test networks"
}, },
@ -4377,6 +4367,9 @@
"swap": { "swap": {
"message": "Swap" "message": "Swap"
}, },
"swapAdjustSlippage": {
"message": "Adjust slippage"
},
"swapAggregator": { "swapAggregator": {
"message": "Aggregator" "message": "Aggregator"
}, },
@ -4435,9 +4428,6 @@
"swapEditLimit": { "swapEditLimit": {
"message": "Edit limit" "message": "Edit limit"
}, },
"swapEditTransactionSettings": {
"message": "Edit transaction settings"
},
"swapEnableDescription": { "swapEnableDescription": {
"message": "This is required and gives MetaMask permission to swap your $1.", "message": "This is required and gives MetaMask permission to swap your $1.",
"description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps." "description": "Gives the user info about the required approval transaction for swaps. $1 will be the symbol of a token being approved for swaps."
@ -4498,6 +4488,9 @@
"message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.", "message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees.",
"description": "$1 is the selected network, e.g. Ethereum or BSC" "description": "$1 is the selected network, e.g. Ethereum or BSC"
}, },
"swapHighSlippage": {
"message": "High slippage"
},
"swapHighSlippageWarning": { "swapHighSlippageWarning": {
"message": "Slippage amount is very high." "message": "Slippage amount is very high."
}, },
@ -4512,6 +4505,9 @@
"swapLearnMore": { "swapLearnMore": {
"message": "Learn more about Swaps" "message": "Learn more about Swaps"
}, },
"swapLowSlippage": {
"message": "Low slippage"
},
"swapLowSlippageError": { "swapLowSlippageError": {
"message": "Transaction may fail, max slippage too low." "message": "Transaction may fail, max slippage too low."
}, },
@ -4622,6 +4618,20 @@
"swapShowLatestQuotes": { "swapShowLatestQuotes": {
"message": "Show latest quotes" "message": "Show latest quotes"
}, },
"swapSlippageHighDescription": {
"message": "The slippage entered ($1%) is considered very high and may result in a bad rate",
"description": "$1 is the amount of % for slippage"
},
"swapSlippageHighTitle": {
"message": "High slippage"
},
"swapSlippageLowDescription": {
"message": "A value this low ($1%) may result in a failed swap",
"description": "$1 is the amount of % for slippage"
},
"swapSlippageLowTitle": {
"message": "Low slippage"
},
"swapSlippageNegative": { "swapSlippageNegative": {
"message": "Slippage must be greater or equal to zero" "message": "Slippage must be greater or equal to zero"
}, },
@ -4635,27 +4645,15 @@
"message": "Slippage tolerance must be 15% or less. Anything higher will result in a bad rate." "message": "Slippage tolerance must be 15% or less. Anything higher will result in a bad rate."
}, },
"swapSlippageOverLimitTitle": { "swapSlippageOverLimitTitle": {
"message": "Reduce slippage to continue" "message": "Very high slippage"
}, },
"swapSlippagePercent": { "swapSlippagePercent": {
"message": "$1%", "message": "$1%",
"description": "$1 is the amount of % for slippage" "description": "$1 is the amount of % for slippage"
}, },
"swapSlippageTooLowDescription": {
"message": "Max slippage is too low which may cause your transaction to fail."
},
"swapSlippageTooLowTitle": {
"message": "Increase slippage to avoid transaction failure"
},
"swapSlippageTooltip": { "swapSlippageTooltip": {
"message": "If the price changes between the time your order is placed and confirmed its called “slippage”. Your swap will automatically cancel if slippage exceeds your “slippage tolerance” setting." "message": "If the price changes between the time your order is placed and confirmed its called “slippage”. Your swap will automatically cancel if slippage exceeds your “slippage tolerance” setting."
}, },
"swapSlippageVeryHighDescription": {
"message": "The slippage entered is considered very high and may result in a bad rate"
},
"swapSlippageVeryHighTitle": {
"message": "Very high slippage"
},
"swapSlippageZeroDescription": { "swapSlippageZeroDescription": {
"message": "There are fewer zero-slippage quote providers which will result in a less competitive quote." "message": "There are fewer zero-slippage quote providers which will result in a less competitive quote."
}, },
@ -5134,9 +5132,6 @@
"txInsightsNotSupported": { "txInsightsNotSupported": {
"message": "Transaction insights not supported for this contract at this time." "message": "Transaction insights not supported for this contract at this time."
}, },
"typePassword": {
"message": "Type your MetaMask password"
},
"typeYourSRP": { "typeYourSRP": {
"message": "Type your Secret Recovery Phrase" "message": "Type your Secret Recovery Phrase"
}, },
@ -5211,7 +5206,7 @@
"message": "Decode smart contracts" "message": "Decode smart contracts"
}, },
"use4ByteResolutionDescription": { "use4ByteResolutionDescription": {
"message": "To improve user experience, we customize the activity tab with messages based on the smart contracts you interact with. MetaMask uses a service called 4byte.directory to decode data and show you a version of a smart contact that's easier to read. This helps reduce your chances of approving malicious smart contract actions, but can result in your IP address being shared." "message": "To improve user experience, we customize the activity tab with messages based on the smart contracts you interact with. MetaMask uses a service called 4byte.directory to decode data and show you a version of a smart contract that's easier to read. This helps reduce your chances of approving malicious smart contract actions, but can result in your IP address being shared."
}, },
"useMultiAccountBalanceChecker": { "useMultiAccountBalanceChecker": {
"message": "Batch account balance requests" "message": "Batch account balance requests"
@ -5324,6 +5319,9 @@
"visitWebSite": { "visitWebSite": {
"message": "Visit our website" "message": "Visit our website"
}, },
"wallet": {
"message": "Wallet"
},
"walletConnectionGuide": { "walletConnectionGuide": {
"message": "our hardware wallet connection guide" "message": "our hardware wallet connection guide"
}, },
@ -5351,8 +5349,7 @@
"message": "Want to add this network?" "message": "Want to add this network?"
}, },
"wantsToAddThisAsset": { "wantsToAddThisAsset": {
"message": "$1 wants to add this asset to your wallet", "message": "This allows the following asset to be added to your wallet."
"description": "$1 is the name of the website that wants to add an asset to your wallet"
}, },
"warning": { "warning": {
"message": "Warning" "message": "Warning"

File diff suppressed because it is too large Load Diff

View File

@ -475,9 +475,6 @@
"copyAddress": { "copyAddress": {
"message": "Copiar dirección al Portapapeles" "message": "Copiar dirección al Portapapeles"
}, },
"copyPrivateKey": {
"message": "Esta es su clave privada (haga clic para copiarla)"
},
"copyRawTransactionData": { "copyRawTransactionData": {
"message": "Copiar los datos de las transacciones en bruto" "message": "Copiar los datos de las transacciones en bruto"
}, },
@ -767,9 +764,6 @@
"enterMaxSpendLimit": { "enterMaxSpendLimit": {
"message": "Escribir límite máximo de gastos" "message": "Escribir límite máximo de gastos"
}, },
"enterPassword": {
"message": "Escribir contraseña"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Escribir contraseña para continuar" "message": "Escribir contraseña para continuar"
}, },
@ -826,9 +820,6 @@
"experimental": { "experimental": {
"message": "Experimental" "message": "Experimental"
}, },
"exportPrivateKey": {
"message": "Exportar clave privada"
},
"externalExtension": { "externalExtension": {
"message": "Extensión externa" "message": "Extensión externa"
}, },
@ -1623,14 +1614,6 @@
"onboardingPinExtensionTitle": { "onboardingPinExtensionTitle": {
"message": "¡Su instalación de MetaMask ha finalizado!" "message": "¡Su instalación de MetaMask ha finalizado!"
}, },
"onboardingShowIncomingTransactionsDescription": {
"message": "Mostrar las transacciones entrantes en su cartera depende de la comunicación con $1. Etherscan tendrá acceso a su dirección de Ethereum y a su dirección IP. Ver $2.",
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
},
"onboardingUsePhishingDetectionDescription": {
"message": "Las alertas de detección de phishing se basan en la comunicación con $1. jsDeliver tendrá acceso a su dirección IP. Ver $2.",
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
},
"onlyAddTrustedNetworks": { "onlyAddTrustedNetworks": {
"message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza." "message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza."
}, },
@ -2020,9 +2003,6 @@
"showPermissions": { "showPermissions": {
"message": "Mostrar permisos" "message": "Mostrar permisos"
}, },
"showPrivateKeys": {
"message": "Mostrar claves privadas"
},
"showTestnetNetworks": { "showTestnetNetworks": {
"message": "Mostrar redes de prueba" "message": "Mostrar redes de prueba"
}, },
@ -2637,9 +2617,6 @@
"txInsightsNotSupported": { "txInsightsNotSupported": {
"message": "En este momento no se admiten informaciones sobre las transacciones para este contrato." "message": "En este momento no se admiten informaciones sobre las transacciones para este contrato."
}, },
"typePassword": {
"message": "Escriba su contraseña de MetaMask"
},
"u2f": { "u2f": {
"message": "U2F", "message": "U2F",
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopeeri aadress lõikelauale" "message": "Kopeeri aadress lõikelauale"
}, },
"copyPrivateKey": {
"message": "See on teie privaatne võti (klõpsake kopeerimiseks)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopeeri lõikelauale" "message": "Kopeeri lõikelauale"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Tõrge ENS-i nime registreerimisel" "message": "Tõrge ENS-i nime registreerimisel"
}, },
"enterPassword": {
"message": "Sisestage parool"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Jätkamiseks sisestage parool" "message": "Jätkamiseks sisestage parool"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Laienda vaadet" "message": "Laienda vaadet"
}, },
"exportPrivateKey": {
"message": "Ekspordi privaatvõti"
},
"failed": { "failed": {
"message": "Nurjus" "message": "Nurjus"
}, },
@ -653,9 +644,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Valige see, et kuvada saatmisekraanil hex-andmete väli" "message": "Valige see, et kuvada saatmisekraanil hex-andmete väli"
}, },
"showPrivateKeys": {
"message": "Kuva privaatvõtmed"
},
"sigRequest": { "sigRequest": {
"message": "Allkirja taotlus" "message": "Allkirja taotlus"
}, },
@ -776,9 +764,6 @@
"tryAgain": { "tryAgain": {
"message": "Proovi uuesti" "message": "Proovi uuesti"
}, },
"typePassword": {
"message": "Sisestage oma MetaMaski parool"
},
"unapproved": { "unapproved": {
"message": "Kinnitamata" "message": "Kinnitamata"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "کاپی آدرس به کلیپ بورد" "message": "کاپی آدرس به کلیپ بورد"
}, },
"copyPrivateKey": {
"message": "این کلید خصوصی شما است (برای کاپی نمودن کلیک کنید)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "کپی در بریده‌دان" "message": "کپی در بریده‌دان"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "خطا در ثبت نام ENS" "message": "خطا در ثبت نام ENS"
}, },
"enterPassword": {
"message": "رمز عبور را وارد کنید"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "برای ادامه رمز عبور را وارد کنید" "message": "برای ادامه رمز عبور را وارد کنید"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "توسعه ساحه دید" "message": "توسعه ساحه دید"
}, },
"exportPrivateKey": {
"message": "صدور کلید شخصی"
},
"failed": { "failed": {
"message": "ناموفق شد" "message": "ناموفق شد"
}, },
@ -663,9 +654,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "برای نمایش بخش اطلاعات hex در صفحه ارسال این را انتخاب نمایید" "message": "برای نمایش بخش اطلاعات hex در صفحه ارسال این را انتخاب نمایید"
}, },
"showPrivateKeys": {
"message": "نمایش کلید های شخصی"
},
"sigRequest": { "sigRequest": {
"message": "درخواست امضاء" "message": "درخواست امضاء"
}, },
@ -786,9 +774,6 @@
"tryAgain": { "tryAgain": {
"message": "امتحان مجدد" "message": "امتحان مجدد"
}, },
"typePassword": {
"message": "رمز عبور MetaMask تان را تایپ نمایید"
},
"unapproved": { "unapproved": {
"message": "تصدیق ناشده" "message": "تصدیق ناشده"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopioi osoite leikepöydälle" "message": "Kopioi osoite leikepöydälle"
}, },
"copyPrivateKey": {
"message": "Tämä on yksityinen avaimesi (kopioi napsauttamalla)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopioi leikepöydälle" "message": "Kopioi leikepöydälle"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Virhe ENS-nimen rekisteröinnissä" "message": "Virhe ENS-nimen rekisteröinnissä"
}, },
"enterPassword": {
"message": "Kirjoita salasana"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Syötä salasana voidaksesi jatkaa" "message": "Syötä salasana voidaksesi jatkaa"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Laajenna näkymää" "message": "Laajenna näkymää"
}, },
"exportPrivateKey": {
"message": "Vie yksityinen avain"
},
"failed": { "failed": {
"message": "Epäonnistui" "message": "Epäonnistui"
}, },
@ -660,9 +651,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Näytä hex-tietokenttä lähetysnäytössä valitsemalla tämän" "message": "Näytä hex-tietokenttä lähetysnäytössä valitsemalla tämän"
}, },
"showPrivateKeys": {
"message": "Näytä yksityiset avaimet"
},
"sigRequest": { "sigRequest": {
"message": "Allekirjoitus pyydetään" "message": "Allekirjoitus pyydetään"
}, },
@ -783,9 +771,6 @@
"tryAgain": { "tryAgain": {
"message": "Yritä uudelleen" "message": "Yritä uudelleen"
}, },
"typePassword": {
"message": "Kirjoita MetaMask-salasanasi"
},
"unapproved": { "unapproved": {
"message": "Ei hyväksytty" "message": "Ei hyväksytty"
}, },

View File

@ -154,9 +154,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopyahin ang address sa clipboard" "message": "Kopyahin ang address sa clipboard"
}, },
"copyPrivateKey": {
"message": "Ito ang iyong pribadong private key (i-click para kopyahin)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopyahin sa clipboard" "message": "Kopyahin sa clipboard"
}, },
@ -217,9 +214,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "May error sa pagrerehistro ng ENS name" "message": "May error sa pagrerehistro ng ENS name"
}, },
"enterPassword": {
"message": "Ilagay ang password"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Ilagay ang password para magpatuloy" "message": "Ilagay ang password para magpatuloy"
}, },
@ -229,9 +223,6 @@
"expandView": { "expandView": {
"message": "I-expand ang View" "message": "I-expand ang View"
}, },
"exportPrivateKey": {
"message": "I-export ang Private Key"
},
"failed": { "failed": {
"message": "Nabigo" "message": "Nabigo"
}, },
@ -587,9 +578,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Piliin ito para ipakita ang hex data field sa screen ng pagpapadala" "message": "Piliin ito para ipakita ang hex data field sa screen ng pagpapadala"
}, },
"showPrivateKeys": {
"message": "Ipakita ang mga Private Key"
},
"sign": { "sign": {
"message": "I-sign" "message": "I-sign"
}, },
@ -698,9 +686,6 @@
"tryAgain": { "tryAgain": {
"message": "Subukang muli" "message": "Subukang muli"
}, },
"typePassword": {
"message": "I-type ang iyong password sa MetaMask"
},
"unapproved": { "unapproved": {
"message": "Hindi inaprubahan" "message": "Hindi inaprubahan"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "העתק כתובת ללוח" "message": "העתק כתובת ללוח"
}, },
"copyPrivateKey": {
"message": "זה המפתח הפרטי שלך (נא להקיש כדי להעתיק)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "העתק ללוח" "message": "העתק ללוח"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "שגיאה ברישום שם ENS" "message": "שגיאה ברישום שם ENS"
}, },
"enterPassword": {
"message": "יש להזין ססמה"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "יש להזין ססמה כדי להמשיך" "message": "יש להזין ססמה כדי להמשיך"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "הרחב תצוגה" "message": "הרחב תצוגה"
}, },
"exportPrivateKey": {
"message": "יצא/י מפתח פרטי"
},
"failed": { "failed": {
"message": "נכשל" "message": "נכשל"
}, },
@ -660,9 +651,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "בחר/י בזה כדי להציג את שדה הנתונים ההקסדצימאלים על מסך השליחה" "message": "בחר/י בזה כדי להציג את שדה הנתונים ההקסדצימאלים על מסך השליחה"
}, },
"showPrivateKeys": {
"message": "הצג מפתחות פרטיים"
},
"sigRequest": { "sigRequest": {
"message": "בקשת חתימה" "message": "בקשת חתימה"
}, },
@ -783,9 +771,6 @@
"tryAgain": { "tryAgain": {
"message": "ניסיון חוזר" "message": "ניסיון חוזר"
}, },
"typePassword": {
"message": "נא להקליד את סיסמת MetaMask שלך"
},
"unapproved": { "unapproved": {
"message": "לא אושר" "message": "לא אושר"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -60,9 +60,6 @@
"copiedExclamation": { "copiedExclamation": {
"message": "कॉपी कर दिया गया!" "message": "कॉपी कर दिया गया!"
}, },
"copyPrivateKey": {
"message": "यह आपकी निजी कुंजी है (कॉपी करने के लिए क्लिक करें)।"
},
"copyToClipboard": { "copyToClipboard": {
"message": "क्लिपबोर्ड पर कॉपी करें" "message": "क्लिपबोर्ड पर कॉपी करें"
}, },
@ -87,15 +84,9 @@
"edit": { "edit": {
"message": "संपादित करें" "message": "संपादित करें"
}, },
"enterPassword": {
"message": "पासवर्ड दर्ज करें"
},
"etherscanView": { "etherscanView": {
"message": "ईथरस्कैन पर खाता देखें" "message": "ईथरस्कैन पर खाता देखें"
}, },
"exportPrivateKey": {
"message": "निजी कुंजी निर्यात करें"
},
"failed": { "failed": {
"message": "विफल" "message": "विफल"
}, },
@ -281,9 +272,6 @@
"settings": { "settings": {
"message": "सेटिंग्स" "message": "सेटिंग्स"
}, },
"showPrivateKeys": {
"message": "निजी कुंजी दिखाएँ"
},
"sigRequest": { "sigRequest": {
"message": "हस्ताक्षर अनुरोध" "message": "हस्ताक्षर अनुरोध"
}, },
@ -317,9 +305,6 @@
"total": { "total": {
"message": "कुल" "message": "कुल"
}, },
"typePassword": {
"message": "अपना पासवर्ड टाइप करें"
},
"unknown": { "unknown": {
"message": "अज्ञात नेटवर्क" "message": "अज्ञात नेटवर्क"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopiraj adresu u međuspremnik" "message": "Kopiraj adresu u međuspremnik"
}, },
"copyPrivateKey": {
"message": "Ovo je vaš privatni ključ (kliknite za kopiranje)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopiraj u međuspremnik" "message": "Kopiraj u međuspremnik"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Greška u registraciji naziva ENS" "message": "Greška u registraciji naziva ENS"
}, },
"enterPassword": {
"message": "Upiši lozinku"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Upišite lozinku za nastavak" "message": "Upišite lozinku za nastavak"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Proširi prikaz" "message": "Proširi prikaz"
}, },
"exportPrivateKey": {
"message": "Izvezi privatni ključ"
},
"failed": { "failed": {
"message": "Neuspješno" "message": "Neuspješno"
}, },
@ -656,9 +647,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Odaberite ovu stavku za prikaz polja namijenjenog za podatke hex na zaslonu za slanje" "message": "Odaberite ovu stavku za prikaz polja namijenjenog za podatke hex na zaslonu za slanje"
}, },
"showPrivateKeys": {
"message": "Prikaži privatne ključe"
},
"sigRequest": { "sigRequest": {
"message": "Zahtjev za potpisom" "message": "Zahtjev za potpisom"
}, },
@ -776,9 +764,6 @@
"tryAgain": { "tryAgain": {
"message": "Pokušaj ponovo" "message": "Pokušaj ponovo"
}, },
"typePassword": {
"message": "Upišite svoju lozinku MetaMask."
},
"unapproved": { "unapproved": {
"message": "Neodobreno" "message": "Neodobreno"
}, },

View File

@ -108,9 +108,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopi adrès clipboard" "message": "Kopi adrès clipboard"
}, },
"copyPrivateKey": {
"message": "Sa a se kle prive ou (klike pou ou kopye)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopi clipboard" "message": "Kopi clipboard"
}, },
@ -147,9 +144,6 @@
"edit": { "edit": {
"message": "Korije" "message": "Korije"
}, },
"enterPassword": {
"message": "Mete modpas"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Mete modpas pou kontinye" "message": "Mete modpas pou kontinye"
}, },
@ -159,9 +153,6 @@
"expandView": { "expandView": {
"message": "Elaji Wè" "message": "Elaji Wè"
}, },
"exportPrivateKey": {
"message": "Voye Kòd Prive"
},
"failed": { "failed": {
"message": "Tonbe" "message": "Tonbe"
}, },
@ -482,9 +473,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Pran sa pouw ka montre chan entèfas hex data a" "message": "Pran sa pouw ka montre chan entèfas hex data a"
}, },
"showPrivateKeys": {
"message": "Montre Kle Prive"
},
"sigRequest": { "sigRequest": {
"message": "Demann Siyati" "message": "Demann Siyati"
}, },
@ -554,9 +542,6 @@
"tryAgain": { "tryAgain": {
"message": "Eseye anko" "message": "Eseye anko"
}, },
"typePassword": {
"message": "Tape modpas ou"
},
"unapproved": { "unapproved": {
"message": "Pa apwouve" "message": "Pa apwouve"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Másolja a címet a vágólapra" "message": "Másolja a címet a vágólapra"
}, },
"copyPrivateKey": {
"message": "Ez a saját titkos kulcsod (kattints rá a másoláshoz)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Másolás a vágólapra" "message": "Másolás a vágólapra"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Hiba történt az ENS név regisztrációjakor" "message": "Hiba történt az ENS név regisztrációjakor"
}, },
"enterPassword": {
"message": "Adja meg a jelszót"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "A folytatáshoz adja meg a jelszót" "message": "A folytatáshoz adja meg a jelszót"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Nézet nagyítása" "message": "Nézet nagyítása"
}, },
"exportPrivateKey": {
"message": "Privát kulcs exportálása"
},
"failed": { "failed": {
"message": "Sikertelen" "message": "Sikertelen"
}, },
@ -656,9 +647,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Válassza ezt, ha a hex adatmezőt a küldő képernyőn szeretné megnézni" "message": "Válassza ezt, ha a hex adatmezőt a küldő képernyőn szeretné megnézni"
}, },
"showPrivateKeys": {
"message": "Mutassa a privát kulcsokat"
},
"sigRequest": { "sigRequest": {
"message": "Aláírás kérése" "message": "Aláírás kérése"
}, },
@ -776,9 +764,6 @@
"tryAgain": { "tryAgain": {
"message": "Újra" "message": "Újra"
}, },
"typePassword": {
"message": "Írd be MetaMask jelszavadat"
},
"unapproved": { "unapproved": {
"message": "Jóvá nem hagyott" "message": "Jóvá nem hagyott"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -579,9 +579,6 @@
"copyAddress": { "copyAddress": {
"message": "Copia l'indirizzo" "message": "Copia l'indirizzo"
}, },
"copyPrivateKey": {
"message": "Questa è la tua chiave privata (clicca per copiare)"
},
"copyRawTransactionData": { "copyRawTransactionData": {
"message": "Copia i dati grezzi della transazione" "message": "Copia i dati grezzi della transazione"
}, },
@ -840,9 +837,6 @@
"enterMaxSpendLimit": { "enterMaxSpendLimit": {
"message": "Inserisici Limite Spesa" "message": "Inserisici Limite Spesa"
}, },
"enterPassword": {
"message": "Inserisci password"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Inserisci la tua password per continuare" "message": "Inserisci la tua password per continuare"
}, },
@ -875,9 +869,6 @@
"expandView": { "expandView": {
"message": "Espandi Vista" "message": "Espandi Vista"
}, },
"exportPrivateKey": {
"message": "Esporta Chiave Privata"
},
"externalExtension": { "externalExtension": {
"message": "Estensione Esterna" "message": "Estensione Esterna"
}, },
@ -1439,9 +1430,6 @@
"showPermissions": { "showPermissions": {
"message": "Mostra permessi" "message": "Mostra permessi"
}, },
"showPrivateKeys": {
"message": "Mostra Chiave Privata"
},
"sigRequest": { "sigRequest": {
"message": "Firma Richiesta" "message": "Firma Richiesta"
}, },
@ -1794,9 +1782,6 @@
"tryAgain": { "tryAgain": {
"message": "Prova di nuovo" "message": "Prova di nuovo"
}, },
"typePassword": {
"message": "Inserisci Password"
},
"unapproved": { "unapproved": {
"message": "Non approvata" "message": "Non approvata"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "ವಿಳಾಸವನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಿ" "message": "ವಿಳಾಸವನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಿ"
}, },
"copyPrivateKey": {
"message": "ಇದು ನಿಮ್ಮ ಖಾಸಗಿ ಕೀ ಆಗಿದೆ (ನಕಲಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಿ" "message": "ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಿ"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "ENS ಹೆಸರಿನ ನೋಂದಣಿಯಲ್ಲಿ ದೋಷ" "message": "ENS ಹೆಸರಿನ ನೋಂದಣಿಯಲ್ಲಿ ದೋಷ"
}, },
"enterPassword": {
"message": "ಪಾಸ್‌ವರ್ಡ್‌ ಅನ್ನು ನಮೂದಿಸಿ"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "ಮುಂದುವರೆಯಲು ಪಾಸ್‌ವರ್ಡ್ ನಮೂದಿಸಿ" "message": "ಮುಂದುವರೆಯಲು ಪಾಸ್‌ವರ್ಡ್ ನಮೂದಿಸಿ"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "ವಿಸ್ತರಿಸಿದ ವೀಕ್ಷಣೆ" "message": "ವಿಸ್ತರಿಸಿದ ವೀಕ್ಷಣೆ"
}, },
"exportPrivateKey": {
"message": "ಖಾಸಗಿ ಕೀಲಿಯನ್ನು ರಫ್ತು ಮಾಡಿ"
},
"failed": { "failed": {
"message": "ವಿಫಲವಾಗಿದೆ" "message": "ವಿಫಲವಾಗಿದೆ"
}, },
@ -663,9 +654,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "ಕಳುಹಿಸುವ ಪರದೆಯಲ್ಲಿ ಹೆಕ್ಸ್ ಡೇಟಾ ಕ್ಷೇತ್ರವನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ" "message": "ಕಳುಹಿಸುವ ಪರದೆಯಲ್ಲಿ ಹೆಕ್ಸ್ ಡೇಟಾ ಕ್ಷೇತ್ರವನ್ನು ತೋರಿಸಲು ಇದನ್ನು ಆಯ್ಕೆಮಾಡಿ"
}, },
"showPrivateKeys": {
"message": "ಖಾಸಗಿ ಕೀಗಳನ್ನು ತೋರಿಸಿ"
},
"sigRequest": { "sigRequest": {
"message": "ಸಹಿಯ ವಿನಂತಿ" "message": "ಸಹಿಯ ವಿನಂತಿ"
}, },
@ -786,9 +774,6 @@
"tryAgain": { "tryAgain": {
"message": "ಪುನಃ ಪ್ರಯತ್ನಿಸಿ" "message": "ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"
}, },
"typePassword": {
"message": "ನಿಮ್ಮ MetaMask ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಟೈಪ್ ಮಾಡಿ"
},
"unapproved": { "unapproved": {
"message": "ಅನುಮೋದಿಸದಿರುವುದು" "message": "ಅನುಮೋದಿಸದಿರುವುದು"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopijuoti adresą į iškarpinę" "message": "Kopijuoti adresą į iškarpinę"
}, },
"copyPrivateKey": {
"message": "Tai yra jūsų asmeninis raktas (spustelėkite, kad nukopijuotumėte)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopijuoti į iškarpinę" "message": "Kopijuoti į iškarpinę"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "ENS pavadinimo registracijos klaida" "message": "ENS pavadinimo registracijos klaida"
}, },
"enterPassword": {
"message": "Įveskite slaptažodį"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Norėdami tęsti, įveskite slaptažodį" "message": "Norėdami tęsti, įveskite slaptažodį"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Išskleisti rodinį" "message": "Išskleisti rodinį"
}, },
"exportPrivateKey": {
"message": "Eksportuoti asmeninį raktą"
},
"failed": { "failed": {
"message": "Nepavyko" "message": "Nepavyko"
}, },
@ -663,9 +654,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Pasirinkite tai, kad siuntimo ekrane būtų rodomas šešioliktainių duomenų laukas" "message": "Pasirinkite tai, kad siuntimo ekrane būtų rodomas šešioliktainių duomenų laukas"
}, },
"showPrivateKeys": {
"message": "Rodyti asmeninius raktus"
},
"sigRequest": { "sigRequest": {
"message": "Parašo užklausa" "message": "Parašo užklausa"
}, },
@ -786,9 +774,6 @@
"tryAgain": { "tryAgain": {
"message": "Bandyti dar kartą" "message": "Bandyti dar kartą"
}, },
"typePassword": {
"message": "Įveskite savo „MetaMask“ slaptažodį"
},
"unapproved": { "unapproved": {
"message": "Nepatvirtinta" "message": "Nepatvirtinta"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Iekopēt adresi starpliktuvē" "message": "Iekopēt adresi starpliktuvē"
}, },
"copyPrivateKey": {
"message": "Šī ir jūsu privātā atslēga (noklikšķiniet, lai nokopētu)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopēt starpliktuvē" "message": "Kopēt starpliktuvē"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Kļūda ENS vārda reģistrācijā" "message": "Kļūda ENS vārda reģistrācijā"
}, },
"enterPassword": {
"message": "Ievadiet paroli"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Ievadiet paroli, lai turpinātu" "message": "Ievadiet paroli, lai turpinātu"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Izvērst skatījumu" "message": "Izvērst skatījumu"
}, },
"exportPrivateKey": {
"message": "Eksportēt privāto atslēgu"
},
"failed": { "failed": {
"message": "Neizdevās" "message": "Neizdevās"
}, },
@ -659,9 +650,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Atlasiet šo, lai atvērtu hex datus sūtīšanas ekrānā" "message": "Atlasiet šo, lai atvērtu hex datus sūtīšanas ekrānā"
}, },
"showPrivateKeys": {
"message": "Rādīt privātās atslēgas"
},
"sigRequest": { "sigRequest": {
"message": "Paraksta pieprasījums" "message": "Paraksta pieprasījums"
}, },
@ -782,9 +770,6 @@
"tryAgain": { "tryAgain": {
"message": "Mēģināt vēlreiz" "message": "Mēģināt vēlreiz"
}, },
"typePassword": {
"message": "Ievadiet savu MetaMask paroli"
},
"unapproved": { "unapproved": {
"message": "Nav apstiprināts" "message": "Nav apstiprināts"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Salin alamat kepada papan klip" "message": "Salin alamat kepada papan klip"
}, },
"copyPrivateKey": {
"message": "Ini kunci persendirian anda (klik untuk menyalin)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Salin ke papan keratan" "message": "Salin ke papan keratan"
}, },
@ -238,9 +235,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Ralat dalam pendaftaran nama ENS" "message": "Ralat dalam pendaftaran nama ENS"
}, },
"enterPassword": {
"message": "Masukkan kata laluan"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Masukkan kata laluan untuk teruskan" "message": "Masukkan kata laluan untuk teruskan"
}, },
@ -253,9 +247,6 @@
"expandView": { "expandView": {
"message": "Kembangkan Paparan" "message": "Kembangkan Paparan"
}, },
"exportPrivateKey": {
"message": "Eksport Kekunci Persendirian"
},
"failed": { "failed": {
"message": "Gagal" "message": "Gagal"
}, },
@ -643,9 +634,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Pilih ini untuk menunjukkan medan data hex pada skrin hantar" "message": "Pilih ini untuk menunjukkan medan data hex pada skrin hantar"
}, },
"showPrivateKeys": {
"message": "Tunjukkan Kunci Persendirian"
},
"sigRequest": { "sigRequest": {
"message": "Permintaan Tandatangan" "message": "Permintaan Tandatangan"
}, },
@ -763,9 +751,6 @@
"tryAgain": { "tryAgain": {
"message": "Cuba lagi" "message": "Cuba lagi"
}, },
"typePassword": {
"message": "Taip kata laluan MetaMask anda"
},
"unapproved": { "unapproved": {
"message": "Belum Diluluskan" "message": "Belum Diluluskan"
}, },

View File

@ -60,9 +60,6 @@
"copiedExclamation": { "copiedExclamation": {
"message": "Gekopieerd!" "message": "Gekopieerd!"
}, },
"copyPrivateKey": {
"message": "Dit is uw privésleutel (klik om te kopiëren)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopieer naar klembord" "message": "Kopieer naar klembord"
}, },
@ -84,15 +81,9 @@
"edit": { "edit": {
"message": "Bewerk" "message": "Bewerk"
}, },
"enterPassword": {
"message": "Voer wachtwoord in"
},
"etherscanView": { "etherscanView": {
"message": "Bekijk account op Etherscan" "message": "Bekijk account op Etherscan"
}, },
"exportPrivateKey": {
"message": "Exporteer privésleutel"
},
"failed": { "failed": {
"message": "mislukt" "message": "mislukt"
}, },
@ -271,9 +262,6 @@
"settings": { "settings": {
"message": "instellingen" "message": "instellingen"
}, },
"showPrivateKeys": {
"message": "Privésleutels weergeven"
},
"sigRequest": { "sigRequest": {
"message": "Ondertekeningsverzoek" "message": "Ondertekeningsverzoek"
}, },
@ -307,9 +295,6 @@
"total": { "total": {
"message": "Totaal" "message": "Totaal"
}, },
"typePassword": {
"message": "Typ uw wachtwoord"
},
"unknown": { "unknown": {
"message": "Onbekend" "message": "Onbekend"
}, },

View File

@ -172,9 +172,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopier adresse til utklippstavlen " "message": "Kopier adresse til utklippstavlen "
}, },
"copyPrivateKey": {
"message": "Dette er din private nøkkel (klikk for å kopiere)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopiér til utklippstavlen" "message": "Kopiér til utklippstavlen"
}, },
@ -238,9 +235,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Feil i ENS-navneregistrering" "message": "Feil i ENS-navneregistrering"
}, },
"enterPassword": {
"message": "Skriv inn passord "
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Skriv inn passord for å fortsette" "message": "Skriv inn passord for å fortsette"
}, },
@ -253,9 +247,6 @@
"expandView": { "expandView": {
"message": "Utvid visning" "message": "Utvid visning"
}, },
"exportPrivateKey": {
"message": "Eksporter privat nøkkel"
},
"failed": { "failed": {
"message": "Mislyktes" "message": "Mislyktes"
}, },
@ -644,9 +635,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Velg dette for å vise hex-datafeltet på sendskjermen" "message": "Velg dette for å vise hex-datafeltet på sendskjermen"
}, },
"showPrivateKeys": {
"message": "Vis private nøkler"
},
"sigRequest": { "sigRequest": {
"message": "Signaturforespørsel " "message": "Signaturforespørsel "
}, },
@ -761,9 +749,6 @@
"tryAgain": { "tryAgain": {
"message": "Prøv igjen" "message": "Prøv igjen"
}, },
"typePassword": {
"message": "Skriv inn MetaMask-passordet"
},
"unapproved": { "unapproved": {
"message": "Ikke godkjent " "message": "Ikke godkjent "
}, },

View File

@ -338,9 +338,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopyahin ang address sa clipboard" "message": "Kopyahin ang address sa clipboard"
}, },
"copyPrivateKey": {
"message": "Ito ang iyong pribadong key (i-click para kopyahin)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopyahin sa clipboard" "message": "Kopyahin sa clipboard"
}, },
@ -489,9 +486,6 @@
"enterMaxSpendLimit": { "enterMaxSpendLimit": {
"message": "Ilagay ang Max na Limitasyon sa Paggastos" "message": "Ilagay ang Max na Limitasyon sa Paggastos"
}, },
"enterPassword": {
"message": "Ilagay ang password"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Ilagay ang password para magpatuloy" "message": "Ilagay ang password para magpatuloy"
}, },
@ -542,9 +536,6 @@
"expandView": { "expandView": {
"message": "I-expand ang view" "message": "I-expand ang view"
}, },
"exportPrivateKey": {
"message": "I-export ang Pribadong Key"
},
"externalExtension": { "externalExtension": {
"message": "External Extension" "message": "External Extension"
}, },
@ -1309,9 +1300,6 @@
"showPermissions": { "showPermissions": {
"message": "Ipakita ang mga pahintulot" "message": "Ipakita ang mga pahintulot"
}, },
"showPrivateKeys": {
"message": "Ipakita ang Mga Private Key"
},
"sigRequest": { "sigRequest": {
"message": "Request ng Signature" "message": "Request ng Signature"
}, },
@ -1764,9 +1752,6 @@
"tryAgain": { "tryAgain": {
"message": "Subukan ulit" "message": "Subukan ulit"
}, },
"typePassword": {
"message": "Uri ng password ng iyong MetaMask"
},
"unapproved": { "unapproved": {
"message": "Hindi inaprubahan" "message": "Hindi inaprubahan"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Skopiuj adres do schowka" "message": "Skopiuj adres do schowka"
}, },
"copyPrivateKey": {
"message": "To jest Twój prywatny klucz (kliknij żeby skopiować)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Skopiuj do schowka" "message": "Skopiuj do schowka"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Błąd w rejestracji nazwy ENS" "message": "Błąd w rejestracji nazwy ENS"
}, },
"enterPassword": {
"message": "Wpisz hasło"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Podaj hasło żeby kontynuować" "message": "Podaj hasło żeby kontynuować"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Rozwiń widok" "message": "Rozwiń widok"
}, },
"exportPrivateKey": {
"message": "Eksportuj klucz prywatny"
},
"failed": { "failed": {
"message": "Nie udało się" "message": "Nie udało się"
}, },
@ -657,9 +648,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania" "message": "Wybierz to żeby pokazać pole danych hex na ekranie wysyłania"
}, },
"showPrivateKeys": {
"message": "Pokaż prywatne klucze"
},
"sigRequest": { "sigRequest": {
"message": "Prośba o podpis" "message": "Prośba o podpis"
}, },
@ -774,9 +762,6 @@
"tryAgain": { "tryAgain": {
"message": "Spróbuj ponownie" "message": "Spróbuj ponownie"
}, },
"typePassword": {
"message": "Wpisz hasło"
},
"unapproved": { "unapproved": {
"message": "Niezatwierdzone" "message": "Niezatwierdzone"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -475,9 +475,6 @@
"copyAddress": { "copyAddress": {
"message": "Copiar endereço para a área de transferência" "message": "Copiar endereço para a área de transferência"
}, },
"copyPrivateKey": {
"message": "Essa é a sua chave privada (clique para copiar)"
},
"copyRawTransactionData": { "copyRawTransactionData": {
"message": "Copiar dados brutos da transação" "message": "Copiar dados brutos da transação"
}, },
@ -767,9 +764,6 @@
"enterMaxSpendLimit": { "enterMaxSpendLimit": {
"message": "Digite um limite máximo de gastos" "message": "Digite um limite máximo de gastos"
}, },
"enterPassword": {
"message": "Digite a senha"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Digite a senha para continuar" "message": "Digite a senha para continuar"
}, },
@ -826,9 +820,6 @@
"experimental": { "experimental": {
"message": "Experimental" "message": "Experimental"
}, },
"exportPrivateKey": {
"message": "Exportar chave privada"
},
"externalExtension": { "externalExtension": {
"message": "Extensão externa" "message": "Extensão externa"
}, },
@ -1623,10 +1614,6 @@
"onboardingPinExtensionTitle": { "onboardingPinExtensionTitle": {
"message": "Sua instalação da MetaMask está concluída!" "message": "Sua instalação da MetaMask está concluída!"
}, },
"onboardingShowIncomingTransactionsDescription": {
"message": "A exibição de transações recebidas na sua carteira depende de comunicação com $1. O Etherscan terá acesso ao seu endereço Ethereum e ao seu endereço IP. Veja $2.",
"description": "$1 is a clickable link with text defined by the 'etherscan' key. $2 is a clickable link with text defined by the 'privacyMsg' key."
},
"onboardingUsePhishingDetectionDescription": { "onboardingUsePhishingDetectionDescription": {
"message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.", "message": "Os alertas de detecção de phishing dependem de comunicação com $1. O jsDeliver terá acesso ao seu endereço IP. Veja $2.",
"description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link" "description": "The $1 is the word 'jsDeliver', from key 'jsDeliver' and $2 is the words Privacy Policy from key 'privacyMsg', both separated here so that it can be wrapped as a link"
@ -2020,9 +2007,6 @@
"showPermissions": { "showPermissions": {
"message": "Mostrar permissões" "message": "Mostrar permissões"
}, },
"showPrivateKeys": {
"message": "Mostrar chaves privadas"
},
"showTestnetNetworks": { "showTestnetNetworks": {
"message": "Mostrar redes de teste" "message": "Mostrar redes de teste"
}, },
@ -2637,9 +2621,6 @@
"txInsightsNotSupported": { "txInsightsNotSupported": {
"message": "As informações sobre transações não são suportadas para esse contrato, por ora." "message": "As informações sobre transações não são suportadas para esse contrato, por ora."
}, },
"typePassword": {
"message": "Digite sua senha da MetaMask"
},
"u2f": { "u2f": {
"message": "U2F", "message": "U2F",
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices." "description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Copiere adresă în clipboard" "message": "Copiere adresă în clipboard"
}, },
"copyPrivateKey": {
"message": "Aceasta este cheia dumneavoastră privată (clic pentru a copia)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copiați în clipboard" "message": "Copiați în clipboard"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Eroare la înregistrarea numelui ENS" "message": "Eroare la înregistrarea numelui ENS"
}, },
"enterPassword": {
"message": "Introduceți parola"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Introduceți parola pentru a continua" "message": "Introduceți parola pentru a continua"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Extindeți vizualizarea" "message": "Extindeți vizualizarea"
}, },
"exportPrivateKey": {
"message": "Exportați cheia privată"
},
"failed": { "failed": {
"message": "Eșuat" "message": "Eșuat"
}, },
@ -650,9 +641,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Selectați această opțiune pentru a arăta câmpul de date hexazecimale în ecranul de trimitere." "message": "Selectați această opțiune pentru a arăta câmpul de date hexazecimale în ecranul de trimitere."
}, },
"showPrivateKeys": {
"message": "Afișați cheile private"
},
"sigRequest": { "sigRequest": {
"message": "Solicitare de semnătură" "message": "Solicitare de semnătură"
}, },
@ -767,9 +755,6 @@
"tryAgain": { "tryAgain": {
"message": "Încearcă din nou" "message": "Încearcă din nou"
}, },
"typePassword": {
"message": "Scrieți parola dvs. pentru MetaMask"
},
"unapproved": { "unapproved": {
"message": "Neaprobat" "message": "Neaprobat"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -169,9 +169,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopírovať adresu do schránky" "message": "Kopírovať adresu do schránky"
}, },
"copyPrivateKey": {
"message": "Toto je váš privátní klíč (kliknutím zkopírujte)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopírovat do schránky" "message": "Kopírovat do schránky"
}, },
@ -235,9 +232,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Chyba pri registrácii názvu ENS" "message": "Chyba pri registrácii názvu ENS"
}, },
"enterPassword": {
"message": "Zadejte heslo"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Pokračujte zadaním hesla" "message": "Pokračujte zadaním hesla"
}, },
@ -250,9 +244,6 @@
"expandView": { "expandView": {
"message": "Rozbaliť zobrazenie" "message": "Rozbaliť zobrazenie"
}, },
"exportPrivateKey": {
"message": "Exportovat privátní klíč"
},
"failed": { "failed": {
"message": "Neúspěšné" "message": "Neúspěšné"
}, },
@ -635,9 +626,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Vyberte toto, ak chcete, aby sa na obrazovke odosielania zobrazilo hex dátové pole" "message": "Vyberte toto, ak chcete, aby sa na obrazovke odosielania zobrazilo hex dátové pole"
}, },
"showPrivateKeys": {
"message": "Zobrazit privátní klíče"
},
"sigRequest": { "sigRequest": {
"message": "Požadavek podpisu" "message": "Požadavek podpisu"
}, },
@ -752,9 +740,6 @@
"tryAgain": { "tryAgain": {
"message": "Skúsiť znova" "message": "Skúsiť znova"
}, },
"typePassword": {
"message": "Zadejte své heslo"
},
"unapproved": { "unapproved": {
"message": "Neschváleno" "message": "Neschváleno"
}, },

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopiraj naslov v odložišče" "message": "Kopiraj naslov v odložišče"
}, },
"copyPrivateKey": {
"message": "To je vaš zesebni ključ (kliknite za kopiranje)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopiraj v odložišče" "message": "Kopiraj v odložišče"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Napaka pri registraciji imena ENS" "message": "Napaka pri registraciji imena ENS"
}, },
"enterPassword": {
"message": "Vnesite geslo"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Za nadaljevanje vnesite geslo" "message": "Za nadaljevanje vnesite geslo"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Razširi pogled" "message": "Razširi pogled"
}, },
"exportPrivateKey": {
"message": "Izvozi zasebni ključ"
},
"failed": { "failed": {
"message": "Ni uspelo" "message": "Ni uspelo"
}, },
@ -651,9 +642,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Izberite za prikaz hex podatkov na zaslonu za pošiljanje" "message": "Izberite za prikaz hex podatkov na zaslonu za pošiljanje"
}, },
"showPrivateKeys": {
"message": "Pokaži zasebni ključ"
},
"sigRequest": { "sigRequest": {
"message": "Zahteva za podpis" "message": "Zahteva za podpis"
}, },
@ -774,9 +762,6 @@
"tryAgain": { "tryAgain": {
"message": "Poskusi znova" "message": "Poskusi znova"
}, },
"typePassword": {
"message": "Vnesite vaše MetaMask geslo"
},
"unapproved": { "unapproved": {
"message": "Neodobreno" "message": "Neodobreno"
}, },

View File

@ -172,9 +172,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopirajte adresu u ostavu" "message": "Kopirajte adresu u ostavu"
}, },
"copyPrivateKey": {
"message": "Ovo je vaš privatni ključ (kliknite kako biste ga kopirali)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Копирај у меморију" "message": "Копирај у меморију"
}, },
@ -238,9 +235,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Greška u registraciji ENS imena." "message": "Greška u registraciji ENS imena."
}, },
"enterPassword": {
"message": "Unesite lozinku"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Unesite lozinku kako biste nastavili" "message": "Unesite lozinku kako biste nastavili"
}, },
@ -253,9 +247,6 @@
"expandView": { "expandView": {
"message": "Proširite prikaz" "message": "Proširite prikaz"
}, },
"exportPrivateKey": {
"message": "Izvezite privatni ključ"
},
"failed": { "failed": {
"message": "Neuspešno" "message": "Neuspešno"
}, },
@ -654,9 +645,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Izaberite ovo da bi se pokazalo polje sa hex podacima na „Pošalji” ekranu " "message": "Izaberite ovo da bi se pokazalo polje sa hex podacima na „Pošalji” ekranu "
}, },
"showPrivateKeys": {
"message": "Prikažite privatne ključeve"
},
"sigRequest": { "sigRequest": {
"message": "Zahtev za potpisom" "message": "Zahtev za potpisom"
}, },
@ -774,9 +762,6 @@
"tryAgain": { "tryAgain": {
"message": "Пробај поново" "message": "Пробај поново"
}, },
"typePassword": {
"message": "Ukucajte svoju MetaMask šifru"
},
"unapproved": { "unapproved": {
"message": "Neodobren" "message": "Neodobren"
}, },

View File

@ -169,9 +169,6 @@
"copyAddress": { "copyAddress": {
"message": "Kopiera adress till urklipp" "message": "Kopiera adress till urklipp"
}, },
"copyPrivateKey": {
"message": "Det här är din privata nyckel (klicka för att kopiera)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Kopiera till Urklipp" "message": "Kopiera till Urklipp"
}, },
@ -235,9 +232,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Fel i ENS-namnregistrering" "message": "Fel i ENS-namnregistrering"
}, },
"enterPassword": {
"message": "Ange lösenord"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Ange lösenord för att fortsätta" "message": "Ange lösenord för att fortsätta"
}, },
@ -250,9 +244,6 @@
"expandView": { "expandView": {
"message": "Expandera vy" "message": "Expandera vy"
}, },
"exportPrivateKey": {
"message": "Exportera privat nyckel"
},
"failed": { "failed": {
"message": "Misslyckades" "message": "Misslyckades"
}, },
@ -647,9 +638,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Välj detta för att visa hex-datafältet på sändarskärmen" "message": "Välj detta för att visa hex-datafältet på sändarskärmen"
}, },
"showPrivateKeys": {
"message": "Visa privata nycklar"
},
"sigRequest": { "sigRequest": {
"message": "Signaturförfrågan" "message": "Signaturförfrågan"
}, },
@ -761,9 +749,6 @@
"tryAgain": { "tryAgain": {
"message": "Försök igen" "message": "Försök igen"
}, },
"typePassword": {
"message": "Ange ditt MetaMask-lösenord"
},
"unapproved": { "unapproved": {
"message": "Inte godkänd" "message": "Inte godkänd"
}, },

View File

@ -169,9 +169,6 @@
"copyAddress": { "copyAddress": {
"message": "Nakili anwani kwenye ubao wa kunakilia" "message": "Nakili anwani kwenye ubao wa kunakilia"
}, },
"copyPrivateKey": {
"message": "Huu ni ufunguo wako wa kibinafsi (bofya ili unakili)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Nakili kwenye ubao wa kunakili" "message": "Nakili kwenye ubao wa kunakili"
}, },
@ -235,9 +232,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Hitilafu imetokea kwenye usajili wa jina la ENS" "message": "Hitilafu imetokea kwenye usajili wa jina la ENS"
}, },
"enterPassword": {
"message": "Ingiza nenosiri"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Ingiza nenosiri ili uendelee" "message": "Ingiza nenosiri ili uendelee"
}, },
@ -250,9 +244,6 @@
"expandView": { "expandView": {
"message": "Panua Mwonekano" "message": "Panua Mwonekano"
}, },
"exportPrivateKey": {
"message": "Panua Mwonekano"
},
"failed": { "failed": {
"message": "Imeshindwa" "message": "Imeshindwa"
}, },
@ -641,9 +632,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Chagua hii ili uonyeshe sehemu ya data ya hex kwenye skrini ya tuma" "message": "Chagua hii ili uonyeshe sehemu ya data ya hex kwenye skrini ya tuma"
}, },
"showPrivateKeys": {
"message": "Onyesha Fungo Binafsi"
},
"sigRequest": { "sigRequest": {
"message": "Ombi la Saini" "message": "Ombi la Saini"
}, },
@ -764,9 +752,6 @@
"tryAgain": { "tryAgain": {
"message": "Jaribu tena" "message": "Jaribu tena"
}, },
"typePassword": {
"message": "Andika nenosiri lako la MetaMask"
},
"unapproved": { "unapproved": {
"message": "Haijaidhinishwa" "message": "Haijaidhinishwa"
}, },

View File

@ -87,9 +87,6 @@
"copiedExclamation": { "copiedExclamation": {
"message": "நகலெடுக்கப்பட்டன!" "message": "நகலெடுக்கப்பட்டன!"
}, },
"copyPrivateKey": {
"message": "இது உங்கள் தனிப்பட்ட விசை (நகலெடுக்க கிளிக் செய்யவும்)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "கிளிப்போர்டுக்கு நகலெடு" "message": "கிளிப்போர்டுக்கு நகலெடு"
}, },
@ -126,15 +123,9 @@
"edit": { "edit": {
"message": "திருத்து" "message": "திருத்து"
}, },
"enterPassword": {
"message": "கடவுச்சொல்லை உள்ளிடவும்"
},
"etherscanView": { "etherscanView": {
"message": "Etherscan கணக்கைப் பார்க்கவும்" "message": "Etherscan கணக்கைப் பார்க்கவும்"
}, },
"exportPrivateKey": {
"message": "தனியார் விசை ஐ ஏற்றுமதி செய்க"
},
"failed": { "failed": {
"message": "தோல்வி" "message": "தோல்வி"
}, },
@ -374,9 +365,6 @@
"settings": { "settings": {
"message": "அமைப்புகள்" "message": "அமைப்புகள்"
}, },
"showPrivateKeys": {
"message": "தனிப்பட்ட விசைகளைக் காண்பி"
},
"sigRequest": { "sigRequest": {
"message": "கையொப்பம் கோரிக்கை" "message": "கையொப்பம் கோரிக்கை"
}, },
@ -425,9 +413,6 @@
"tryAgain": { "tryAgain": {
"message": "மீண்டும் முயல்க" "message": "மீண்டும் முயல்க"
}, },
"typePassword": {
"message": "உங்கள் கடவுச்சொல்லை தட்டச்சு செய்யவும்"
},
"unapproved": { "unapproved": {
"message": "அங்கீகரிக்கப்படாத" "message": "அங்கீகரிக்கப்படாத"
}, },

View File

@ -78,9 +78,6 @@
"copiedExclamation": { "copiedExclamation": {
"message": "คัดลอกแล้ว!" "message": "คัดลอกแล้ว!"
}, },
"copyPrivateKey": {
"message": "นี่คือคีย์ส่วนตัวของคุณ(กดเพื่อคัดลอก)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "คัดลอกไปคลิปบอร์ด" "message": "คัดลอกไปคลิปบอร์ด"
}, },
@ -117,18 +114,12 @@
"editContact": { "editContact": {
"message": "แก้ไขผู้ติดต่อ" "message": "แก้ไขผู้ติดต่อ"
}, },
"enterPassword": {
"message": "ใส่รหัสผ่าน"
},
"etherscanView": { "etherscanView": {
"message": "ดูบัญชีบน Etherscan" "message": "ดูบัญชีบน Etherscan"
}, },
"expandView": { "expandView": {
"message": "ขยายมุมมอง" "message": "ขยายมุมมอง"
}, },
"exportPrivateKey": {
"message": "ส่งออกคีย์ส่วนตัว"
},
"failed": { "failed": {
"message": "ล้มเหลว" "message": "ล้มเหลว"
}, },
@ -338,9 +329,6 @@
"settings": { "settings": {
"message": "การตั้งค่า" "message": "การตั้งค่า"
}, },
"showPrivateKeys": {
"message": "แสดงคีย์ส่วนตัว"
},
"sigRequest": { "sigRequest": {
"message": "ขอลายเซ็น" "message": "ขอลายเซ็น"
}, },
@ -392,9 +380,6 @@
"transactionDropped": { "transactionDropped": {
"message": "ธุรกรรมถูกยกเลิกเมื่อ $2" "message": "ธุรกรรมถูกยกเลิกเมื่อ $2"
}, },
"typePassword": {
"message": "พิมพ์รหัสผ่านของคุณ"
},
"unknown": { "unknown": {
"message": "ไม่รู้จัก" "message": "ไม่รู้จัก"
}, },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -175,9 +175,6 @@
"copyAddress": { "copyAddress": {
"message": "Копіювати адресу в буфер обміну" "message": "Копіювати адресу в буфер обміну"
}, },
"copyPrivateKey": {
"message": "Це ваш закритий ключ (натисніть, щоб скопіювати)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Копіювати в буфер" "message": "Копіювати в буфер"
}, },
@ -241,9 +238,6 @@
"ensRegistrationError": { "ensRegistrationError": {
"message": "Помилка у реєстрації ENS ім'я" "message": "Помилка у реєстрації ENS ім'я"
}, },
"enterPassword": {
"message": "Введіть пароль"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "Введіть пароль, щоб продовжити" "message": "Введіть пароль, щоб продовжити"
}, },
@ -256,9 +250,6 @@
"expandView": { "expandView": {
"message": "Розгорнути подання" "message": "Розгорнути подання"
}, },
"exportPrivateKey": {
"message": "Експортувати приватний ключ"
},
"failed": { "failed": {
"message": "Помилка" "message": "Помилка"
}, },
@ -663,9 +654,6 @@
"showHexDataDescription": { "showHexDataDescription": {
"message": "Оберіть це, щоб показати поле для шістнадцятирикових даних на екрані надсилання" "message": "Оберіть це, щоб показати поле для шістнадцятирикових даних на екрані надсилання"
}, },
"showPrivateKeys": {
"message": "Показати приватні ключі"
},
"sigRequest": { "sigRequest": {
"message": "Запит підпису" "message": "Запит підпису"
}, },
@ -786,9 +774,6 @@
"tryAgain": { "tryAgain": {
"message": "Повторити" "message": "Повторити"
}, },
"typePassword": {
"message": "Введіть ваш пароль MetaMask"
},
"unapproved": { "unapproved": {
"message": "Не затверджено" "message": "Не затверджено"
}, },

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -334,9 +334,6 @@
"copyAddress": { "copyAddress": {
"message": "複製到剪貼簿" "message": "複製到剪貼簿"
}, },
"copyPrivateKey": {
"message": "這是您的私鑰(點擊複製)"
},
"copyToClipboard": { "copyToClipboard": {
"message": "複製到剪貼簿" "message": "複製到剪貼簿"
}, },
@ -497,9 +494,6 @@
"enterMaxSpendLimit": { "enterMaxSpendLimit": {
"message": "輸入最大花費限制" "message": "輸入最大花費限制"
}, },
"enterPassword": {
"message": "請輸入密碼"
},
"enterPasswordContinue": { "enterPasswordContinue": {
"message": "請輸入密碼" "message": "請輸入密碼"
}, },
@ -547,9 +541,6 @@
"expandView": { "expandView": {
"message": "展開畫面" "message": "展開畫面"
}, },
"exportPrivateKey": {
"message": "匯出私鑰"
},
"externalExtension": { "externalExtension": {
"message": "外部擴充功能" "message": "外部擴充功能"
}, },
@ -1231,9 +1222,6 @@
"showPermissions": { "showPermissions": {
"message": "顯示權限" "message": "顯示權限"
}, },
"showPrivateKeys": {
"message": "顯示私鑰"
},
"sigRequest": { "sigRequest": {
"message": "請求簽署" "message": "請求簽署"
}, },
@ -1440,9 +1428,6 @@
"tryAgain": { "tryAgain": {
"message": "再試一次" "message": "再試一次"
}, },
"typePassword": {
"message": "請輸入密碼"
},
"unapproved": { "unapproved": {
"message": "未批准" "message": "未批准"
}, },

View File

@ -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',
);
});
});
});

View File

@ -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;

View File

@ -2,9 +2,11 @@
* @file The entry point for the web extension singleton process. * @file The entry point for the web extension singleton process.
*/ */
// This import sets up a global function required for Sentry to function. // Disabled to allow setting up initial state hooks first
// This import sets up global functions required for Sentry to function.
// It must be run first in case an error is thrown later during initialization. // It must be run first in case an error is thrown later during initialization.
import './lib/setup-persisted-state-hook'; import './lib/setup-initial-state-hooks';
import EventEmitter from 'events'; import EventEmitter from 'events';
import endOfStream from 'end-of-stream'; import endOfStream from 'end-of-stream';
@ -13,6 +15,7 @@ import debounce from 'debounce-stream';
import log from 'loglevel'; import log from 'loglevel';
import browser from 'webextension-polyfill'; import browser from 'webextension-polyfill';
import { storeAsStream } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store';
import { isObject } from '@metamask/utils';
///: BEGIN:ONLY_INCLUDE_IN(snaps) ///: BEGIN:ONLY_INCLUDE_IN(snaps)
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
@ -41,7 +44,7 @@ import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension'; import ExtensionPlatform from './platforms/extension';
import LocalStore from './lib/local-store'; import LocalStore from './lib/local-store';
import ReadOnlyNetworkStore from './lib/network-store'; import ReadOnlyNetworkStore from './lib/network-store';
import { SENTRY_STATE } from './lib/setupSentry'; import { SENTRY_BACKGROUND_STATE } from './lib/setupSentry';
import createStreamSink from './lib/createStreamSink'; import createStreamSink from './lib/createStreamSink';
import NotificationManager, { import NotificationManager, {
@ -68,6 +71,12 @@ import DesktopManager from '@metamask/desktop/dist/desktop-manager';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
/* eslint-enable import/order */ /* eslint-enable import/order */
// Setup global hook for improved Sentry state snapshots during initialization
const inTest = process.env.IN_TEST;
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
global.stateHooks.getMostRecentPersistedState = () =>
localStore.mostRecentRetrievedState;
const { sentry } = global; const { sentry } = global;
const firstTimeState = { ...rawFirstTimeState }; const firstTimeState = { ...rawFirstTimeState };
@ -79,7 +88,7 @@ const metamaskInternalProcessHash = {
const metamaskBlockedPorts = ['trezor-connect']; const metamaskBlockedPorts = ['trezor-connect'];
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info'); log.setLevel(process.env.METAMASK_DEBUG ? 'debug' : 'info', false);
const platform = new ExtensionPlatform(); const platform = new ExtensionPlatform();
const notificationManager = new NotificationManager(); const notificationManager = new NotificationManager();
@ -90,10 +99,6 @@ let uiIsTriggering = false;
const openMetamaskTabsIDs = {}; const openMetamaskTabsIDs = {};
const requestAccountTabIds = {}; const requestAccountTabIds = {};
let controller; let controller;
// state persistence
const inTest = process.env.IN_TEST;
const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
let versionedData; let versionedData;
if (inTest || process.env.METAMASK_DEBUG) { if (inTest || process.env.METAMASK_DEBUG) {
@ -264,7 +269,8 @@ browser.runtime.onConnectExternal.addListener(async (...args) => {
*/ */
async function initialize() { async function initialize() {
try { try {
const initState = await loadStateFromPersistence(); const initData = await loadStateFromPersistence();
const initState = initData.data;
const initLangCode = await getFirstPreferredLangCode(); const initLangCode = await getFirstPreferredLangCode();
///: BEGIN:ONLY_INCLUDE_IN(desktop) ///: BEGIN:ONLY_INCLUDE_IN(desktop)
@ -287,6 +293,7 @@ async function initialize() {
initLangCode, initLangCode,
{}, {},
isFirstMetaMaskControllerSetup, isFirstMetaMaskControllerSetup,
initData.meta,
); );
if (!isManifestV3) { if (!isManifestV3) {
await loadPhishingWarningPage(); await loadPhishingWarningPage();
@ -409,6 +416,19 @@ export async function loadStateFromPersistence() {
versionedData = await migrator.migrateData(versionedData); versionedData = await migrator.migrateData(versionedData);
if (!versionedData) { if (!versionedData) {
throw new Error('MetaMask - migrator returned undefined'); throw new Error('MetaMask - migrator returned undefined');
} else if (!isObject(versionedData.meta)) {
throw new Error(
`MetaMask - migrator metadata has invalid type '${typeof versionedData.meta}'`,
);
} else if (typeof versionedData.meta.version !== 'number') {
throw new Error(
`MetaMask - migrator metadata version has invalid type '${typeof versionedData
.meta.version}'`,
);
} else if (!isObject(versionedData.data)) {
throw new Error(
`MetaMask - migrator data has invalid type '${typeof versionedData.data}'`,
);
} }
// this initializes the meta/version data as a class variable to be used for future writes // this initializes the meta/version data as a class variable to be used for future writes
localStore.setMetadata(versionedData.meta); localStore.setMetadata(versionedData.meta);
@ -417,7 +437,7 @@ export async function loadStateFromPersistence() {
localStore.set(versionedData.data); localStore.set(versionedData.data);
// return just the data // return just the data
return versionedData.data; return versionedData;
} }
/** /**
@ -430,12 +450,14 @@ export async function loadStateFromPersistence() {
* @param {string} initLangCode - The region code for the language preferred by the current user. * @param {string} initLangCode - The region code for the language preferred by the current user.
* @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app) * @param {object} overrides - object with callbacks that are allowed to override the setup controller logic (usefull for desktop app)
* @param isFirstMetaMaskControllerSetup * @param isFirstMetaMaskControllerSetup
* @param {object} stateMetadata - Metadata about the initial state and migrations, including the most recent migration version
*/ */
export function setupController( export function setupController(
initState, initState,
initLangCode, initLangCode,
overrides, overrides,
isFirstMetaMaskControllerSetup, isFirstMetaMaskControllerSetup,
stateMetadata,
) { ) {
// //
// MetaMask Controller // MetaMask Controller
@ -462,6 +484,7 @@ export function setupController(
localStore, localStore,
overrides, overrides,
isFirstMetaMaskControllerSetup, isFirstMetaMaskControllerSetup,
currentMigrationVersion: stateMetadata.version,
}); });
setupEnsIpfsResolver({ setupEnsIpfsResolver({
@ -880,14 +903,9 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
}); });
function setupSentryGetStateGlobal(store) { function setupSentryGetStateGlobal(store) {
global.stateHooks.getSentryState = function () { global.stateHooks.getSentryAppState = function () {
const fullState = store.getState(); const backgroundState = store.memStore.getState();
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE); return maskObject(backgroundState, SENTRY_BACKGROUND_STATE);
return {
browser: window.navigator.userAgent,
store: debugState,
version: platform.getVersion(),
};
}; };
} }

View 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,
});
});
});
});

View 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,
});
}
}
}

View File

@ -225,6 +225,7 @@ describe('DetectTokensController', function () {
tokenListController, tokenListController,
onInfuraIsBlocked: sinon.stub(), onInfuraIsBlocked: sinon.stub(),
onInfuraIsUnblocked: sinon.stub(), onInfuraIsUnblocked: sinon.stub(),
networkConfigurations: {},
}); });
preferences.setAddresses([ preferences.setAddresses([
'0x7e57e2', '0x7e57e2',

View File

@ -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

View File

@ -292,9 +292,9 @@ export default class MMIController extends EventEmitter {
})), })),
); );
newAccounts.forEach( for (let i = 0; i < newAccounts.length; i++) {
async () => await this.keyringController.addNewAccount(keyring), await this.keyringController.addNewAccount(keyring);
); }
const allAccounts = await this.keyringController.getAccounts(); const allAccounts = await this.keyringController.getAccounts();
@ -303,13 +303,34 @@ export default class MMIController extends EventEmitter {
...new Set(oldAccounts.concat(allAccounts.map((a) => a.toLowerCase()))), ...new Set(oldAccounts.concat(allAccounts.map((a) => a.toLowerCase()))),
]; ];
// Create a Set of lowercased addresses from oldAccounts for efficient existence checks
const oldAccountsSet = new Set(
oldAccounts.map((address) => address.toLowerCase()),
);
// Create a map of lowercased addresses to names from newAccounts for efficient lookups
const accountNameMap = newAccounts.reduce((acc, item) => {
// For each account in newAccounts, add an entry to the map with the lowercased address as the key and the name as the value
acc[item.toLowerCase()] = accounts[item].name;
return acc;
}, {});
// Iterate over all accounts
allAccounts.forEach((address) => { allAccounts.forEach((address) => {
if (!oldAccounts.includes(address.toLowerCase())) { // Convert the address to lowercase for consistent comparisons
const label = newAccounts const lowercasedAddress = address.toLowerCase();
.filter((item) => item.toLowerCase() === address)
.map((item) => accounts[item].name)[0]; // If the address is not in oldAccounts
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.preferencesController.setAccountLabel(address, label);
} }
}
}); });
this.accountTracker.syncWithAddresses(accountsToTrack); this.accountTracker.syncWithAddresses(accountsToTrack);
@ -569,7 +590,7 @@ export default class MMIController extends EventEmitter {
const mmiDashboardData = await this.handleMmiDashboardData(); const mmiDashboardData = await this.handleMmiDashboardData();
const cookieSetUrls = const cookieSetUrls =
this.mmiConfigurationController.store.mmiConfiguration?.portfolio this.mmiConfigurationController.store.mmiConfiguration?.portfolio
?.cookieSetUrls; ?.cookieSetUrls || [];
setDashboardCookie(mmiDashboardData, cookieSetUrls); setDashboardCookie(mmiDashboardData, cookieSetUrls);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -578,7 +599,12 @@ export default class MMIController extends EventEmitter {
} }
async newUnsignedMessage(msgParams, req, version) { async newUnsignedMessage(msgParams, req, version) {
const updatedMsgParams = { ...msgParams, deferSetAsSigned: true }; // The code path triggered by deferSetAsSigned: true is for custodial accounts
const accountDetails = this.custodyController.getAccountDetails(
msgParams.from,
);
const isCustodial = Boolean(accountDetails);
const updatedMsgParams = { ...msgParams, deferSetAsSigned: isCustodial };
if (req.method.includes('eth_signTypedData')) { if (req.method.includes('eth_signTypedData')) {
return await this.signatureController.newUnsignedTypedMessage( return await this.signatureController.newUnsignedTypedMessage(

View File

@ -53,6 +53,7 @@ describe('MMIController', function () {
onInfuraIsBlocked: jest.fn(), onInfuraIsBlocked: jest.fn(),
onInfuraIsUnblocked: jest.fn(), onInfuraIsUnblocked: jest.fn(),
provider: {}, provider: {},
networkConfigurations: {},
}), }),
appStateController: new AppStateController({ appStateController: new AppStateController({
addUnlockListener: jest.fn(), addUnlockListener: jest.fn(),

View File

@ -1,6 +1,9 @@
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import { normalize as normalizeAddress } from 'eth-sig-util'; import { normalize as normalizeAddress } from 'eth-sig-util';
import { IPFS_DEFAULT_GATEWAY_URL } from '../../../shared/constants/network'; import {
CHAIN_IDS,
IPFS_DEFAULT_GATEWAY_URL,
} from '../../../shared/constants/network';
import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets'; import { LedgerTransportTypes } from '../../../shared/constants/hardware-wallets';
import { ThemeType } from '../../../shared/constants/preferences'; import { ThemeType } from '../../../shared/constants/preferences';
import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils'; import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils';
@ -8,6 +11,17 @@ import { shouldShowLineaMainnet } from '../../../shared/modules/network.utils';
import { KEYRING_SNAPS_REGISTRY_URL } from '../../../shared/constants/app'; import { KEYRING_SNAPS_REGISTRY_URL } from '../../../shared/constants/app';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
const mainNetworks = {
[CHAIN_IDS.MAINNET]: true,
[CHAIN_IDS.LINEA_MAINNET]: true,
};
const testNetworks = {
[CHAIN_IDS.GOERLI]: true,
[CHAIN_IDS.SEPOLIA]: true,
[CHAIN_IDS.LINEA_GOERLI]: true,
};
export default class PreferencesController { export default class PreferencesController {
/** /**
* *
@ -25,6 +39,13 @@ export default class PreferencesController {
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app * @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
*/ */
constructor(opts = {}) { constructor(opts = {}) {
const addedNonMainNetwork = Object.values(
opts.networkConfigurations,
).reduce((acc, element) => {
acc[element.chainId] = true;
return acc;
}, {});
const initState = { const initState = {
useBlockie: false, useBlockie: false,
useNonceField: false, useNonceField: false,
@ -51,8 +72,11 @@ export default class PreferencesController {
// Feature flag toggling is available in the global namespace // Feature flag toggling is available in the global namespace
// for convenient testing of pre-release features, and should never // for convenient testing of pre-release features, and should never
// perform sensitive operations. // perform sensitive operations.
featureFlags: { featureFlags: {},
showIncomingTransactions: true, incomingTransactionsPreferences: {
...mainNetworks,
...addedNonMainNetwork,
...testNetworks,
}, },
knownMethodData: {}, knownMethodData: {},
currentLocale: opts.initLangCode, currentLocale: opts.initLangCode,
@ -84,6 +108,7 @@ export default class PreferencesController {
}; };
this.network = opts.network; this.network = opts.network;
this._onInfuraIsBlocked = opts.onInfuraIsBlocked; this._onInfuraIsBlocked = opts.onInfuraIsBlocked;
this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked; this._onInfuraIsUnblocked = opts.onInfuraIsUnblocked;
this.store = new ObservableStore(initState); this.store = new ObservableStore(initState);
@ -448,7 +473,7 @@ export default class PreferencesController {
* found in the settings page. * found in the settings page.
* *
* @param {string} preference - The preference to enable or disable. * @param {string} preference - The preference to enable or disable.
* @param {boolean} value - Indicates whether or not the preference should be enabled or disabled. * @param {boolean |object} value - Indicates whether or not the preference should be enabled or disabled.
* @returns {Promise<object>} Promises a new object; the updated preferences object. * @returns {Promise<object>} Promises a new object; the updated preferences object.
*/ */
async setPreference(preference, value) { async setPreference(preference, value) {
@ -550,6 +575,18 @@ export default class PreferencesController {
}); });
} }
/**
* A setter for the incomingTransactions in preference to be updated
*
* @param {string} chainId - chainId of the network
* @param {bool} value - preference of certain network, true to be enabled
*/
setIncomingTransactionsPreferences(chainId, value) {
const previousValue = this.store.getState().incomingTransactionsPreferences;
const updatedValue = { ...previousValue, [chainId]: value };
this.store.updateState({ incomingTransactionsPreferences: updatedValue });
}
getRpcMethodPreferences() { getRpcMethodPreferences() {
return this.store.getState().disabledRpcMethodPreferences; return this.store.getState().disabledRpcMethodPreferences;
} }
@ -570,6 +607,7 @@ export default class PreferencesController {
} }
this.store.updateState({ snapRegistryList: snapRegistry }); this.store.updateState({ snapRegistryList: snapRegistry });
} }
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
// //

View File

@ -1,67 +1,85 @@
import { strict as assert } from 'assert'; /**
import sinon from 'sinon'; * @jest-environment node
*/
import { ControllerMessenger } from '@metamask/base-controller'; import { ControllerMessenger } from '@metamask/base-controller';
import { TokenListController } from '@metamask/assets-controllers'; import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences'; import PreferencesController from './preferences';
describe('preferences controller', function () { const NETWORK_CONFIGURATION_DATA = {
'test-networkConfigurationId-1': {
rpcUrl: 'https://testrpc.com',
chainId: CHAIN_IDS.GOERLI,
nickname: '0X5',
rpcPrefs: { blockExplorerUrl: 'https://etherscan.io' },
},
'test-networkConfigurationId-2': {
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
ticker: 'ETH',
nickname: 'Localhost 8545',
rpcPrefs: {},
},
};
describe('preferences controller', () => {
let preferencesController; let preferencesController;
let tokenListController; let tokenListController;
beforeEach(function () { beforeEach(() => {
const tokenListMessenger = new ControllerMessenger().getRestricted({ const tokenListMessenger = new ControllerMessenger().getRestricted({
name: 'TokenListController', name: 'TokenListController',
}); });
tokenListController = new TokenListController({ tokenListController = new TokenListController({
chainId: '1', chainId: '1',
preventPollingOnNetworkRestart: false, preventPollingOnNetworkRestart: false,
onNetworkStateChange: sinon.spy(), onNetworkStateChange: jest.fn(),
onPreferencesStateChange: sinon.spy(), onPreferencesStateChange: jest.fn(),
messenger: tokenListMessenger, messenger: tokenListMessenger,
}); });
preferencesController = new PreferencesController({ preferencesController = new PreferencesController({
initLangCode: 'en_US', initLangCode: 'en_US',
tokenListController, tokenListController,
onInfuraIsBlocked: sinon.spy(), onInfuraIsBlocked: jest.fn(),
onInfuraIsUnblocked: sinon.spy(), onInfuraIsUnblocked: jest.fn(),
networkConfigurations: NETWORK_CONFIGURATION_DATA,
}); });
}); });
afterEach(function () { describe('useBlockie', () => {
sinon.restore(); it('defaults useBlockie to false', () => {
expect(preferencesController.store.getState().useBlockie).toStrictEqual(
false,
);
}); });
describe('useBlockie', function () { it('setUseBlockie to true', () => {
it('defaults useBlockie to false', function () {
assert.equal(preferencesController.store.getState().useBlockie, false);
});
it('setUseBlockie to true', function () {
preferencesController.setUseBlockie(true); preferencesController.setUseBlockie(true);
assert.equal(preferencesController.store.getState().useBlockie, true); expect(preferencesController.store.getState().useBlockie).toStrictEqual(
true,
);
}); });
}); });
describe('setCurrentLocale', function () { describe('setCurrentLocale', () => {
it('checks the default currentLocale', function () { it('checks the default currentLocale', () => {
const { currentLocale } = preferencesController.store.getState(); const { currentLocale } = preferencesController.store.getState();
assert.equal(currentLocale, 'en_US'); expect(currentLocale).toStrictEqual('en_US');
}); });
it('sets current locale in preferences controller', function () { it('sets current locale in preferences controller', () => {
preferencesController.setCurrentLocale('ja'); preferencesController.setCurrentLocale('ja');
const { currentLocale } = preferencesController.store.getState(); const { currentLocale } = preferencesController.store.getState();
assert.equal(currentLocale, 'ja'); expect(currentLocale).toStrictEqual('ja');
}); });
}); });
describe('setAddresses', function () { describe('setAddresses', () => {
it('should keep a map of addresses to names and addresses in the store', function () { it('should keep a map of addresses to names and addresses in the store', () => {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
const { identities } = preferencesController.store.getState(); const { identities } = preferencesController.store.getState();
assert.deepEqual(identities, { expect(identities).toStrictEqual({
'0xda22le': { '0xda22le': {
name: 'Account 1', name: 'Account 1',
address: '0xda22le', address: '0xda22le',
@ -73,12 +91,12 @@ describe('preferences controller', function () {
}); });
}); });
it('should replace its list of addresses', function () { it('should replace its list of addresses', () => {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); preferencesController.setAddresses(['0xda22le77', '0x7e57e277']);
const { identities } = preferencesController.store.getState(); const { identities } = preferencesController.store.getState();
assert.deepEqual(identities, { expect(identities).toStrictEqual({
'0xda22le77': { '0xda22le77': {
name: 'Account 1', name: 'Account 1',
address: '0xda22le77', address: '0xda22le77',
@ -91,237 +109,235 @@ describe('preferences controller', function () {
}); });
}); });
describe('removeAddress', function () { describe('removeAddress', () => {
it('should remove an address from state', function () { it('should remove an address from state', () => {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.removeAddress('0xda22le'); preferencesController.removeAddress('0xda22le');
assert.equal( expect(
preferencesController.store.getState().identities['0xda22le'], preferencesController.store.getState().identities['0xda22le'],
undefined, ).toStrictEqual(undefined);
);
}); });
it('should switch accounts if the selected address is removed', function () { it('should switch accounts if the selected address is removed', () => {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
preferencesController.setSelectedAddress('0x7e57e2'); preferencesController.setSelectedAddress('0x7e57e2');
preferencesController.removeAddress('0x7e57e2'); preferencesController.removeAddress('0x7e57e2');
expect(preferencesController.getSelectedAddress()).toStrictEqual(
assert.equal(preferencesController.getSelectedAddress(), '0xda22le'); '0xda22le',
);
}); });
}); });
describe('setAccountLabel', function () { describe('setAccountLabel', () => {
it('should update a label for the given account', function () { it('should update a label for the given account', () => {
preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le', '0x7e57e2']);
assert.deepEqual( expect(
preferencesController.store.getState().identities['0xda22le'], preferencesController.store.getState().identities['0xda22le'],
{ ).toStrictEqual({
name: 'Account 1', name: 'Account 1',
address: '0xda22le', address: '0xda22le',
}, });
);
preferencesController.setAccountLabel('0xda22le', 'Dazzle'); preferencesController.setAccountLabel('0xda22le', 'Dazzle');
assert.deepEqual( expect(
preferencesController.store.getState().identities['0xda22le'], preferencesController.store.getState().identities['0xda22le'],
{ ).toStrictEqual({
name: 'Dazzle', name: 'Dazzle',
address: '0xda22le', address: '0xda22le',
}, });
);
}); });
}); });
describe('setPasswordForgotten', function () { describe('setPasswordForgotten', () => {
it('should default to false', function () { it('should default to false', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.forgottenPassword, false);
});
it('should set the forgottenPassword property in state', function () {
assert.equal(
preferencesController.store.getState().forgottenPassword, preferencesController.store.getState().forgottenPassword,
false, ).toStrictEqual(false);
); });
it('should set the forgottenPassword property in state', () => {
preferencesController.setPasswordForgotten(true); preferencesController.setPasswordForgotten(true);
expect(
assert.equal(
preferencesController.store.getState().forgottenPassword, preferencesController.store.getState().forgottenPassword,
true, ).toStrictEqual(true);
);
}); });
}); });
describe('setUsePhishDetect', function () { describe('setUsePhishDetect', () => {
it('should default to true', function () { it('should default to true', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.usePhishDetect, true);
});
it('should set the usePhishDetect property in state', function () {
assert.equal(preferencesController.store.getState().usePhishDetect, true);
preferencesController.setUsePhishDetect(false);
assert.equal(
preferencesController.store.getState().usePhishDetect, preferencesController.store.getState().usePhishDetect,
false, ).toStrictEqual(true);
); });
it('should set the usePhishDetect property in state', () => {
preferencesController.setUsePhishDetect(false);
expect(
preferencesController.store.getState().usePhishDetect,
).toStrictEqual(false);
}); });
}); });
describe('setUseMultiAccountBalanceChecker', function () { describe('setUseMultiAccountBalanceChecker', () => {
it('should default to true', function () { it('should default to true', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.useMultiAccountBalanceChecker, true);
});
it('should set the setUseMultiAccountBalanceChecker property in state', function () {
assert.equal(
preferencesController.store.getState().useMultiAccountBalanceChecker, preferencesController.store.getState().useMultiAccountBalanceChecker,
true, ).toStrictEqual(true);
); });
it('should set the setUseMultiAccountBalanceChecker property in state', () => {
preferencesController.setUseMultiAccountBalanceChecker(false); preferencesController.setUseMultiAccountBalanceChecker(false);
expect(
assert.equal(
preferencesController.store.getState().useMultiAccountBalanceChecker, preferencesController.store.getState().useMultiAccountBalanceChecker,
false, ).toStrictEqual(false);
);
}); });
}); });
describe('setUseTokenDetection', function () { describe('setUseTokenDetection', () => {
it('should default to false', function () { it('should default to false', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.useTokenDetection, false);
});
it('should set the useTokenDetection property in state', function () {
assert.equal(
preferencesController.store.getState().useTokenDetection, preferencesController.store.getState().useTokenDetection,
false, ).toStrictEqual(false);
); });
it('should set the useTokenDetection property in state', () => {
preferencesController.setUseTokenDetection(true); preferencesController.setUseTokenDetection(true);
assert.equal( expect(
preferencesController.store.getState().useTokenDetection, preferencesController.store.getState().useTokenDetection,
true, ).toStrictEqual(true);
);
}); });
}); });
describe('setUseNftDetection', function () { describe('setUseNftDetection', () => {
it('should default to false', function () { it('should default to false', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.useNftDetection, false);
});
it('should set the useNftDetection property in state', function () {
assert.equal(
preferencesController.store.getState().useNftDetection, preferencesController.store.getState().useNftDetection,
false, ).toStrictEqual(false);
); });
it('should set the useNftDetection property in state', () => {
preferencesController.setOpenSeaEnabled(true); preferencesController.setOpenSeaEnabled(true);
preferencesController.setUseNftDetection(true); preferencesController.setUseNftDetection(true);
assert.equal( expect(
preferencesController.store.getState().useNftDetection, preferencesController.store.getState().useNftDetection,
true, ).toStrictEqual(true);
);
}); });
}); });
describe('setUse4ByteResolution', function () { describe('setUse4ByteResolution', function () {
it('should default to true', function () { it('should default to true', function () {
const state = preferencesController.store.getState(); expect(
assert.equal(state.use4ByteResolution, true); preferencesController.store.getState().use4ByteResolution,
).toStrictEqual(true);
}); });
it('should set the use4ByteResolution property in state', function () { it('should set the use4ByteResolution property in state', function () {
assert.equal(
preferencesController.store.getState().use4ByteResolution,
true,
);
preferencesController.setUse4ByteResolution(false); preferencesController.setUse4ByteResolution(false);
assert.equal( expect(
preferencesController.store.getState().use4ByteResolution, preferencesController.store.getState().use4ByteResolution,
false, ).toStrictEqual(false);
);
}); });
}); });
describe('setOpenSeaEnabled', function () { describe('setOpenSeaEnabled', () => {
it('should default to false', function () { it('should default to false', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.openSeaEnabled, false);
});
it('should set the openSeaEnabled property in state', function () {
assert.equal(
preferencesController.store.getState().openSeaEnabled, preferencesController.store.getState().openSeaEnabled,
false, ).toStrictEqual(false);
); });
it('should set the openSeaEnabled property in state', () => {
preferencesController.setOpenSeaEnabled(true); preferencesController.setOpenSeaEnabled(true);
assert.equal(preferencesController.store.getState().openSeaEnabled, true); expect(
preferencesController.store.getState().openSeaEnabled,
).toStrictEqual(true);
}); });
}); });
describe('setAdvancedGasFee', function () { describe('setAdvancedGasFee', () => {
it('should default to null', function () { it('should default to null', () => {
const state = preferencesController.store.getState(); expect(
assert.equal(state.advancedGasFee, null); preferencesController.store.getState().advancedGasFee,
).toStrictEqual(null);
}); });
it('should set the setAdvancedGasFee property in state', function () { it('should set the setAdvancedGasFee property in state', () => {
const state = preferencesController.store.getState();
assert.equal(state.advancedGasFee, null);
preferencesController.setAdvancedGasFee({ preferencesController.setAdvancedGasFee({
maxBaseFee: '1.5', maxBaseFee: '1.5',
priorityFee: '2', priorityFee: '2',
}); });
assert.equal( expect(
preferencesController.store.getState().advancedGasFee.maxBaseFee, preferencesController.store.getState().advancedGasFee.maxBaseFee,
'1.5', ).toStrictEqual('1.5');
); expect(
assert.equal(
preferencesController.store.getState().advancedGasFee.priorityFee, preferencesController.store.getState().advancedGasFee.priorityFee,
'2', ).toStrictEqual('2');
);
}); });
}); });
describe('setTheme', function () { describe('setTheme', () => {
it('should default to value "OS"', function () { it('should default to value "OS"', () => {
const state = preferencesController.store.getState(); expect(preferencesController.store.getState().theme).toStrictEqual('os');
assert.equal(state.theme, 'os');
}); });
it('should set the setTheme property in state', function () { it('should set the setTheme property in state', () => {
const state = preferencesController.store.getState();
assert.equal(state.theme, 'os');
preferencesController.setTheme('dark'); preferencesController.setTheme('dark');
assert.equal(preferencesController.store.getState().theme, 'dark'); expect(preferencesController.store.getState().theme).toStrictEqual(
}); 'dark',
});
describe('setUseCurrencyRateCheck', function () {
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,
); );
preferencesController.setUseCurrencyRateCheck(false); });
assert.equal( });
describe('setUseCurrencyRateCheck', () => {
it('should default to false', () => {
expect(
preferencesController.store.getState().useCurrencyRateCheck, 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, 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,
});
}); });
}); });
}); });

View File

@ -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([]);
});
});
});

View File

@ -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;
}
}

View File

@ -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: [],
});
});
});
});

View File

@ -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;
}
}

View 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`,
);
});
});
});

View 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;
}

View File

@ -72,6 +72,8 @@ import TransactionStateManager from './tx-state-manager';
import TxGasUtil from './tx-gas-utils'; import TxGasUtil from './tx-gas-utils';
import PendingTransactionTracker from './pending-tx-tracker'; import PendingTransactionTracker from './pending-tx-tracker';
import * as txUtils from './lib/util'; import * as txUtils from './lib/util';
import { IncomingTransactionHelper } from './IncomingTransactionHelper';
import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource';
const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory
const UPDATE_POST_TX_BALANCE_TIMEOUT = 5000; const UPDATE_POST_TX_BALANCE_TIMEOUT = 5000;
@ -127,6 +129,7 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
* @param {object} opts.initState - initial transaction list default is an empty array * @param {object} opts.initState - initial transaction list default is an empty array
* @param {Function} opts.getNetworkId - Get the current network ID. * @param {Function} opts.getNetworkId - Get the current network ID.
* @param {Function} opts.getNetworkStatus - Get the current network status. * @param {Function} opts.getNetworkStatus - Get the current network status.
* @param {Function} opts.getNetworkState - Get the network state.
* @param {Function} opts.onNetworkStateChange - Subscribe to network state change events. * @param {Function} opts.onNetworkStateChange - Subscribe to network state change events.
* @param {object} opts.blockTracker - An instance of eth-blocktracker * @param {object} opts.blockTracker - An instance of eth-blocktracker
* @param {object} opts.provider - A network provider. * @param {object} opts.provider - A network provider.
@ -134,6 +137,7 @@ const METRICS_STATUS_FAILED = 'failed on-chain';
* @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for * @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for
* @param {Function} opts.signTransaction - ethTx signer that returns a rawTx * @param {Function} opts.signTransaction - ethTx signer that returns a rawTx
* @param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state * @param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
* @param {Function} opts.hasCompletedOnboarding - Returns whether or not the user has completed the onboarding flow
* @param {object} opts.preferencesStore * @param {object} opts.preferencesStore
*/ */
@ -142,6 +146,7 @@ export default class TransactionController extends EventEmitter {
super(); super();
this.getNetworkId = opts.getNetworkId; this.getNetworkId = opts.getNetworkId;
this.getNetworkStatus = opts.getNetworkStatus; this.getNetworkStatus = opts.getNetworkStatus;
this._getNetworkState = opts.getNetworkState;
this._getCurrentChainId = opts.getCurrentChainId; this._getCurrentChainId = opts.getCurrentChainId;
this.getProviderConfig = opts.getProviderConfig; this.getProviderConfig = opts.getProviderConfig;
this._getCurrentNetworkEIP1559Compatibility = this._getCurrentNetworkEIP1559Compatibility =
@ -166,6 +171,7 @@ export default class TransactionController extends EventEmitter {
this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails; this.getTokenStandardAndDetails = opts.getTokenStandardAndDetails;
this.securityProviderRequest = opts.securityProviderRequest; this.securityProviderRequest = opts.securityProviderRequest;
this.messagingSystem = opts.messenger; this.messagingSystem = opts.messenger;
this._hasCompletedOnboarding = opts.hasCompletedOnboarding;
this.memStore = new ObservableStore({}); this.memStore = new ObservableStore({});
@ -216,6 +222,33 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
}); });
this.incomingTransactionHelper = new IncomingTransactionHelper({
blockTracker: this.blockTracker,
getCurrentAccount: () => this.getSelectedAddress(),
getNetworkState: () => this._getNetworkState(),
isEnabled: () =>
Boolean(
this.preferencesStore.getState().incomingTransactionsPreferences?.[
this._getChainId()
] && this._hasCompletedOnboarding(),
),
lastFetchedBlockNumbers: opts.initState?.lastFetchedBlockNumbers || {},
remoteTransactionSource: new EtherscanRemoteTransactionSource({
includeTokenTransfers: false,
}),
updateTransactions: false,
});
this.incomingTransactionHelper.hub.on(
'transactions',
this._onIncomingTransactions.bind(this),
);
this.incomingTransactionHelper.hub.on(
'updatedLastFetchedBlockNumbers',
this._onUpdatedLastFetchedBlockNumbers.bind(this),
);
this.txStateManager.store.subscribe(() => this.txStateManager.store.subscribe(() =>
this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE), this.emit(METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE),
); );
@ -759,6 +792,18 @@ export default class TransactionController extends EventEmitter {
); );
} }
startIncomingTransactionPolling() {
this.incomingTransactionHelper.start();
}
stopIncomingTransactionPolling() {
this.incomingTransactionHelper.stop();
}
async updateIncomingTransactions() {
await this.incomingTransactionHelper.update();
}
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
@ -1779,7 +1824,11 @@ export default class TransactionController extends EventEmitter {
// MMI does not broadcast transactions, as that is the responsibility of the custodian // MMI does not broadcast transactions, as that is the responsibility of the custodian
if (txMeta.custodyStatus) { if (txMeta.custodyStatus) {
this.inProcessOfSigning.delete(txId); this.inProcessOfSigning.delete(txId);
// Custodial nonces and gas params are set by the custodian, so MMI follows the approve
// workflow before the transaction parameters are sent to the keyring
this.txStateManager.setTxStatusApproved(txId);
await this._signTransaction(txId); await this._signTransaction(txId);
// MMI relies on custodian to publish transactions so exits this code path early
return; return;
} }
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
@ -2082,11 +2131,18 @@ export default class TransactionController extends EventEmitter {
* Updates the memStore in transaction controller * Updates the memStore in transaction controller
*/ */
_updateMemstore() { _updateMemstore() {
const { transactions } = this.store.getState();
const unapprovedTxs = this.txStateManager.getUnapprovedTxList(); const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
const currentNetworkTxList = this.txStateManager.getTransactions({ const currentNetworkTxList = this.txStateManager.getTransactions({
limit: MAX_MEMSTORE_TX_LIST_SIZE, limit: MAX_MEMSTORE_TX_LIST_SIZE,
}); });
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList });
this.memStore.updateState({
unapprovedTxs,
currentNetworkTxList,
transactions,
});
} }
_calculateTransactionsCost(txMeta, approvalTxMeta) { _calculateTransactionsCost(txMeta, approvalTxMeta) {
@ -2730,6 +2786,34 @@ export default class TransactionController extends EventEmitter {
); );
} }
_onIncomingTransactions({ added: transactions }) {
log.debug('Detected new incoming transactions', transactions);
const currentTransactions = this.store.getState().transactions || {};
const incomingTransactions = transactions
.filter((tx) => !this._hasTransactionHash(tx.hash, currentTransactions))
.reduce((result, tx) => {
result[tx.id] = tx;
return result;
}, {});
const updatedTransactions = {
...currentTransactions,
...incomingTransactions,
};
this.store.updateState({ transactions: updatedTransactions });
}
_onUpdatedLastFetchedBlockNumbers({ lastFetchedBlockNumbers }) {
this.store.updateState({ lastFetchedBlockNumbers });
}
_hasTransactionHash(hash, transactions) {
return Object.values(transactions).some((tx) => tx.hash === hash);
}
// Approvals // Approvals
async _requestTransactionApproval( async _requestTransactionApproval(

View File

@ -38,6 +38,7 @@ import { ORIGIN_METAMASK } from '../../../../shared/constants/app';
import { NetworkStatus } from '../../../../shared/constants/network'; import { NetworkStatus } from '../../../../shared/constants/network';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils'; import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../shared/lib/transactions-controller-utils';
import TxGasUtil from './tx-gas-utils'; import TxGasUtil from './tx-gas-utils';
import * as IncomingTransactionHelperClass from './IncomingTransactionHelper';
import TransactionController from '.'; import TransactionController from '.';
const noop = () => true; const noop = () => true;
@ -51,6 +52,16 @@ const actionId = 'DUMMY_ACTION_ID';
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
const TRANSACTION_META_MOCK = {
hash: '0x1',
id: 1,
status: TransactionStatus.confirmed,
transaction: {
from: VALID_ADDRESS,
},
time: 123456789,
};
async function flushPromises() { async function flushPromises() {
await new Promise((resolve) => setImmediate(resolve)); await new Promise((resolve) => setImmediate(resolve));
} }
@ -65,7 +76,9 @@ describe('Transaction Controller', function () {
getCurrentChainId, getCurrentChainId,
messengerMock, messengerMock,
resultCallbacksMock, resultCallbacksMock,
updateSpy; updateSpy,
incomingTransactionHelperClassMock,
incomingTransactionHelperEventMock;
beforeEach(function () { beforeEach(function () {
fragmentExists = false; fragmentExists = false;
@ -101,6 +114,16 @@ describe('Transaction Controller', function () {
call: sinon.stub(), call: sinon.stub(),
}; };
incomingTransactionHelperEventMock = sinon.spy();
incomingTransactionHelperClassMock = sinon
.stub(IncomingTransactionHelperClass, 'IncomingTransactionHelper')
.returns({
hub: {
on: incomingTransactionHelperEventMock,
},
});
txController = new TransactionController({ txController = new TransactionController({
provider, provider,
getGasPrice() { getGasPrice() {
@ -148,6 +171,10 @@ describe('Transaction Controller', function () {
); );
}); });
afterEach(function () {
incomingTransactionHelperClassMock.restore();
});
function getLastTxMeta() { function getLastTxMeta() {
return updateSpy.lastCall.args[0]; return updateSpy.lastCall.args[0];
} }
@ -3374,4 +3401,78 @@ describe('Transaction Controller', function () {
assert.deepEqual(transaction1, transaction2); assert.deepEqual(transaction1, transaction2);
}); });
}); });
describe('on incoming transaction helper transactions event', function () {
it('adds new transactions to state', async function () {
const existingTransaction = TRANSACTION_META_MOCK;
const incomingTransaction1 = {
...TRANSACTION_META_MOCK,
id: 2,
hash: '0x2',
};
const incomingTransaction2 = {
...TRANSACTION_META_MOCK,
id: 3,
hash: '0x3',
};
txController.store.getState().transactions = {
[existingTransaction.id]: existingTransaction,
};
await incomingTransactionHelperEventMock.firstCall.args[1]({
added: [incomingTransaction1, incomingTransaction2],
updated: [],
});
assert.deepEqual(txController.store.getState().transactions, {
[existingTransaction.id]: existingTransaction,
[incomingTransaction1.id]: incomingTransaction1,
[incomingTransaction2.id]: incomingTransaction2,
});
});
it('ignores new transactions if hash matches existing transaction', async function () {
const existingTransaction = TRANSACTION_META_MOCK;
const incomingTransaction1 = { ...TRANSACTION_META_MOCK, id: 2 };
const incomingTransaction2 = { ...TRANSACTION_META_MOCK, id: 3 };
txController.store.getState().transactions = {
[existingTransaction.id]: existingTransaction,
};
await incomingTransactionHelperEventMock.firstCall.args[1]({
added: [incomingTransaction1, incomingTransaction2],
updated: [],
});
assert.deepEqual(txController.store.getState().transactions, {
[existingTransaction.id]: existingTransaction,
});
});
});
describe('on incoming transaction helper updatedLastFetchedBlockNumbers event', function () {
it('updates state', async function () {
const lastFetchedBlockNumbers = {
key: 234,
};
assert.deepEqual(
txController.store.getState().lastFetchedBlockNumbers,
undefined,
);
await incomingTransactionHelperEventMock.secondCall.args[1]({
lastFetchedBlockNumbers,
});
assert.deepEqual(
txController.store.getState().lastFetchedBlockNumbers,
lastFetchedBlockNumbers,
);
});
});
}); });

View 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[]>;
}

View File

@ -51,6 +51,7 @@ export default class ComposableObservableStore extends ObservableStore {
updateStructure(config) { updateStructure(config) {
this.config = config; this.config = config;
this.removeAllListeners(); this.removeAllListeners();
const initialState = {};
for (const key of Object.keys(config)) { for (const key of Object.keys(config)) {
if (!config[key]) { if (!config[key]) {
throw new Error(`Undefined '${key}'`); throw new Error(`Undefined '${key}'`);
@ -72,7 +73,10 @@ export default class ComposableObservableStore extends ObservableStore {
}, },
); );
} }
initialState[key] = store.state ?? store.getState?.();
} }
this.updateState(initialState);
} }
/** /**

View File

@ -120,6 +120,46 @@ describe('ComposableObservableStore', () => {
}); });
}); });
it('should initialize state with all three types of stores', () => {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
const exampleController = new ExampleController({
messenger: controllerMessenger,
});
const oldExampleController = new OldExampleController();
exampleStore.putState('state');
exampleController.updateBar('state');
oldExampleController.updateBaz('state');
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({
Example: exampleController,
OldExample: oldExampleController,
Store: exampleStore,
});
expect(store.getState()).toStrictEqual({
Example: { bar: 'state' },
OldExample: { baz: 'state' },
Store: 'state',
});
});
it('should initialize falsy state', () => {
const controllerMessenger = new ControllerMessenger();
const exampleStore = new ObservableStore();
exampleStore.putState(false);
const store = new ComposableObservableStore({ controllerMessenger });
store.updateStructure({
Example: exampleStore,
});
expect(store.getState()).toStrictEqual({
Example: false,
});
});
it('should return flattened state', () => { it('should return flattened state', () => {
const controllerMessenger = new ControllerMessenger(); const controllerMessenger = new ControllerMessenger();
const fooStore = new ObservableStore({ foo: 'foo' }); const fooStore = new ObservableStore({ foo: 'foo' });

View File

@ -1,6 +1,6 @@
import { prependZero } from '../../../shared/modules/string-utils'; import { prependZero } from '../../../shared/modules/string-utils';
export default class BackupController { export default class Backup {
constructor(opts = {}) { constructor(opts = {}) {
const { const {
preferencesController, preferencesController,

View File

@ -1,6 +1,6 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import sinon from 'sinon'; import sinon from 'sinon';
import BackupController from './backup'; import Backup from './backup';
function getMockPreferencesController() { function getMockPreferencesController() {
const mcState = { const mcState = {
@ -131,7 +131,6 @@ const jsonData = JSON.stringify({
advancedGasFee: null, advancedGasFee: null,
featureFlags: { featureFlags: {
sendHexData: true, sendHexData: true,
showIncomingTransactions: true,
}, },
knownMethodData: {}, knownMethodData: {},
currentLocale: 'en', currentLocale: 'en',
@ -151,9 +150,9 @@ const jsonData = JSON.stringify({
}, },
}); });
describe('BackupController', function () { describe('Backup', function () {
const getBackupController = () => { const getBackup = () => {
return new BackupController({ return new Backup({
preferencesController: getMockPreferencesController(), preferencesController: getMockPreferencesController(),
addressBookController: getMockAddressBookController(), addressBookController: getMockAddressBookController(),
networkController: getMockNetworkController(), networkController: getMockNetworkController(),
@ -163,85 +162,81 @@ describe('BackupController', function () {
describe('constructor', function () { describe('constructor', function () {
it('should setup correctly', async function () { it('should setup correctly', async function () {
const backupController = getBackupController(); const backup = getBackup();
const selectedAddress = const selectedAddress = backup.preferencesController.getSelectedAddress();
backupController.preferencesController.getSelectedAddress();
assert.equal(selectedAddress, '0x01'); assert.equal(selectedAddress, '0x01');
}); });
it('should restore backup', async function () { it('should restore backup', async function () {
const backupController = getBackupController(); const backup = getBackup();
await backupController.restoreUserData(jsonData); await backup.restoreUserData(jsonData);
// check networks backup // check networks backup
assert.equal( assert.equal(
backupController.networkController.state.networkConfigurations[ backup.networkController.state.networkConfigurations[
'network-configuration-id-1' 'network-configuration-id-1'
].chainId, ].chainId,
'0x539', '0x539',
); );
assert.equal( assert.equal(
backupController.networkController.state.networkConfigurations[ backup.networkController.state.networkConfigurations[
'network-configuration-id-2' 'network-configuration-id-2'
].chainId, ].chainId,
'0x38', '0x38',
); );
assert.equal( assert.equal(
backupController.networkController.state.networkConfigurations[ backup.networkController.state.networkConfigurations[
'network-configuration-id-3' 'network-configuration-id-3'
].chainId, ].chainId,
'0x61', '0x61',
); );
assert.equal( assert.equal(
backupController.networkController.state.networkConfigurations[ backup.networkController.state.networkConfigurations[
'network-configuration-id-4' 'network-configuration-id-4'
].chainId, ].chainId,
'0x89', '0x89',
); );
// make sure identities are not lost after restore // make sure identities are not lost after restore
assert.equal( assert.equal(
backupController.preferencesController.store.identities[ backup.preferencesController.store.identities[
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
].lastSelected, ].lastSelected,
1655380342907, 1655380342907,
); );
assert.equal( assert.equal(
backupController.preferencesController.store.identities[ backup.preferencesController.store.identities[
'0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B' '0x295e26495CEF6F69dFA69911d9D8e4F3bBadB89B'
].name, ].name,
'Account 3', 'Account 3',
); );
assert.equal( assert.equal(
backupController.preferencesController.store.lostIdentities[ backup.preferencesController.store.lostIdentities[
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435' '0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
].lastSelected, ].lastSelected,
1655379648197, 1655379648197,
); );
assert.equal( assert.equal(
backupController.preferencesController.store.lostIdentities[ backup.preferencesController.store.lostIdentities[
'0xfd59bbe569376e3d3e4430297c3c69ea93f77435' '0xfd59bbe569376e3d3e4430297c3c69ea93f77435'
].name, ].name,
'Ledger 1', 'Ledger 1',
); );
// make sure selected address is not lost after restore // make sure selected address is not lost after restore
assert.equal( assert.equal(backup.preferencesController.store.selectedAddress, '0x01');
backupController.preferencesController.store.selectedAddress,
'0x01',
);
// check address book backup // check address book backup
assert.equal( assert.equal(
backupController.addressBookController.store.addressBook['0x61'][ backup.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06' '0x42EB768f2244C8811C63729A21A3569731535f06'
].chainId, ].chainId,
'0x61', '0x61',
); );
assert.equal( assert.equal(
backupController.addressBookController.store.addressBook['0x61'][ backup.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06' '0x42EB768f2244C8811C63729A21A3569731535f06'
].address, ].address,
'0x42EB768f2244C8811C63729A21A3569731535f06', '0x42EB768f2244C8811C63729A21A3569731535f06',
); );
assert.equal( assert.equal(
backupController.addressBookController.store.addressBook['0x61'][ backup.addressBookController.store.addressBook['0x61'][
'0x42EB768f2244C8811C63729A21A3569731535f06' '0x42EB768f2244C8811C63729A21A3569731535f06'
].isEns, ].isEns,
false, false,

View File

@ -16,6 +16,7 @@ export default class ExtensionStore {
// once data persistence fails once and it flips true we don't send further // once data persistence fails once and it flips true we don't send further
// data persistence errors to sentry // data persistence errors to sentry
this.dataPersistenceFailing = false; this.dataPersistenceFailing = false;
this.mostRecentRetrievedState = null;
} }
setMetadata(initMetaData) { setMetadata(initMetaData) {
@ -66,8 +67,10 @@ export default class ExtensionStore {
// extension.storage.local always returns an obj // extension.storage.local always returns an obj
// if the object is empty, treat it as undefined // if the object is empty, treat it as undefined
if (isEmpty(result)) { if (isEmpty(result)) {
this.mostRecentRetrievedState = null;
return undefined; return undefined;
} }
this.mostRecentRetrievedState = result;
return result; return result;
} }

View File

@ -2,11 +2,12 @@ import browser from 'webextension-polyfill';
import LocalStore from './local-store'; import LocalStore from './local-store';
jest.mock('webextension-polyfill', () => ({ jest.mock('webextension-polyfill', () => ({
runtime: { lastError: null },
storage: { local: true }, storage: { local: true },
})); }));
const setup = ({ isSupported }) => { const setup = ({ localMock = jest.fn() } = {}) => {
browser.storage.local = isSupported; browser.storage.local = localMock;
return new LocalStore(); return new LocalStore();
}; };
describe('LocalStore', () => { describe('LocalStore', () => {
@ -15,21 +16,27 @@ describe('LocalStore', () => {
}); });
describe('contructor', () => { describe('contructor', () => {
it('should set isSupported property to false when browser does not support local storage', () => { it('should set isSupported property to false when browser does not support local storage', () => {
const localStore = setup({ isSupported: false }); const localStore = setup({ localMock: false });
expect(localStore.isSupported).toBe(false); expect(localStore.isSupported).toBe(false);
}); });
it('should set isSupported property to true when browser supports local storage', () => { it('should set isSupported property to true when browser supports local storage', () => {
const localStore = setup({ isSupported: true }); const localStore = setup();
expect(localStore.isSupported).toBe(true); expect(localStore.isSupported).toBe(true);
}); });
it('should initialize mostRecentRetrievedState to null', () => {
const localStore = setup({ localMock: false });
expect(localStore.mostRecentRetrievedState).toBeNull();
});
}); });
describe('setMetadata', () => { describe('setMetadata', () => {
it('should set the metadata property on LocalStore', () => { it('should set the metadata property on LocalStore', () => {
const metadata = { version: 74 }; const metadata = { version: 74 };
const localStore = setup({ isSupported: true }); const localStore = setup();
localStore.setMetadata(metadata); localStore.setMetadata(metadata);
expect(localStore.metadata).toStrictEqual(metadata); expect(localStore.metadata).toStrictEqual(metadata);
@ -38,21 +45,21 @@ describe('LocalStore', () => {
describe('set', () => { describe('set', () => {
it('should throw an error if called in a browser that does not support local storage', async () => { it('should throw an error if called in a browser that does not support local storage', async () => {
const localStore = setup({ isSupported: false }); const localStore = setup({ localMock: false });
await expect(() => localStore.set()).rejects.toThrow( await expect(() => localStore.set()).rejects.toThrow(
'Metamask- cannot persist state to local store as this browser does not support this action', 'Metamask- cannot persist state to local store as this browser does not support this action',
); );
}); });
it('should throw an error if not passed a truthy value as an argument', async () => { it('should throw an error if not passed a truthy value as an argument', async () => {
const localStore = setup({ isSupported: true }); const localStore = setup();
await expect(() => localStore.set()).rejects.toThrow( await expect(() => localStore.set()).rejects.toThrow(
'MetaMask - updated state is missing', 'MetaMask - updated state is missing',
); );
}); });
it('should throw an error if passed a valid argument but metadata has not yet been set', async () => { it('should throw an error if passed a valid argument but metadata has not yet been set', async () => {
const localStore = setup({ isSupported: true }); const localStore = setup();
await expect(() => await expect(() =>
localStore.set({ appState: { test: true } }), localStore.set({ appState: { test: true } }),
).rejects.toThrow( ).rejects.toThrow(
@ -61,7 +68,7 @@ describe('LocalStore', () => {
}); });
it('should not throw if passed a valid argument and metadata has been set', async () => { it('should not throw if passed a valid argument and metadata has been set', async () => {
const localStore = setup({ isSupported: true }); const localStore = setup();
localStore.setMetadata({ version: 74 }); localStore.setMetadata({ version: 74 });
await expect(async function () { await expect(async function () {
localStore.set({ appState: { test: true } }); localStore.set({ appState: { test: true } });
@ -71,9 +78,39 @@ describe('LocalStore', () => {
describe('get', () => { describe('get', () => {
it('should return undefined if called in a browser that does not support local storage', async () => { it('should return undefined if called in a browser that does not support local storage', async () => {
const localStore = setup({ isSupported: false }); const localStore = setup({ localMock: false });
const result = await localStore.get(); const result = await localStore.get();
expect(result).toStrictEqual(undefined); expect(result).toStrictEqual(undefined);
}); });
it('should update mostRecentRetrievedState', async () => {
const localStore = setup({
localMock: {
get: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ appState: { test: true } }),
),
},
});
await localStore.get();
expect(localStore.mostRecentRetrievedState).toStrictEqual({
appState: { test: true },
});
});
it('should reset mostRecentRetrievedState to null if storage.local is empty', async () => {
const localStore = setup({
localMock: {
get: jest.fn().mockImplementation(() => Promise.resolve({})),
},
});
await localStore.get();
expect(localStore.mostRecentRetrievedState).toStrictEqual(null);
});
}); });
}); });

View File

@ -1,4 +1,5 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import log from 'loglevel';
/** /**
* @typedef {object} Migration * @typedef {object} Migration
@ -36,6 +37,8 @@ export default class Migrator extends EventEmitter {
// perform each migration // perform each migration
for (const migration of pendingMigrations) { for (const migration of pendingMigrations) {
try { try {
log.info(`Running migration ${migration.version}...`);
// attempt migration and validate // attempt migration and validate
const migratedData = await migration.migrate(versionedData); const migratedData = await migration.migrate(versionedData);
if (!migratedData.data) { if (!migratedData.data) {
@ -52,6 +55,8 @@ export default class Migrator extends EventEmitter {
// accept the migration as good // accept the migration as good
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
versionedData = migratedData; versionedData = migratedData;
log.info(`Migration ${migration.version} complete`);
} catch (err) { } catch (err) {
// rewrite error message to add context without clobbering stack // rewrite error message to add context without clobbering stack
const originalErrorMessage = err.message; const originalErrorMessage = err.message;

View File

@ -15,6 +15,7 @@ export default class ReadOnlyNetworkStore {
this._initialized = false; this._initialized = false;
this._initializing = this._init(); this._initializing = this._init();
this._state = undefined; this._state = undefined;
this.mostRecentRetrievedState = null;
} }
/** /**
@ -47,6 +48,11 @@ export default class ReadOnlyNetworkStore {
if (!this._initialized) { if (!this._initialized) {
await this._initializing; await this._initializing;
} }
// Delay setting this until after the first read, to match the
// behavior of the local store.
if (!this.mostRecentRetrievedState) {
this.mostRecentRetrievedState = this._state;
}
return this._state; return this._state;
} }

View File

@ -1,16 +1,16 @@
import { ethErrors, errorCodes } from 'eth-rpc-errors';
import validUrl from 'valid-url';
import { omit } from 'lodash';
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
import { errorCodes, ethErrors } from 'eth-rpc-errors';
import { omit } from 'lodash';
import { import {
MESSAGE_TYPE, MESSAGE_TYPE,
UNKNOWN_TICKER_SYMBOL, UNKNOWN_TICKER_SYMBOL,
} from '../../../../../shared/constants/app'; } from '../../../../../shared/constants/app';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
} from '../../../../../shared/modules/network.utils'; } from '../../../../../shared/modules/network.utils';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; import { getValidUrl } from '../../util';
const addEthereumChain = { const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
@ -83,27 +83,25 @@ async function addEthereumChainHandler(
); );
} }
const isLocalhost = (strUrl) => { function isLocalhostOrHttps(urlString) {
try { const url = getValidUrl(urlString);
const url = new URL(strUrl);
return url.hostname === 'localhost' || url.hostname === '127.0.0.1'; return (
} catch (error) { url !== null &&
return false; (url.hostname === 'localhost' ||
url.hostname === '127.0.0.1' ||
url.protocol === 'https:')
);
} }
};
const firstValidRPCUrl = Array.isArray(rpcUrls) const firstValidRPCUrl = Array.isArray(rpcUrls)
? rpcUrls.find( ? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl))
(rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl),
)
: null; : null;
const firstValidBlockExplorerUrl = const firstValidBlockExplorerUrl =
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls) blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
? blockExplorerUrls.find( ? blockExplorerUrls.find((blockExplorerUrl) =>
(blockExplorerUrl) => isLocalhostOrHttps(blockExplorerUrl),
isLocalhost(blockExplorerUrl) ||
validUrl.isHttpsUri(blockExplorerUrl),
) )
: null; : null;

View File

@ -1,18 +1,15 @@
import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics';
/** /**
* This RPC method is called by the inpage provider whenever it detects the * This RPC method is called by the inpage provider whenever it detects the
* accessing of a non-existent property on our window.web3 shim. * accessing of a non-existent property on our window.web3 shim. We use this
* We collect this data to understand which sites are breaking due to the * to alert the user that they are using a legacy dapp, and will have to take
* removal of our window.web3. * further steps to be able to use it.
*/ */
const logWeb3ShimUsage = { const logWeb3ShimUsage = {
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE], methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
implementation: logWeb3ShimUsageHandler, implementation: logWeb3ShimUsageHandler,
hookNames: { hookNames: {
sendMetrics: true,
getWeb3ShimUsageState: true, getWeb3ShimUsageState: true,
setWeb3ShimUsageRecorded: true, setWeb3ShimUsageRecorded: true,
}, },
@ -21,7 +18,6 @@ export default logWeb3ShimUsage;
/** /**
* @typedef {object} LogWeb3ShimUsageOptions * @typedef {object} LogWeb3ShimUsageOptions
* @property {Function} sendMetrics - A function that registers a metrics event.
* @property {Function} getWeb3ShimUsageState - A function that gets web3 shim * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim
* usage state for the given origin. * usage state for the given origin.
* @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim
@ -40,24 +36,11 @@ function logWeb3ShimUsageHandler(
res, res,
_next, _next,
end, end,
{ sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, { getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
) { ) {
const { origin } = req; const { origin } = req;
if (getWeb3ShimUsageState(origin) === undefined) { if (getWeb3ShimUsageState(origin) === undefined) {
setWeb3ShimUsageRecorded(origin); setWeb3ShimUsageRecorded(origin);
sendMetrics(
{
event: `Website Accessed window.web3 Shim`,
category: MetaMetricsEventCategory.InpageProvider,
referrer: {
url: origin,
},
},
{
excludeMetaMetricsId: true,
},
);
} }
res.result = true; res.result = true;

View 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;
};

View File

@ -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();
};

View File

@ -23,58 +23,172 @@ export const ERROR_URL_ALLOWLIST = {
SEGMENT: 'segment.io', SEGMENT: 'segment.io',
}; };
// This describes the subset of Redux state attached to errors sent to Sentry // This describes the subset of background controller state attached to errors
// These properties have some potential to be useful for debugging, and they do // sent to Sentry These properties have some potential to be useful for
// not contain any identifiable information. // debugging, and they do not contain any identifiable information.
export const SENTRY_STATE = { export const SENTRY_BACKGROUND_STATE = {
gas: true, AccountTracker: {
history: true, currentBlockGasLimit: true,
metamask: { },
AlertController: {
alertEnabledness: true, alertEnabledness: true,
completedOnboarding: true, },
AppMetadataController: {
currentAppVersion: true,
previousAppVersion: true,
previousMigrationVersion: true,
currentMigrationVersion: true,
},
AppStateController: {
connectedStatusPopoverHasBeenShown: true, connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: true,
},
CurrencyController: {
conversionDate: true, conversionDate: true,
conversionRate: true, conversionRate: true,
currentBlockGasLimit: true,
currentCurrency: true, currentCurrency: true,
currentLocale: true,
customNonceValue: true,
defaultHomeActiveTabName: true,
desktopEnabled: true,
featureFlags: true,
firstTimeFlowType: true,
forgottenPassword: true,
incomingTxLastFetchedBlockByChainId: true,
ipfsGateway: true,
isAccountMenuOpen: true,
isInitialized: true,
isUnlocked: true,
metaMetricsId: true,
nativeCurrency: true, nativeCurrency: true,
},
DecryptMessageController: {
unapprovedDecryptMsgCount: true,
},
DesktopController: {
desktopEnabled: true,
},
EncryptionPublicKeyController: {
unapprovedEncryptionPublicKeyMsgCount: true,
},
KeyringController: {
isUnlocked: true,
},
MetaMetricsController: {
metaMetricsId: true,
participateInMetaMetrics: true,
},
NetworkController: {
networkId: true, networkId: true,
networkStatus: true, networkStatus: true,
nextNonce: true,
participateInMetaMetrics: true,
preferences: true,
providerConfig: { providerConfig: {
nickname: true, nickname: true,
ticker: true, ticker: true,
type: true, type: true,
}, },
},
OnboardingController: {
completedOnboarding: true,
firstTimeFlowType: true,
seedPhraseBackedUp: true, seedPhraseBackedUp: true,
unapprovedDecryptMsgCount: true, },
unapprovedEncryptionPublicKeyMsgCount: true, PreferencesController: {
unapprovedMsgCount: true, currentLocale: true,
unapprovedPersonalMsgCount: true, featureFlags: true,
unapprovedTypedMessagesCount: true, forgottenPassword: true,
ipfsGateway: true,
preferences: true,
useBlockie: true, useBlockie: true,
useNonceField: true, useNonceField: true,
usePhishDetect: true, usePhishDetect: true,
},
SignatureController: {
unapprovedMsgCount: true,
unapprovedPersonalMsgCount: true,
unapprovedTypedMessagesCount: true,
},
};
const flattenedBackgroundStateMask = Object.values(
SENTRY_BACKGROUND_STATE,
).reduce((partialBackgroundState, controllerState) => {
return {
...partialBackgroundState,
...controllerState,
};
}, {});
// This describes the subset of Redux state attached to errors sent to Sentry
// These properties have some potential to be useful for debugging, and they do
// not contain any identifiable information.
export const SENTRY_UI_STATE = {
gas: true,
history: true,
metamask: {
...flattenedBackgroundStateMask,
// This property comes from the background but isn't in controller state
isInitialized: true,
// These properties are in the `metamask` slice but not in the background state
customNonceValue: true,
isAccountMenuOpen: true,
nextNonce: true,
welcomeScreenSeen: true, welcomeScreenSeen: true,
}, },
unconnectedAccount: true, unconnectedAccount: true,
}; };
/**
* Returns whether MetaMetrics is enabled, given the application state.
*
* @param {{ state: unknown} | { persistedState: unknown }} appState - Application state
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
* is enabled, `false` otherwise.
*/
function getMetaMetricsEnabledFromAppState(appState) {
// during initialization after loading persisted state
if (appState.persistedState) {
return getMetaMetricsEnabledFromPersistedState(appState.persistedState);
// After initialization
} else if (appState.state) {
// UI
if (appState.state.metamask) {
return Boolean(appState.state.metamask.participateInMetaMetrics);
}
// background
return Boolean(
appState.state.MetaMetricsController?.participateInMetaMetrics,
);
}
// during initialization, before first persisted state is read
return false;
}
/**
* Returns whether MetaMetrics is enabled, given the persisted state.
*
* @param {unknown} persistedState - Application state
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
* is enabled, `false` otherwise.
*/
function getMetaMetricsEnabledFromPersistedState(persistedState) {
return Boolean(
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
);
}
/**
* Returns whether onboarding has completed, given the application state.
*
* @param {Record<string, unknown>} appState - Application state
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
* is enabled, `false` otherwise.
*/
function getOnboardingCompleteFromAppState(appState) {
// during initialization after loading persisted state
if (appState.persistedState) {
return Boolean(
appState.persistedState.data?.OnboardingController?.completedOnboarding,
);
// After initialization
} else if (appState.state) {
// UI
if (appState.state.metamask) {
return Boolean(appState.state.metamask.completedOnboarding);
}
// background
return Boolean(appState.state.OnboardingController?.completedOnboarding);
}
// during initialization, before first persisted state is read
return false;
}
export default function setupSentry({ release, getState }) { export default function setupSentry({ release, getState }) {
if (!release) { if (!release) {
throw new Error('Missing release'); throw new Error('Missing release');
@ -112,22 +226,21 @@ export default function setupSentry({ release, getState }) {
} }
/** /**
* A function that returns whether MetaMetrics is enabled. This should also * Returns whether MetaMetrics is enabled. If the application hasn't yet
* return `false` if state has not yet been initialzed. * been initialized, the persisted state will be used (if any).
* *
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics * @returns `true` if MetaMetrics is enabled, `false` otherwise.
* is enabled, `false` otherwise.
*/ */
async function getMetaMetricsEnabled() { async function getMetaMetricsEnabled() {
const appState = getState(); const appState = getState();
if (Object.keys(appState) > 0) { if (appState.state || appState.persistedState) {
return Boolean(appState?.store?.metamask?.participateInMetaMetrics); return getMetaMetricsEnabledFromAppState(appState);
} }
// If we reach here, it means the error was thrown before initialization
// completed, and before we loaded the persisted state for the first time.
try { try {
const persistedState = await globalThis.stateHooks.getPersistedState(); const persistedState = await globalThis.stateHooks.getPersistedState();
return Boolean( return getMetaMetricsEnabledFromPersistedState(persistedState);
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return false; return false;
@ -269,17 +382,15 @@ function hideUrlIfNotInternal(url) {
*/ */
export function beforeBreadcrumb(getState) { export function beforeBreadcrumb(getState) {
return (breadcrumb) => { return (breadcrumb) => {
if (getState) { if (!getState) {
const appState = getState();
if (
Object.values(appState).length &&
(!appState?.store?.metamask?.participateInMetaMetrics ||
!appState?.store?.metamask?.completedOnboarding ||
breadcrumb?.category === 'ui.input')
) {
return null; return null;
} }
} else { const appState = getState();
if (
!getMetaMetricsEnabledFromAppState(appState) ||
!getOnboardingCompleteFromAppState(appState) ||
breadcrumb?.category === 'ui.input'
) {
return null; return null;
} }
const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb); const newBreadcrumb = removeUrlsFromBreadCrumb(breadcrumb);

View File

@ -1,24 +1,27 @@
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { import {
TransactionEnvelopeType,
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
TransactionEnvelopeType,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
addUrlProtocolPrefix,
deferredPromise, deferredPromise,
formatTxMetaForRpcResult,
getEnvironmentType, getEnvironmentType,
getPlatform, getPlatform,
formatTxMetaForRpcResult, getValidUrl,
isWebUrl,
} from './util'; } from './util';
describe('app utils', () => { describe('app utils', () => {
@ -73,6 +76,39 @@ describe('app utils', () => {
}); });
}); });
describe('URL utils', () => {
it('should test addUrlProtocolPrefix', () => {
expect(addUrlProtocolPrefix('http://example.com')).toStrictEqual(
'http://example.com',
);
expect(addUrlProtocolPrefix('https://example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('exa mple.com')).toStrictEqual(null);
});
it('should test isWebUrl', () => {
expect(isWebUrl('http://example.com')).toStrictEqual(true);
expect(isWebUrl('https://example.com')).toStrictEqual(true);
expect(isWebUrl('https://exa mple.com')).toStrictEqual(false);
expect(isWebUrl('')).toStrictEqual(false);
});
it('should test getValidUrl', () => {
expect(getValidUrl('http://example.com').toString()).toStrictEqual(
'http://example.com/',
);
expect(getValidUrl('https://example.com').toString()).toStrictEqual(
'https://example.com/',
);
expect(getValidUrl('https://exa%20mple.com')).toStrictEqual(null);
expect(getValidUrl('')).toStrictEqual(null);
});
});
describe('isPrefixedFormattedHexString', () => { describe('isPrefixedFormattedHexString', () => {
it('should return true for valid hex strings', () => { it('should return true for valid hex strings', () => {
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true); expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);

View File

@ -1,24 +1,24 @@
import urlLib from 'url';
import { AccessList } from '@ethereumjs/tx';
import BN from 'bn.js'; import BN from 'bn.js';
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { AccessList } from '@ethereumjs/tx';
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_BRAVE,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_BRAVE, PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
TransactionEnvelopeType, TransactionEnvelopeType,
TransactionMeta, TransactionMeta,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
/** /**
* @see {@link getEnvironmentType} * @see {@link getEnvironmentType}
@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) {
} }
export { export {
getPlatform,
getEnvironmentType,
hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
addHexPrefix, addHexPrefix,
getChainType,
checkAlarmExists, checkAlarmExists,
getChainType,
getEnvironmentType,
getPlatform,
hexToBn,
}; };
// Taken from https://stackoverflow.com/a/1349426/3696652 // Taken from https://stackoverflow.com/a/1349426/3696652
@ -235,10 +235,43 @@ export function previousValueComparator<A>(
} }
export function addUrlProtocolPrefix(urlString: string) { export function addUrlProtocolPrefix(urlString: string) {
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { let trimmed = urlString.trim();
return `https://${urlString}`;
if (trimmed.length && !urlLib.parse(trimmed).protocol) {
trimmed = `https://${trimmed}`;
} }
return urlString;
if (getValidUrl(trimmed) !== null) {
return trimmed;
}
return null;
}
export function getValidUrl(urlString: string): URL | null {
try {
const url = new URL(urlString);
if (url.hostname.length === 0 || url.pathname.length === 0) {
return null;
}
if (url.hostname !== decodeURIComponent(url.hostname)) {
return null; // will happen if there's a %, a space, or other invalid character in the hostname
}
return url;
} catch (error) {
return null;
}
}
export function isWebUrl(urlString: string): boolean {
const url = getValidUrl(urlString);
return (
url !== null && (url.protocol === 'https:' || url.protocol === 'http:')
);
} }
interface FormattedTransactionMeta { interface FormattedTransactionMeta {

View File

@ -145,27 +145,21 @@ describe('MetaMaskController', function () {
metamaskController.addNewAccount(1), metamaskController.addNewAccount(1),
metamaskController.addNewAccount(1), metamaskController.addNewAccount(1),
]); ]);
assert.deepEqual( assert.equal(addNewAccountResult1, addNewAccountResult2);
Object.keys(addNewAccountResult1.identities),
Object.keys(addNewAccountResult2.identities),
);
}); });
it('two successive calls with same accountCount give same result', async function () { it('two successive calls with same accountCount give same result', async function () {
await metamaskController.createNewVaultAndKeychain('test@123'); await metamaskController.createNewVaultAndKeychain('test@123');
const addNewAccountResult1 = await metamaskController.addNewAccount(1); const addNewAccountResult1 = await metamaskController.addNewAccount(1);
const addNewAccountResult2 = await metamaskController.addNewAccount(1); const addNewAccountResult2 = await metamaskController.addNewAccount(1);
assert.deepEqual( assert.equal(addNewAccountResult1, addNewAccountResult2);
Object.keys(addNewAccountResult1.identities),
Object.keys(addNewAccountResult2.identities),
);
}); });
it('two successive calls with different accountCount give different results', async function () { it('two successive calls with different accountCount give different results', async function () {
await metamaskController.createNewVaultAndKeychain('test@123'); await metamaskController.createNewVaultAndKeychain('test@123');
const addNewAccountResult1 = await metamaskController.addNewAccount(1); const addNewAccountResult1 = await metamaskController.addNewAccount(1);
const addNewAccountResult2 = await metamaskController.addNewAccount(2); const addNewAccountResult2 = await metamaskController.addNewAccount(2);
assert.notDeepEqual(addNewAccountResult1, addNewAccountResult2); assert.notEqual(addNewAccountResult1, addNewAccountResult2);
}); });
}); });
@ -178,14 +172,14 @@ describe('MetaMaskController', function () {
await metamaskController.createNewVaultAndKeychain('test@123'); await metamaskController.createNewVaultAndKeychain('test@123');
await Promise.all([ await Promise.all([
metamaskController.importAccountWithStrategy('Private Key', [ metamaskController.importAccountWithStrategy('privateKey', [
importPrivkey, importPrivkey,
]), ]),
Promise.resolve(1).then(() => { Promise.resolve(1).then(() => {
keyringControllerState1 = JSON.stringify( keyringControllerState1 = JSON.stringify(
metamaskController.keyringController.memStore.getState(), metamaskController.keyringController.memStore.getState(),
); );
metamaskController.importAccountWithStrategy('Private Key', [ metamaskController.importAccountWithStrategy('privateKey', [
importPrivkey, importPrivkey,
]); ]);
}), }),
@ -226,6 +220,14 @@ describe('MetaMaskController', function () {
}); });
}); });
describe('#setLocked', function () {
it('should lock the wallet', async function () {
const { isUnlocked, keyrings } = await metamaskController.setLocked();
assert(!isUnlocked);
assert.deepEqual(keyrings, []);
});
});
describe('#addToken', function () { describe('#addToken', function () {
const address = '0x514910771af9ca656af840dff83e8264ecf986ca'; const address = '0x514910771af9ca656af840dff83e8264ecf986ca';
const symbol = 'LINK'; const symbol = 'LINK';

Some files were not shown because too many files have changed in this diff Show More